mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-06 07:49:52 +03:00
Compare commits
2050 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f444ca31e0 | ||
|
|
3cd67741d6 | ||
|
|
4a4ea0e749 | ||
|
|
13a24e4d85 | ||
|
|
72a515faab | ||
|
|
4fdf1d810e | ||
|
|
d440e6dc9a | ||
|
|
8a12248bc5 | ||
|
|
997b05528b | ||
|
|
8bb8dae763 | ||
|
|
9eb71cfe95 | ||
|
|
21c6742725 | ||
|
|
1809ceb380 | ||
|
|
4a1b78636b | ||
|
|
0521bbb66e | ||
|
|
0ab8692e36 | ||
|
|
5a659ff6c9 | ||
|
|
fbc2e40fba | ||
|
|
e8e73e1b83 | ||
|
|
76af1b73c1 | ||
|
|
108f91b44f | ||
|
|
eaeecc2f84 | ||
|
|
698ab4b72e | ||
|
|
b2c5a387bb | ||
|
|
f901559499 | ||
|
|
2d4fb09e0e | ||
|
|
c3ee89e8cd | ||
|
|
33884a9c94 | ||
|
|
0d43703f16 | ||
|
|
d248d5cc21 | ||
|
|
03445377b5 | ||
|
|
bf1c2b8a6c | ||
|
|
3535e4eeee | ||
|
|
db86c564b3 | ||
|
|
003ab39c97 | ||
|
|
5c3441aa88 | ||
|
|
2d0de0d976 | ||
|
|
16be0e1cbc | ||
|
|
68c924427e | ||
|
|
a9d5133917 | ||
|
|
185188d964 | ||
|
|
f12cbb1f32 | ||
|
|
808385fb8a | ||
|
|
98b544d789 | ||
|
|
c85a8f80e5 | ||
|
|
9a19464eac | ||
|
|
5f018310e3 | ||
|
|
94d4d13a47 | ||
|
|
496b335780 | ||
|
|
747aabef0a | ||
|
|
9d22609370 | ||
|
|
ebe9dc8fa9 | ||
|
|
7e2f3f610f | ||
|
|
de64e95441 | ||
|
|
b610bcd4bb | ||
|
|
6094804e5b | ||
|
|
4565e64f63 | ||
|
|
0a062b17da | ||
|
|
aa4fab7296 | ||
|
|
bc17e2cd94 | ||
|
|
f877aaa11d | ||
|
|
1513e1cec5 | ||
|
|
10cffd5c6f | ||
|
|
4e8771ead2 | ||
|
|
614f05a286 | ||
|
|
21b2fb46de | ||
|
|
0dfcd10505 | ||
|
|
26aa55346a | ||
|
|
f929a8e843 | ||
|
|
5bb125545d | ||
|
|
c6c4a5bfb7 | ||
|
|
697daeeefd | ||
|
|
03d4e560e5 | ||
|
|
8f35be4edf | ||
|
|
670a2bbb4a | ||
|
|
07d35fd19c | ||
|
|
120bdf653d | ||
|
|
21e6775182 | ||
|
|
8e48348d10 | ||
|
|
28ee4f0255 | ||
|
|
85d68129d8 | ||
|
|
a049f063a1 | ||
|
|
4bd95b77c9 | ||
|
|
adaa58c499 | ||
|
|
9320316041 | ||
|
|
a735db7e8f | ||
|
|
3edbf68de0 | ||
|
|
d8f92e509a | ||
|
|
798e20a266 | ||
|
|
bd08b7e0e4 | ||
|
|
685dec4c8e | ||
|
|
995b759230 | ||
|
|
f816bbde62 | ||
|
|
a130dabda5 | ||
|
|
f60c692d59 | ||
|
|
5f0d49e13b | ||
|
|
8bab7579ee | ||
|
|
849610950f | ||
|
|
fd7b823f52 | ||
|
|
210637fde3 | ||
|
|
5bae37275e | ||
|
|
a801af1e89 | ||
|
|
0292d55ccc | ||
|
|
064b6ba53f | ||
|
|
c4f3046167 | ||
|
|
7d93ecb949 | ||
|
|
3f1c30c948 | ||
|
|
c7b565f033 | ||
|
|
96d37ea289 | ||
|
|
32aa4923dc | ||
|
|
9bdebb4960 | ||
|
|
19f983c657 | ||
|
|
dd8d4b29e5 | ||
|
|
0f877b67de | ||
|
|
c06017ccf6 | ||
|
|
aa741ae09b | ||
|
|
5f35659fa7 | ||
|
|
2cb55be1cc | ||
|
|
6767ec7208 | ||
|
|
f27916274a | ||
|
|
b59681a939 | ||
|
|
216fcd4939 | ||
|
|
19d7e8dcd0 | ||
|
|
93f3583527 | ||
|
|
66f3c57c8e | ||
|
|
e44487b67f | ||
|
|
b9a8bbd1a9 | ||
|
|
f773f03c11 | ||
|
|
1435409540 | ||
|
|
ee9682ae0a | ||
|
|
6964620bd4 | ||
|
|
079cae767c | ||
|
|
f66832c7f3 | ||
|
|
e71542d9aa | ||
|
|
00dcdecc30 | ||
|
|
3c1bfa0f3e | ||
|
|
55bcc4410b | ||
|
|
e40d01ff87 | ||
|
|
ed11260ffa | ||
|
|
4ca7da684e | ||
|
|
f0a2f4082c | ||
|
|
801c1aedaa | ||
|
|
67f7cc53b3 | ||
|
|
b78f6da35b | ||
|
|
b251fa4f26 | ||
|
|
712c627bbd | ||
|
|
eb895b44ae | ||
|
|
ebd7d86502 | ||
|
|
4fac364600 | ||
|
|
c79d5f2890 | ||
|
|
38f42445aa | ||
|
|
e36f6db59d | ||
|
|
662df4c902 | ||
|
|
33efb1a440 | ||
|
|
ef3d42b619 | ||
|
|
57f337ce22 | ||
|
|
9304cc6e47 | ||
|
|
6d45dfb925 | ||
|
|
d373309fea | ||
|
|
a001a31401 | ||
|
|
31cfdc6604 | ||
|
|
37ac684d24 | ||
|
|
e52753e83c | ||
|
|
9a6e84c68a | ||
|
|
b7e6552557 | ||
|
|
ca8ed2d1a5 | ||
|
|
a9b01e05c9 | ||
|
|
5234a4477f | ||
|
|
c5257d7ccf | ||
|
|
0e451f0a82 | ||
|
|
5a472cb6ee | ||
|
|
594a48afed | ||
|
|
03ae9735ef | ||
|
|
87c9036b87 | ||
|
|
faa019863b | ||
|
|
2bdd4afd23 | ||
|
|
d39fc8221e | ||
|
|
3203d03252 | ||
|
|
6b99fb0404 | ||
|
|
ffdcbac1e0 | ||
|
|
e78b54dc23 | ||
|
|
c4e9f3c328 | ||
|
|
6187378388 | ||
|
|
08f5c4b674 | ||
|
|
1c3b1f5790 | ||
|
|
1e7835787b | ||
|
|
0881c574b2 | ||
|
|
63f35170c6 | ||
|
|
8e56cbfa63 | ||
|
|
81541d0323 | ||
|
|
dbaefe4d50 | ||
|
|
f94ac6b067 | ||
|
|
4611ed49b0 | ||
|
|
fc0fd625d3 | ||
|
|
aa88c0793b | ||
|
|
8cbac95b99 | ||
|
|
8b1015b706 | ||
|
|
621f35b1d5 | ||
|
|
e7d9978e02 | ||
|
|
0c3a83d3b3 | ||
|
|
59c4866530 | ||
|
|
1681d99238 | ||
|
|
5d69ce18a2 | ||
|
|
87cea5865a | ||
|
|
10c03a3f6b | ||
|
|
60ab5a3e42 | ||
|
|
95944aa7a6 | ||
|
|
da53d063e7 | ||
|
|
90b2ba14c6 | ||
|
|
ffbaff8028 | ||
|
|
34fc9f3047 | ||
|
|
812df3640b | ||
|
|
e501ac31f5 | ||
|
|
4fde9e7c54 | ||
|
|
fd7d8ca532 | ||
|
|
01db9db6ae | ||
|
|
fb0d9454ef | ||
|
|
cd4d0f5abe | ||
|
|
f9f87ddc0e | ||
|
|
fa4281cc63 | ||
|
|
58ddee98f4 | ||
|
|
aac0a375c2 | ||
|
|
2528e15a9c | ||
|
|
5d1c007777 | ||
|
|
7ea0caaab7 | ||
|
|
f1800d9250 | ||
|
|
a541a56caa | ||
|
|
50b22dd265 | ||
|
|
c3dd5e002a | ||
|
|
e0130affb7 | ||
|
|
8e6434cf7f | ||
|
|
1eaa16995b | ||
|
|
8ccc708532 | ||
|
|
4dee945632 | ||
|
|
91d6249ada | ||
|
|
52cd8fa3e8 | ||
|
|
e5b98dadde | ||
|
|
465f14a113 | ||
|
|
fe0d0bb11c | ||
|
|
fc896914e9 | ||
|
|
eef130e229 | ||
|
|
88a01426c1 | ||
|
|
ed685d4a5e | ||
|
|
c8603dcd81 | ||
|
|
d728b1c116 | ||
|
|
b5203e11f0 | ||
|
|
23c2f0ceba | ||
|
|
30f491b434 | ||
|
|
d5aac1789e | ||
|
|
670a5499dd | ||
|
|
2d011a899e | ||
|
|
ab040e120e | ||
|
|
596872f5aa | ||
|
|
93066e4836 | ||
|
|
c9dd2d4a72 | ||
|
|
505aa1f945 | ||
|
|
a2e41d6d1e | ||
|
|
f096c1b632 | ||
|
|
77768f9bdc | ||
|
|
7c02894e02 | ||
|
|
554425b727 | ||
|
|
6b0ad1f3f6 | ||
|
|
243da8851c | ||
|
|
68efcf3742 | ||
|
|
b35abd744b | ||
|
|
64fbc1e4aa | ||
|
|
6814f54e64 | ||
|
|
ccfbdfbd0f | ||
|
|
00ca334fd3 | ||
|
|
607f2cfa71 | ||
|
|
bf46efd326 | ||
|
|
715f86b717 | ||
|
|
ccd6b132cc | ||
|
|
e7dca3d2f4 | ||
|
|
d60cbc6a02 | ||
|
|
83d5b919d6 | ||
|
|
9ca16bd2c8 | ||
|
|
c48b7a9a5d | ||
|
|
5cba925717 | ||
|
|
e802812fc6 | ||
|
|
978799f3a7 | ||
|
|
7d38db705f | ||
|
|
bf1979705b | ||
|
|
a79d176793 | ||
|
|
b9d7a6ac85 | ||
|
|
d61876298e | ||
|
|
9787dfc0a0 | ||
|
|
c248830042 | ||
|
|
6e22e69fec | ||
|
|
cfe5cd9bff | ||
|
|
b8c6efffcd | ||
|
|
32739fe77b | ||
|
|
171cbd3bb1 | ||
|
|
b0451024f8 | ||
|
|
c3aa64227e | ||
|
|
5e498cb0c2 | ||
|
|
a5a309ff09 | ||
|
|
82502e8222 | ||
|
|
c405f5b14f | ||
|
|
ad586f1101 | ||
|
|
1c7186171e | ||
|
|
4905047177 | ||
|
|
d2558708a0 | ||
|
|
483a138f9d | ||
|
|
c315463692 | ||
|
|
c2b52f731c | ||
|
|
cf9bd4d824 | ||
|
|
c34e59b540 | ||
|
|
6c61297449 | ||
|
|
c2db3c0bd6 | ||
|
|
6c2769fe08 | ||
|
|
748062d0e7 | ||
|
|
e292aa3e77 | ||
|
|
5b1493b940 | ||
|
|
e0130638c3 | ||
|
|
8762f9c64a | ||
|
|
788efb1266 | ||
|
|
384eb9bd1f | ||
|
|
529b5d9321 | ||
|
|
876a45d7a0 | ||
|
|
cf1eae9426 | ||
|
|
0ee1246865 | ||
|
|
3195f92276 | ||
|
|
aca2ae88cc | ||
|
|
3fa18e2984 | ||
|
|
1235ea88b9 | ||
|
|
c30f821015 | ||
|
|
83744a96a6 | ||
|
|
f386fdaedd | ||
|
|
fc163cc00d | ||
|
|
9aadedd884 | ||
|
|
64d4e4a2ac | ||
|
|
661a7cfde9 | ||
|
|
09753421d9 | ||
|
|
b3e749b10f | ||
|
|
869ba98d6e | ||
|
|
7fe68d9db3 | ||
|
|
b1795fec4d | ||
|
|
da08429ce8 | ||
|
|
a7bee4bf9a | ||
|
|
ade5cb79e3 | ||
|
|
6f556a9ebb | ||
|
|
1263df39ef | ||
|
|
c168ef93f4 | ||
|
|
01e79aee4c | ||
|
|
0c72005ebb | ||
|
|
4af5484e16 | ||
|
|
f0974a552f | ||
|
|
872e4117b0 | ||
|
|
cf49f1b398 | ||
|
|
4a16af08b5 | ||
|
|
856f95769d | ||
|
|
8c1f67a779 | ||
|
|
1c6464a4f3 | ||
|
|
a6ca1313ca | ||
|
|
77b5e934a3 | ||
|
|
3e9b18222b | ||
|
|
a80a5093d6 | ||
|
|
865cd634f8 | ||
|
|
5cb4f48905 | ||
|
|
9234bfd34c | ||
|
|
d10e27c7c3 | ||
|
|
e54d9a6c32 | ||
|
|
9a20bf350a | ||
|
|
1bb33fbd3c | ||
|
|
2e4a2952f7 | ||
|
|
65b90b595b | ||
|
|
35ffd9b2dc | ||
|
|
1757f67bbc | ||
|
|
4881e95d28 | ||
|
|
e92fa7b85e | ||
|
|
c73ac6cca9 | ||
|
|
3904b20ff9 | ||
|
|
6b372402f6 | ||
|
|
191edffe3c | ||
|
|
63a3152b91 | ||
|
|
dd1ed08f7c | ||
|
|
003d78be9f | ||
|
|
68036f6467 | ||
|
|
2578c052b3 | ||
|
|
f0ff51af25 | ||
|
|
fde425512e | ||
|
|
b4712db4ae | ||
|
|
44644d522c | ||
|
|
ddd2083de8 | ||
|
|
a417cf7136 | ||
|
|
26fbfd6671 | ||
|
|
9e5182e4b2 | ||
|
|
141c3a7b1e | ||
|
|
64c93bda6b | ||
|
|
083041134b | ||
|
|
4c877e1ea1 | ||
|
|
6c1fe40ef7 | ||
|
|
6e5ffa9920 | ||
|
|
92ee5c3c38 | ||
|
|
8091250397 | ||
|
|
8858820630 | ||
|
|
0572968530 | ||
|
|
3c12f01cce | ||
|
|
6ab70b2609 | ||
|
|
86597e0ee0 | ||
|
|
ee355f44d7 | ||
|
|
0fe49528fa | ||
|
|
061a5d4abf | ||
|
|
1eb4fd95d8 | ||
|
|
b2fcce180f | ||
|
|
59273027fb | ||
|
|
57709d7dbb | ||
|
|
1e3285b299 | ||
|
|
76673d6694 | ||
|
|
b9924587d7 | ||
|
|
44ef7dd0bc | ||
|
|
426555bac6 | ||
|
|
694f1f01d9 | ||
|
|
9ba9ffcdf9 | ||
|
|
e3157907de | ||
|
|
55efaa2c9e | ||
|
|
5fc82c72f4 | ||
|
|
5f1877f995 | ||
|
|
837bdf37e7 | ||
|
|
9368f78401 | ||
|
|
3bb936784b | ||
|
|
a0d90fd78e | ||
|
|
74dff21706 | ||
|
|
806e40cb31 | ||
|
|
88c8155b05 | ||
|
|
532e36a9b0 | ||
|
|
fc5b0c762f | ||
|
|
ab943a4bbd | ||
|
|
cfb19328f0 | ||
|
|
04dc0401b5 | ||
|
|
cc14a87357 | ||
|
|
b0114e9d68 | ||
|
|
49c0bb4f17 | ||
|
|
6c349d0304 | ||
|
|
79e2d6b4a5 | ||
|
|
ab40a21805 | ||
|
|
c5e2402770 | ||
|
|
ef73994b08 | ||
|
|
f6c6359121 | ||
|
|
39c7435c21 | ||
|
|
bb2ffca295 | ||
|
|
e1cbe557d2 | ||
|
|
95dfec8bf9 | ||
|
|
92904e5861 | ||
|
|
716d249719 | ||
|
|
a7c7fc31b5 | ||
|
|
690a4c74c7 | ||
|
|
bf5bcd6fd3 | ||
|
|
9daae873ea | ||
|
|
9ffc8665f4 | ||
|
|
b2b65365f2 | ||
|
|
54d8c5ad8f | ||
|
|
5fae9872e6 | ||
|
|
8617658169 | ||
|
|
fc9372b2ec | ||
|
|
f51781ae77 | ||
|
|
2c8c8e3365 | ||
|
|
69171f03e9 | ||
|
|
b7703c46e6 | ||
|
|
ee87ee8c34 | ||
|
|
2558513809 | ||
|
|
1e3c115b58 | ||
|
|
6a43f20767 | ||
|
|
f9295d9e22 | ||
|
|
783c226d2a | ||
|
|
fbbf93bd28 | ||
|
|
e71965c7d5 | ||
|
|
577e429edb | ||
|
|
df76340b6f | ||
|
|
be6e189948 | ||
|
|
b57bf51afb | ||
|
|
fa614dcaca | ||
|
|
99d97b2c7b | ||
|
|
5b8d0b9dda | ||
|
|
03a607fe83 | ||
|
|
e96a760fe6 | ||
|
|
30fb4fbad7 | ||
|
|
48fa6f149b | ||
|
|
0db3a9c632 | ||
|
|
859b218609 | ||
|
|
20a1a6f952 | ||
|
|
ae98027ced | ||
|
|
8d9e594f1c | ||
|
|
1cceda2c49 | ||
|
|
4fc10a1f6a | ||
|
|
29c5881cf0 | ||
|
|
195c889e17 | ||
|
|
5f4dd924ca | ||
|
|
c69d870925 | ||
|
|
b5f571f2cb | ||
|
|
4f29221c39 | ||
|
|
38bb7a195f | ||
|
|
c6879ca1d5 | ||
|
|
98fd43f8f2 | ||
|
|
2c60b13022 | ||
|
|
aa29b70cf1 | ||
|
|
5544dc3b52 | ||
|
|
8f6e2926c2 | ||
|
|
383c3d026a | ||
|
|
56d799b00a | ||
|
|
03127e5ff8 | ||
|
|
1b650dd118 | ||
|
|
b92973cec2 | ||
|
|
2957cb2a81 | ||
|
|
2aee961f4d | ||
|
|
649c3f0095 | ||
|
|
e02fe4df32 | ||
|
|
32dec56703 | ||
|
|
d31eb3d606 | ||
|
|
8c29ccc153 | ||
|
|
d7a4f4af00 | ||
|
|
69b62e2be9 | ||
|
|
581e027307 | ||
|
|
2450bf966b | ||
|
|
d3fe0422d0 | ||
|
|
795fbabe02 | ||
|
|
07f5a8090e | ||
|
|
42572977d4 | ||
|
|
316c1d0912 | ||
|
|
3a032e4bb3 | ||
|
|
2885629d8d | ||
|
|
da383864fd | ||
|
|
de567649ab | ||
|
|
c9349e4167 | ||
|
|
16bfaedd90 | ||
|
|
737c07d3d8 | ||
|
|
5d541b4c84 | ||
|
|
6dfb83d90f | ||
|
|
21f53252fd | ||
|
|
e92b43e62b | ||
|
|
17489a63ff | ||
|
|
84dc41ff01 | ||
|
|
77c95d6300 | ||
|
|
082c7858dc | ||
|
|
c951877172 | ||
|
|
71743d4dce | ||
|
|
6d020fa4d1 | ||
|
|
b3a89ee8c9 | ||
|
|
4807e9749f | ||
|
|
cbd38dbf15 | ||
|
|
e1f5dbae81 | ||
|
|
6504ac8cb7 | ||
|
|
94f521e460 | ||
|
|
722e7f38fc | ||
|
|
dfdb6bca47 | ||
|
|
0e5f0215f3 | ||
|
|
53db93fe04 | ||
|
|
1040e4fb8c | ||
|
|
94d1aa56b1 | ||
|
|
1d45a08668 | ||
|
|
89854a45b5 | ||
|
|
4ff7e95b19 | ||
|
|
a257733b3d | ||
|
|
689a326c89 | ||
|
|
ccf4362bfc | ||
|
|
8d6f0cc44c | ||
|
|
3699c76985 | ||
|
|
3607d9f2ad | ||
|
|
c78777835c | ||
|
|
66fe96454f | ||
|
|
f68c61d229 | ||
|
|
a36e26b767 | ||
|
|
d9e4c58543 | ||
|
|
e023e74057 | ||
|
|
b787b975a2 | ||
|
|
b3951e92a4 | ||
|
|
931827c526 | ||
|
|
382c46622d | ||
|
|
e965ffc210 | ||
|
|
9c83124424 | ||
|
|
0753fee385 | ||
|
|
55a55cbfca | ||
|
|
71d90d6416 | ||
|
|
35cb567b62 | ||
|
|
eff3e3f404 | ||
|
|
7c3c5917db | ||
|
|
c0305ddf38 | ||
|
|
6cacc1473d | ||
|
|
70642de2a6 | ||
|
|
5635268fd1 | ||
|
|
3f2d5bea67 | ||
|
|
4a6e85106a | ||
|
|
ad386f7c8d | ||
|
|
67c3a652b7 | ||
|
|
5d6830c7c6 | ||
|
|
00bb31738e | ||
|
|
5bafe6587c | ||
|
|
6d1e08d244 | ||
|
|
117b55cc16 | ||
|
|
d2f6f8203f | ||
|
|
79ae51fbe5 | ||
|
|
666f5626ea | ||
|
|
21b75ef392 | ||
|
|
2ce728d03f | ||
|
|
ffcd5b7100 | ||
|
|
41ec9162fd | ||
|
|
908284177f | ||
|
|
19922ca9fb | ||
|
|
e2aabb1418 | ||
|
|
eafd659870 | ||
|
|
7aafe1fbbc | ||
|
|
4188588ea0 | ||
|
|
cdb8920db6 | ||
|
|
1bf4e85fcb | ||
|
|
fe50c9f6d0 | ||
|
|
f1e6d5a331 | ||
|
|
58531815d0 | ||
|
|
0c4d0d0afb | ||
|
|
3a5194cb6c | ||
|
|
1cd547049e | ||
|
|
fb5c272899 | ||
|
|
8b68f03dd9 | ||
|
|
f2d2e07325 | ||
|
|
4e016baca8 | ||
|
|
7f247e58e9 | ||
|
|
3c16e95cee | ||
|
|
ff9f82aa6c | ||
|
|
87ee50708b | ||
|
|
405e23561f | ||
|
|
a7a93eb4f5 | ||
|
|
119dcaa7fc | ||
|
|
92a8a268a7 | ||
|
|
d9d854e456 | ||
|
|
47dc7346dc | ||
|
|
cd5fb061aa | ||
|
|
bfc712f365 | ||
|
|
67a21b25a8 | ||
|
|
c7d721bb10 | ||
|
|
5de696d7be | ||
|
|
44477d8fcd | ||
|
|
f576ee4fe6 | ||
|
|
cf949a9c86 | ||
|
|
22ed193609 | ||
|
|
b2bcc67923 | ||
|
|
7a1a5aad7e | ||
|
|
8eb2de5e76 | ||
|
|
32053ad0fd | ||
|
|
6c6461dd9a | ||
|
|
8975c5fba2 | ||
|
|
1c86cc6799 | ||
|
|
dc7042c8fa | ||
|
|
388245ece9 | ||
|
|
bc8a840695 | ||
|
|
1d287b6e20 | ||
|
|
140aba52d1 | ||
|
|
d06465132c | ||
|
|
0a1743b99b | ||
|
|
a8895290bc | ||
|
|
668334d139 | ||
|
|
aa8009019d | ||
|
|
00e2808afe | ||
|
|
68ec48fa80 | ||
|
|
2fad0df647 | ||
|
|
bbbea027cb | ||
|
|
ed0fcefc27 | ||
|
|
1672cc84ef | ||
|
|
5404537da8 | ||
|
|
d3a7ab8fc1 | ||
|
|
1f8c966022 | ||
|
|
0ff1de8baa | ||
|
|
7e0d568a5a | ||
|
|
a3f69b64df | ||
|
|
10dd117d0f | ||
|
|
1a6bfebf9b | ||
|
|
500a398dd1 | ||
|
|
d4d296a97e | ||
|
|
07507eaeb6 | ||
|
|
b1d5d07cab | ||
|
|
e0765f1c5b | ||
|
|
817b2d1ad7 | ||
|
|
4e640a0abe | ||
|
|
20b833b4ee | ||
|
|
5a058746bb | ||
|
|
7dc4dc67f6 | ||
|
|
9a46db07d1 | ||
|
|
d5fec4aec8 | ||
|
|
b5d964c074 | ||
|
|
9181b70394 | ||
|
|
8a409e8e9c | ||
|
|
f5968412a0 | ||
|
|
962d007d91 | ||
|
|
cde8ba0e9e | ||
|
|
5b60ec1836 | ||
|
|
1b70b55989 | ||
|
|
a842a3ee43 | ||
|
|
3c04bfadc9 | ||
|
|
fd5e254a84 | ||
|
|
53d0afb774 | ||
|
|
a718f8ed0d | ||
|
|
bb98042957 | ||
|
|
efa9718081 | ||
|
|
681cdfb01e | ||
|
|
251b0ea287 | ||
|
|
93ed502e57 | ||
|
|
3d1fba1c30 | ||
|
|
3e7ce2025a | ||
|
|
08f1915b4c | ||
|
|
5355c65da8 | ||
|
|
3481a879c2 | ||
|
|
dc53ff42f6 | ||
|
|
f10d8757b8 | ||
|
|
d807f2fa21 | ||
|
|
46ab42531d | ||
|
|
70b16149d0 | ||
|
|
0105338e29 | ||
|
|
7b6fe53e74 | ||
|
|
3045144cc3 | ||
|
|
0e8655862e | ||
|
|
e8839b22b8 | ||
|
|
968cd7de5f | ||
|
|
66ed8a1e17 | ||
|
|
8e52af2338 | ||
|
|
38ae744a10 | ||
|
|
4ade5094f3 | ||
|
|
a9bbbc025f | ||
|
|
aa807c3b78 | ||
|
|
48dc9738b1 | ||
|
|
574f685d93 | ||
|
|
01b63482bd | ||
|
|
09d393d002 | ||
|
|
1f2f77b833 | ||
|
|
8d70dc7a02 | ||
|
|
95c995f87a | ||
|
|
1a11aa50ac | ||
|
|
ebb327edf6 | ||
|
|
7751baf8bf | ||
|
|
9a23d4e2b3 | ||
|
|
71183d81e5 | ||
|
|
276a19648e | ||
|
|
5050d11555 | ||
|
|
8f91394c75 | ||
|
|
2c21658de7 | ||
|
|
732949fdb8 | ||
|
|
2ec52cccf0 | ||
|
|
ce1c5671b4 | ||
|
|
ab587fa1b7 | ||
|
|
aeafb5f566 | ||
|
|
60c1da327b | ||
|
|
3159561cfb | ||
|
|
21c23e1fd8 | ||
|
|
de582f2f61 | ||
|
|
a22ee0274b | ||
|
|
69aa7275d8 | ||
|
|
57416bc817 | ||
|
|
7e8a4f72ef | ||
|
|
085314fba4 | ||
|
|
cd79e73693 | ||
|
|
e109e383c0 | ||
|
|
cd992ff457 | ||
|
|
d0b01812e6 | ||
|
|
70e5b19223 | ||
|
|
fb04bdf54c | ||
|
|
f417cb991d | ||
|
|
f47dd7b629 | ||
|
|
11d0335815 | ||
|
|
b38ec2f25e | ||
|
|
4ec5739b67 | ||
|
|
dac033e962 | ||
|
|
279d40636c | ||
|
|
900b2bf340 | ||
|
|
1b69c3ef4e | ||
|
|
63ba0df07a | ||
|
|
49b7f99e81 | ||
|
|
a33733484c | ||
|
|
95f0478fa7 | ||
|
|
80aa1e65b7 | ||
|
|
d844e67239 | ||
|
|
ee193c6366 | ||
|
|
ab06c53720 | ||
|
|
a14d3b98e1 | ||
|
|
0fbba50b2f | ||
|
|
4b136fb7ee | ||
|
|
f4781b91c2 | ||
|
|
0edb5c0fd9 | ||
|
|
008e2f0c7a | ||
|
|
0a7f3ae930 | ||
|
|
7703ca15dc | ||
|
|
a83bca995b | ||
|
|
5b67060674 | ||
|
|
1183db88b7 | ||
|
|
dd7cce508f | ||
|
|
a06b9d7268 | ||
|
|
8f082dfd68 | ||
|
|
65a5eeee69 | ||
|
|
7a3300b8f8 | ||
|
|
78a0fc2091 | ||
|
|
51783ab7a1 | ||
|
|
b3532bd372 | ||
|
|
c947889e53 | ||
|
|
e169f27ade | ||
|
|
3f6157d7a4 | ||
|
|
ae7737e47b | ||
|
|
8adb35e47a | ||
|
|
9c80c92f06 | ||
|
|
9ba026934b | ||
|
|
593593d3bb | ||
|
|
eb120f5b90 | ||
|
|
4446c839d3 | ||
|
|
c6e6b62435 | ||
|
|
babf112a7a | ||
|
|
51d323a41d | ||
|
|
cfecb390f9 | ||
|
|
bdfd40d230 | ||
|
|
89e95daaee | ||
|
|
2970065a4a | ||
|
|
9b20edf862 | ||
|
|
141fa3c953 | ||
|
|
1eb903d228 | ||
|
|
26e290a4ef | ||
|
|
645d6a514c | ||
|
|
a8307bddbe | ||
|
|
5da56bc853 | ||
|
|
1b666a07dc | ||
|
|
d92f17c774 | ||
|
|
05ff54e5e5 | ||
|
|
b2873fb901 | ||
|
|
fa54cb6a48 | ||
|
|
0054a89c38 | ||
|
|
5c2aa63842 | ||
|
|
0cca9b7723 | ||
|
|
93e4e4ba0d | ||
|
|
70344ce832 | ||
|
|
c4f4abf1bd | ||
|
|
0028dbfb4f | ||
|
|
ee87b75cf5 | ||
|
|
89e9b14347 | ||
|
|
fa24a6878e | ||
|
|
ada7b1740b | ||
|
|
0e40acb90f | ||
|
|
ae9aaf327c | ||
|
|
76418dd39d | ||
|
|
e30c2b673e | ||
|
|
bcd1b90bc5 | ||
|
|
baf381461f | ||
|
|
48b5970d28 | ||
|
|
0d0d0aa111 | ||
|
|
49ca42d683 | ||
|
|
ad561129a2 | ||
|
|
0b8034a3d6 | ||
|
|
bc7d848b9b | ||
|
|
39199e1701 | ||
|
|
dfe85b9ba7 | ||
|
|
ed234f9fee | ||
|
|
e90f80d690 | ||
|
|
6a9e073f03 | ||
|
|
914b84de7d | ||
|
|
e98264d1a6 | ||
|
|
641b4e5bf0 | ||
|
|
77e5c6bf2c | ||
|
|
60368a32c3 | ||
|
|
53e9a062e8 | ||
|
|
9c20e93b3c | ||
|
|
d0976cd660 | ||
|
|
152b7bdee2 | ||
|
|
7ef99f3dc0 | ||
|
|
217d2703f5 | ||
|
|
452b3be953 | ||
|
|
8a291bea61 | ||
|
|
169e8be5ee | ||
|
|
62152825bd | ||
|
|
2a9ebde829 | ||
|
|
006b38df27 | ||
|
|
cb8e8cb1da | ||
|
|
337d27a41c | ||
|
|
55476a7828 | ||
|
|
74b009f658 | ||
|
|
e4decb53b0 | ||
|
|
4dd3a668df | ||
|
|
c21c0b5dd1 | ||
|
|
d35dd1a9c4 | ||
|
|
74c4940971 | ||
|
|
87548f9322 | ||
|
|
809c5c7ead | ||
|
|
353e1f4460 | ||
|
|
f27eb05024 | ||
|
|
6bdca66bfb | ||
|
|
164c1f5542 | ||
|
|
8838eae3fa | ||
|
|
d3447694fa | ||
|
|
27eb56aee8 | ||
|
|
ac27096087 | ||
|
|
a0ba0e8af9 | ||
|
|
c3296ccf97 | ||
|
|
1a400bfd40 | ||
|
|
58c48584ac | ||
|
|
5603360aab | ||
|
|
f7278ab3a9 | ||
|
|
3ad676235d | ||
|
|
559caeb30f | ||
|
|
b857eedab8 | ||
|
|
9ac1907e16 | ||
|
|
9efd9f27fc | ||
|
|
d8c1a7e82d | ||
|
|
79e51ad71c | ||
|
|
887cd33f5b | ||
|
|
7826f3b873 | ||
|
|
3af3d3f0d8 | ||
|
|
ce84aae950 | ||
|
|
5b4d392640 | ||
|
|
f5e2bbfdec | ||
|
|
a7dcb32931 | ||
|
|
d135fa3a66 | ||
|
|
836f6bf5df | ||
|
|
581e3e6649 | ||
|
|
48f48564e7 | ||
|
|
7891b8abe5 | ||
|
|
0c1db756c7 | ||
|
|
f34f503b19 | ||
|
|
e2ce367478 | ||
|
|
0c1c3c9b8d | ||
|
|
31be3fb3c3 | ||
|
|
b31e035eeb | ||
|
|
fb0491d8ba | ||
|
|
95f6d57df4 | ||
|
|
51b24fe766 | ||
|
|
83042e3560 | ||
|
|
2e9e5b69be | ||
|
|
78e1ec483e | ||
|
|
711d1dfe94 | ||
|
|
54a023d812 | ||
|
|
58aacc189f | ||
|
|
af0e8db8ea | ||
|
|
4755e685f4 | ||
|
|
a3ef7fcf09 | ||
|
|
1afc21d565 | ||
|
|
e15f0e7d6d | ||
|
|
4cfcb33b4f | ||
|
|
d0bb9aca3b | ||
|
|
2d63320afb | ||
|
|
f04ab8e3c1 | ||
|
|
87b47c57bb | ||
|
|
16fa03fb48 | ||
|
|
6097c01d72 | ||
|
|
dae1d4e3a8 | ||
|
|
686ed20222 | ||
|
|
ce33ec4bd3 | ||
|
|
5b143cd22a | ||
|
|
4c6d396d70 | ||
|
|
3d2912b998 | ||
|
|
46f674f0dc | ||
|
|
7727093767 | ||
|
|
96bb220b36 | ||
|
|
7fc2afc11b | ||
|
|
5f1cc56939 | ||
|
|
10444473c9 | ||
|
|
301a1afd41 | ||
|
|
b35ff53d63 | ||
|
|
2c2ffe1555 | ||
|
|
d696da2ee8 | ||
|
|
a6b307442c | ||
|
|
710a924cbe | ||
|
|
fbe20fec41 | ||
|
|
f839c07d23 | ||
|
|
901b58220e | ||
|
|
c4ace6f877 | ||
|
|
937e4890f7 | ||
|
|
f3f5bbb460 | ||
|
|
a9d85748ef | ||
|
|
d7ba360483 | ||
|
|
76675c79fb | ||
|
|
fec76baea5 | ||
|
|
2b7b8c1bd9 | ||
|
|
d3d303cfc4 | ||
|
|
3c64693275 | ||
|
|
e1ecb78375 | ||
|
|
0de73c8c85 | ||
|
|
7e0e28f515 | ||
|
|
4a2036d588 | ||
|
|
7b80524a5a | ||
|
|
1da44afddb | ||
|
|
154b1d5ec1 | ||
|
|
07c6f8993d | ||
|
|
4f40a3d990 | ||
|
|
c3ce0c1e2d | ||
|
|
3d7378a1a8 | ||
|
|
6f0062be5c | ||
|
|
4f9a32fb36 | ||
|
|
f05a012ef3 | ||
|
|
e4e1bc3c1e | ||
|
|
a902a24150 | ||
|
|
bef664b6d6 | ||
|
|
9bda29044f | ||
|
|
b9c2c2ad59 | ||
|
|
39b5aa6c8e | ||
|
|
94b0f21461 | ||
|
|
1f106ac5f5 | ||
|
|
8dbde11dd5 | ||
|
|
30430844d3 | ||
|
|
8dc2677c13 | ||
|
|
08efccf260 | ||
|
|
d774591065 | ||
|
|
2f66fec748 | ||
|
|
e0d30bfca5 | ||
|
|
acda3a254c | ||
|
|
611f02f81b | ||
|
|
6a721808a4 | ||
|
|
9cb6b800cb | ||
|
|
05cd1ef686 | ||
|
|
5eb56c3243 | ||
|
|
8a4218206d | ||
|
|
ba3df3e3df | ||
|
|
dec7e2ec68 | ||
|
|
7f0cbe7226 | ||
|
|
2e6add0dac | ||
|
|
c795eea436 | ||
|
|
ca7373792d | ||
|
|
fc4e1c0084 | ||
|
|
e92b27c0bb | ||
|
|
9d5eb11ba4 | ||
|
|
a2c5aebfd9 | ||
|
|
af07572b1c | ||
|
|
0dc125df4d | ||
|
|
fb517d2521 | ||
|
|
503f7e8fbb | ||
|
|
6feae50ecd | ||
|
|
cdac298d38 | ||
|
|
c82b64e257 | ||
|
|
5f2e6280ac | ||
|
|
03c5f48208 | ||
|
|
bab64a3ab3 | ||
|
|
6d0e73cc4b | ||
|
|
c527d006dd | ||
|
|
81d97405f9 | ||
|
|
827b7fc370 | ||
|
|
2b4b2881c9 | ||
|
|
cee2d5ca31 | ||
|
|
147804b4b8 | ||
|
|
a211f95758 | ||
|
|
6b6224dead | ||
|
|
8c810f2276 | ||
|
|
a0ca1ce87c | ||
|
|
12a4942714 | ||
|
|
9277efd1d7 | ||
|
|
0c76a249e3 | ||
|
|
217d90629a | ||
|
|
807804ca90 | ||
|
|
307c196046 | ||
|
|
0aa8e138b5 | ||
|
|
03ef598b42 | ||
|
|
86f6e0a2fd | ||
|
|
3ae48f4d16 | ||
|
|
46b79f4819 | ||
|
|
20381e5e09 | ||
|
|
259cba8aa9 | ||
|
|
9a01feafd1 | ||
|
|
ab24528582 | ||
|
|
642f44daaa | ||
|
|
3309e8c3d6 | ||
|
|
c5c5651149 | ||
|
|
a28db9369b | ||
|
|
a626ad4af1 | ||
|
|
fcbd668d7a | ||
|
|
2688ed0084 | ||
|
|
463fbd69c8 | ||
|
|
2155c2e70f | ||
|
|
47cbbba675 | ||
|
|
66470071f6 | ||
|
|
e63090573a | ||
|
|
5a7da199ed | ||
|
|
03e5834d59 | ||
|
|
23fdfd4f26 | ||
|
|
021cf8dd70 | ||
|
|
3a6bbf57ae | ||
|
|
430bbf9c46 | ||
|
|
ffb91ce997 | ||
|
|
c74ebc8aae | ||
|
|
69b6bcfaed | ||
|
|
b2b79734c1 | ||
|
|
a9de8ec046 | ||
|
|
ae70ca2079 | ||
|
|
eebb8e68f7 | ||
|
|
407191b72c | ||
|
|
2097e8e271 | ||
|
|
139d32c431 | ||
|
|
320035fd72 | ||
|
|
ccd4b8917f | ||
|
|
eccca23163 | ||
|
|
deedd49721 | ||
|
|
95915611b5 | ||
|
|
266a7a4e9b | ||
|
|
a46b9c60dd | ||
|
|
03513be144 | ||
|
|
39014a2216 | ||
|
|
e5a8fd7be4 | ||
|
|
a4ad8f551a | ||
|
|
edaeafd619 | ||
|
|
9a63307b33 | ||
|
|
18483558d4 | ||
|
|
2184fbb113 | ||
|
|
4ebd0657f4 | ||
|
|
c7759a71b4 | ||
|
|
e3b37e8220 | ||
|
|
5f08e218ec | ||
|
|
26eb2a3b29 | ||
|
|
03933edcae | ||
|
|
e037c0753d | ||
|
|
ef4588e3de | ||
|
|
79e784239b | ||
|
|
9d42684591 | ||
|
|
8c4e8f5fa6 | ||
|
|
f417b6087b | ||
|
|
a07cc4fcc7 | ||
|
|
402498d7cb | ||
|
|
78b38c2ee1 | ||
|
|
9098689136 | ||
|
|
eba737a119 | ||
|
|
02242a2568 | ||
|
|
dc6b41a473 | ||
|
|
5ffd4d87fb | ||
|
|
bed7793dab | ||
|
|
4b3d801312 | ||
|
|
ddbd9dab79 | ||
|
|
ec2acb60b3 | ||
|
|
999d08e485 | ||
|
|
2e2cf30c12 | ||
|
|
4754ea6211 | ||
|
|
ae716b5caa | ||
|
|
50d8814f3b | ||
|
|
345c7378ad | ||
|
|
649734df43 | ||
|
|
a8adc26fc4 | ||
|
|
920f24b45a | ||
|
|
3ff3e8a8cf | ||
|
|
fb8deb41f9 | ||
|
|
74506e1775 | ||
|
|
0fc62b216a | ||
|
|
926a22d9ba | ||
|
|
d37e4d7e5f | ||
|
|
09bc8f42cc | ||
|
|
1b09a301ba | ||
|
|
d8f989543e | ||
|
|
12701b2143 | ||
|
|
a861649249 | ||
|
|
4a48e03552 | ||
|
|
c40625d658 | ||
|
|
6c111c7816 | ||
|
|
95d404551c | ||
|
|
240137d3a2 | ||
|
|
fbc3807daa | ||
|
|
6ffc466dff | ||
|
|
8f1d5e4752 | ||
|
|
19c93e25ef | ||
|
|
fc54d6a289 | ||
|
|
1364fcca52 | ||
|
|
20beda740a | ||
|
|
9a085c8961 | ||
|
|
e6ccee1bf2 | ||
|
|
d975e1b6da | ||
|
|
c9a8d6aeb0 | ||
|
|
b1750265aa | ||
|
|
03496acd22 | ||
|
|
bae5a11264 | ||
|
|
912826d20e | ||
|
|
f76c5f6afe | ||
|
|
644a694ed2 | ||
|
|
fc75824921 | ||
|
|
55316334d4 | ||
|
|
ffb7fd3f12 | ||
|
|
0b6f7527e5 | ||
|
|
f94bbcaf39 | ||
|
|
c7b7952303 | ||
|
|
d9906b0da4 | ||
|
|
9bfdb69012 | ||
|
|
1c7c2a7f83 | ||
|
|
ff49caaada | ||
|
|
13eee19981 | ||
|
|
3d335963b9 | ||
|
|
9989a03993 | ||
|
|
c9148b574f | ||
|
|
74e0123475 | ||
|
|
28abf579e8 | ||
|
|
c044e1a221 | ||
|
|
ce4957f087 | ||
|
|
53837e36b4 | ||
|
|
07dbe7d260 | ||
|
|
e56263eb51 | ||
|
|
9e2b6a2963 | ||
|
|
b4a8d3c181 | ||
|
|
509c7ee900 | ||
|
|
9e1d9431c1 | ||
|
|
b92607f0e9 | ||
|
|
dcb83989c8 | ||
|
|
9331a6e7a1 | ||
|
|
caa6a0b7ae | ||
|
|
8783267e18 | ||
|
|
a1d61b4422 | ||
|
|
924de1cd42 | ||
|
|
66ba2f3218 | ||
|
|
bc53c953e8 | ||
|
|
9d64282c0b | ||
|
|
8625d65fb6 | ||
|
|
23fe7f6ad3 | ||
|
|
b7f3612738 | ||
|
|
e9bba003af | ||
|
|
fe17411406 | ||
|
|
ec01dd7581 | ||
|
|
6a734b9c61 | ||
|
|
0c6141e13b | ||
|
|
cb844e0b92 | ||
|
|
1883c98b39 | ||
|
|
69d0cba893 | ||
|
|
7f1dbbcb94 | ||
|
|
26ac0057a5 | ||
|
|
d63bab5982 | ||
|
|
d3690bb51b | ||
|
|
d327ae25c5 | ||
|
|
a2ba88fe26 | ||
|
|
354e9154c5 | ||
|
|
fffb6d6072 | ||
|
|
07034824d6 | ||
|
|
c8300a7a12 | ||
|
|
9b983a8069 | ||
|
|
00528c8c6d | ||
|
|
0184f77246 | ||
|
|
6f9633ae9a | ||
|
|
abfe6f0cbd | ||
|
|
826559c2a1 | ||
|
|
49bd11dd57 | ||
|
|
44abbcbc22 | ||
|
|
cfe8235cee | ||
|
|
845e6f4527 | ||
|
|
93a9e5da1d | ||
|
|
f942aec67b | ||
|
|
5fc6678d0b | ||
|
|
b9fa689829 | ||
|
|
fb6b8813c7 | ||
|
|
3fa42be6d4 | ||
|
|
5daa76649b | ||
|
|
fdad5973c3 | ||
|
|
4cce16b168 | ||
|
|
5fceca38d9 | ||
|
|
38d22fd587 | ||
|
|
bb1d2d6680 | ||
|
|
bcb78ee4d2 | ||
|
|
b0d3a2204a | ||
|
|
29da1b646a | ||
|
|
20063d48ed | ||
|
|
8ffed9fab7 | ||
|
|
c8b0e07203 | ||
|
|
98fbb2b926 | ||
|
|
bd42b4539e | ||
|
|
4975771f28 | ||
|
|
dd7456cdd5 | ||
|
|
2cfdd12168 | ||
|
|
e00284deb1 | ||
|
|
43476f41df | ||
|
|
e87ad5db2d | ||
|
|
825d8b57a4 | ||
|
|
ab9241376b | ||
|
|
0f13449548 | ||
|
|
0b498633d3 | ||
|
|
f080107d86 | ||
|
|
9acaf41564 | ||
|
|
d1f14ac298 | ||
|
|
f8050f3638 | ||
|
|
4f6399f35e | ||
|
|
24af84f27c | ||
|
|
c481bc78dc | ||
|
|
11e887553d | ||
|
|
ff3dad26db | ||
|
|
708b7f384d | ||
|
|
24dc7489da | ||
|
|
7a4476d302 | ||
|
|
2e1b57dcdd | ||
|
|
52c045e839 | ||
|
|
2f015d940f | ||
|
|
c5f88daa29 | ||
|
|
e279de4474 | ||
|
|
660bc633da | ||
|
|
b51b751e53 | ||
|
|
3bd0567f52 | ||
|
|
bbe4d35f26 | ||
|
|
de16f9a585 | ||
|
|
628083101c | ||
|
|
39b2148609 | ||
|
|
4f40e2457a | ||
|
|
fa6649d57e | ||
|
|
9f4aef6d40 | ||
|
|
837d70e79d | ||
|
|
4a1319d95a | ||
|
|
24c7238a8b | ||
|
|
d829850fc9 | ||
|
|
e87de4bd17 | ||
|
|
85c249db2a | ||
|
|
d45fa55f66 | ||
|
|
8084b9141d | ||
|
|
f44bd81fd9 | ||
|
|
2bd14e5648 | ||
|
|
8c627a2ade | ||
|
|
43777f4a15 | ||
|
|
98839015fa | ||
|
|
dabb958f06 | ||
|
|
6537abedb9 | ||
|
|
42cbacd07c | ||
|
|
2112f1916f | ||
|
|
a2e2cf7f75 | ||
|
|
f14523d7aa | ||
|
|
a58706ef41 | ||
|
|
5af16b7b7e | ||
|
|
71eea5abb3 | ||
|
|
e8241e580b | ||
|
|
9a2d635c62 | ||
|
|
e84bf9a181 | ||
|
|
8c88c2ec38 | ||
|
|
f54b6959ab | ||
|
|
1fa6da8eff | ||
|
|
ee2fded5de | ||
|
|
35ee3776ca | ||
|
|
7cec0e58a1 | ||
|
|
476c8de82f | ||
|
|
3a772a0dbf | ||
|
|
d056846eb0 | ||
|
|
65d9c2d5d3 | ||
|
|
0598f0d679 | ||
|
|
961f61d48d | ||
|
|
cce76c1769 | ||
|
|
dc68258e8c | ||
|
|
d5675e24e7 | ||
|
|
ad73fba36e | ||
|
|
14af4a0680 | ||
|
|
1af43a8397 | ||
|
|
9ed554bef7 | ||
|
|
5f77fd7f40 | ||
|
|
dcedd75ea1 | ||
|
|
543cfc921e | ||
|
|
c12c7845bf | ||
|
|
34d2f7fa27 | ||
|
|
74c672afab | ||
|
|
5da07db42d | ||
|
|
b6db8767e5 | ||
|
|
dfe56618f2 | ||
|
|
0e69ffa7bf | ||
|
|
b034bd05e8 | ||
|
|
abbf2e091b | ||
|
|
0b8d8eb7a0 | ||
|
|
87f05df375 | ||
|
|
80915cb7b2 | ||
|
|
7199fae4fa | ||
|
|
9e72bef480 | ||
|
|
ecc625cca4 | ||
|
|
9b47cf0e0e | ||
|
|
e43b7dec1b | ||
|
|
b149fb2e4d | ||
|
|
c07dc899de | ||
|
|
9252093fb6 | ||
|
|
13e7a53e5e | ||
|
|
7973c3d4a8 | ||
|
|
444bd67f07 | ||
|
|
aeb10aa6ab | ||
|
|
56c9d3c265 | ||
|
|
f2a6b861aa | ||
|
|
611f37bf2c | ||
|
|
ed6a4ea1c1 | ||
|
|
13db893e4a | ||
|
|
3627dff3a1 | ||
|
|
a3b0e37060 | ||
|
|
a5f8a900b6 | ||
|
|
6cc912fd5e | ||
|
|
8b781da564 | ||
|
|
634ee24a66 | ||
|
|
4f9a2fe1aa | ||
|
|
21488ad95a | ||
|
|
a28c2441a0 | ||
|
|
c4d2060cd9 | ||
|
|
3270139b01 | ||
|
|
696cfc0415 | ||
|
|
5ec68b85d2 | ||
|
|
355ff5656a | ||
|
|
85492781bc | ||
|
|
28347fd0a4 | ||
|
|
b893b5d609 | ||
|
|
0dcd0bad68 | ||
|
|
d166b05c3c | ||
|
|
01cd3c3587 | ||
|
|
1027d403c5 | ||
|
|
719cd46a21 | ||
|
|
66570e6f46 | ||
|
|
cafa1cbc90 | ||
|
|
ac39a46442 | ||
|
|
befa487482 | ||
|
|
43606e151a | ||
|
|
0eb04b9027 | ||
|
|
8c7b0a674f | ||
|
|
ed317902ca | ||
|
|
8bcfd9b86f | ||
|
|
b68805e078 | ||
|
|
9dc91f2d69 | ||
|
|
037175f7b0 | ||
|
|
010ca2f2ab | ||
|
|
37ce3047cb | ||
|
|
0114899944 | ||
|
|
ae7cd23758 | ||
|
|
1966a68638 | ||
|
|
c58baf3fec | ||
|
|
c60254b624 | ||
|
|
4d400bd7ce | ||
|
|
3c5790639e | ||
|
|
7b1845ed2e | ||
|
|
e2569f5317 | ||
|
|
605c61cd29 | ||
|
|
7e6ebb217a | ||
|
|
6a16027e16 | ||
|
|
fc1b7becce | ||
|
|
8774ca5f7f | ||
|
|
adebc15939 | ||
|
|
0df53b3856 | ||
|
|
52704039ca | ||
|
|
d0f7d4ebbc | ||
|
|
8c6a45493b | ||
|
|
e095f6bd97 | ||
|
|
0881262f4c | ||
|
|
8080989005 | ||
|
|
bfbc77c92e | ||
|
|
5cda8f599b | ||
|
|
85c8b663b1 | ||
|
|
f0f65c65dc | ||
|
|
e9e3594ee4 | ||
|
|
8c0d0c4468 | ||
|
|
f111c75e19 | ||
|
|
6dde6580f3 | ||
|
|
f21279056a | ||
|
|
3359f87177 | ||
|
|
09401c0524 | ||
|
|
3135496f30 | ||
|
|
a28fd1484b | ||
|
|
4b4d7e40f0 | ||
|
|
bd78c07c80 | ||
|
|
46c4dc8925 | ||
|
|
e02a731237 | ||
|
|
2604368cf8 | ||
|
|
9822aa6e13 | ||
|
|
d8ff9da733 | ||
|
|
09e5231735 | ||
|
|
5ae837e9eb | ||
|
|
a3511724f3 | ||
|
|
d9a5232293 | ||
|
|
b25cb244c0 | ||
|
|
7bff76e553 | ||
|
|
90eda9f996 | ||
|
|
52f605b118 | ||
|
|
c7c17a4617 | ||
|
|
973ee2fd43 | ||
|
|
cd2afd02be | ||
|
|
07f8f9e704 | ||
|
|
b7b09a8c93 | ||
|
|
8628bfa983 | ||
|
|
8ef8eeb9ec | ||
|
|
765ddb6702 | ||
|
|
6cab020241 | ||
|
|
cf489f7632 | ||
|
|
6943913d30 | ||
|
|
c5eaebc4b4 | ||
|
|
6905abf9f4 | ||
|
|
ef3b8a308f | ||
|
|
8a66c056d8 | ||
|
|
7967754024 | ||
|
|
800528f843 | ||
|
|
a6a60215d4 | ||
|
|
1487f30c43 | ||
|
|
793021573a | ||
|
|
0deaafb9ce | ||
|
|
2ab50bd0a2 | ||
|
|
ecb82bd48b | ||
|
|
5592d18e1f | ||
|
|
bcfcc7690f | ||
|
|
a2fa2515b3 | ||
|
|
c8e7eb3657 | ||
|
|
24ea975575 | ||
|
|
863bc04c21 | ||
|
|
217b424320 | ||
|
|
e022c34fe7 | ||
|
|
1af103d5ee | ||
|
|
20ddbeb709 | ||
|
|
e1ad7d3c01 | ||
|
|
8f7c65c9b5 | ||
|
|
9bf7fbfb2e | ||
|
|
2739712c5b | ||
|
|
fbfaea6b56 | ||
|
|
21207f88f3 | ||
|
|
9945b8d09f | ||
|
|
ee3fafa066 | ||
|
|
eec3b3be7a | ||
|
|
77ad209ce1 | ||
|
|
8a0152ebe6 | ||
|
|
ce8d8699c3 | ||
|
|
2efb9d18c9 | ||
|
|
9af782c485 | ||
|
|
bc232fcff2 | ||
|
|
287232be5c | ||
|
|
c1058ba06c | ||
|
|
9fe6d1092a | ||
|
|
ada55ffaba | ||
|
|
c70b1c3bbd | ||
|
|
a5708e11ba | ||
|
|
8dfc84eac2 | ||
|
|
e00c30cd4f | ||
|
|
be6bb1de6a | ||
|
|
0bd57973c5 | ||
|
|
39cfa3ab79 | ||
|
|
d36fe1c0bf | ||
|
|
27aa57da3c | ||
|
|
b7bd2be0a5 | ||
|
|
9ad80fc74d | ||
|
|
7fc7c24a20 | ||
|
|
c1ae0e76c8 | ||
|
|
f1f9bacf76 | ||
|
|
4e3eb3aeaa | ||
|
|
18c5aaf598 | ||
|
|
cff60f4ed8 | ||
|
|
9fe54825f8 | ||
|
|
c5f2dba1ef | ||
|
|
b94b3e7e2e | ||
|
|
c75f7b6c7d | ||
|
|
d25ead5208 | ||
|
|
68f09f03f8 | ||
|
|
fc1e009f09 | ||
|
|
a5ef1d16d5 | ||
|
|
e80c2b0814 | ||
|
|
7ba330176a | ||
|
|
ab9caeba9c | ||
|
|
7a5eeaa88a | ||
|
|
bb3550810d | ||
|
|
5bdf7978bf | ||
|
|
f77fb12c80 | ||
|
|
9fc109eec1 | ||
|
|
f1342e4d62 | ||
|
|
854f764b3c | ||
|
|
463c68d08c | ||
|
|
2ddd2401eb | ||
|
|
ff045b1a01 | ||
|
|
7c73e70986 | ||
|
|
8699bd4eb0 | ||
|
|
626c32763f | ||
|
|
56c958a141 | ||
|
|
d09abc1b49 | ||
|
|
8c4fc495a3 | ||
|
|
fd1d4b97a0 | ||
|
|
d8b77fc056 | ||
|
|
82579869a4 | ||
|
|
12690eeaf4 | ||
|
|
a359618cca | ||
|
|
20165a528d | ||
|
|
3a23dae178 | ||
|
|
e50d4fafb5 | ||
|
|
673b4c2881 | ||
|
|
b676c4d164 | ||
|
|
40716f9c55 | ||
|
|
df0210bfac | ||
|
|
41ac8120d0 | ||
|
|
6a66c7def7 | ||
|
|
3b0b6d75a7 | ||
|
|
292ed242c4 | ||
|
|
bb670e97ff | ||
|
|
768bdcaaaa | ||
|
|
1db4a33a1d | ||
|
|
61d11ce440 | ||
|
|
08918a7349 | ||
|
|
87542fb9df | ||
|
|
fb09386c22 | ||
|
|
d42ae52aff | ||
|
|
271d1fda92 | ||
|
|
df982e3ea9 | ||
|
|
222aaca218 | ||
|
|
8a56c599e6 | ||
|
|
003d3740af | ||
|
|
74342ba654 | ||
|
|
392015f3af | ||
|
|
c769fd6d2c | ||
|
|
0f06bfa91c | ||
|
|
dffc4a7c02 | ||
|
|
34201025c3 | ||
|
|
189ea6b23d | ||
|
|
24f2d86059 | ||
|
|
1a08ab6a2b | ||
|
|
5e5e6ff053 | ||
|
|
08204a94d8 | ||
|
|
0eb3df704e | ||
|
|
2eb77b5f97 | ||
|
|
33b6ece55b | ||
|
|
0010f71a3c | ||
|
|
38a546d6f7 | ||
|
|
4346de27b6 | ||
|
|
d797c3371b | ||
|
|
5d3f8e5b69 | ||
|
|
8ebc552cac | ||
|
|
944d86b644 | ||
|
|
8bd2a39d4e | ||
|
|
ed9cad6e39 | ||
|
|
e31330e931 | ||
|
|
49d749e89f | ||
|
|
d3fadd7081 | ||
|
|
31ff0f5aba | ||
|
|
b24a63b992 | ||
|
|
f5ec9e9602 | ||
|
|
b6accb8d02 | ||
|
|
a35486ec24 | ||
|
|
fdaa9a6188 | ||
|
|
daf08e7bd9 | ||
|
|
994e1fc26b | ||
|
|
34b7dd61cf | ||
|
|
ce3c3e0b3e | ||
|
|
92a80c3aaf | ||
|
|
df21c15972 | ||
|
|
b683d1dd21 | ||
|
|
a7d0259b30 | ||
|
|
16779064f4 | ||
|
|
732ad4bc6a | ||
|
|
f40a6f20c6 | ||
|
|
4bf22dd6a5 | ||
|
|
62ae5332de | ||
|
|
12e65279ef | ||
|
|
295b90f49c | ||
|
|
644907e58b | ||
|
|
a8a875f9d5 | ||
|
|
6cd9dfc685 | ||
|
|
80a3007f8b | ||
|
|
ed5f0bc6d5 | ||
|
|
df1109ea39 | ||
|
|
1f7c968d0d | ||
|
|
65cf8005a4 | ||
|
|
7c97aaf735 | ||
|
|
23cfdd9b34 | ||
|
|
b454e87405 | ||
|
|
3d715c45e0 | ||
|
|
fea63b0d52 | ||
|
|
ce33fa6535 | ||
|
|
57296e55f2 | ||
|
|
12f0120afd | ||
|
|
303e86a5eb | ||
|
|
52479c408f | ||
|
|
fc8eea91eb | ||
|
|
fe5a6fb568 | ||
|
|
55672cc9de | ||
|
|
6d6291e659 | ||
|
|
c2be9b210e | ||
|
|
257b40c2e4 | ||
|
|
e6b61b7a51 | ||
|
|
f167be37a1 | ||
|
|
4ac2d1a9a7 | ||
|
|
8c602cd058 | ||
|
|
b8f6664176 | ||
|
|
1024dbb61f | ||
|
|
464341c2cb | ||
|
|
f765d7c31b | ||
|
|
72b64a0c30 | ||
|
|
2b88fec2ee | ||
|
|
119b2b9514 | ||
|
|
4f406e8d33 | ||
|
|
0d80f58ea6 | ||
|
|
6c7a3ad68c | ||
|
|
52a8b20c54 | ||
|
|
5213382246 | ||
|
|
0b452ddd39 | ||
|
|
253adfeb45 | ||
|
|
355c7cbd92 | ||
|
|
99d7ff0dc7 | ||
|
|
6564e444ab | ||
|
|
51b0a0ae5b | ||
|
|
738d9b1d94 | ||
|
|
4319d648aa | ||
|
|
2595b906a5 | ||
|
|
c98e1a629b | ||
|
|
ae7f0445a3 | ||
|
|
8406657906 | ||
|
|
9135635af2 | ||
|
|
25098409df | ||
|
|
8176120bf9 | ||
|
|
5cfb7b4548 | ||
|
|
3f17d74bc6 | ||
|
|
1694a0b41d | ||
|
|
ec8a182aa3 | ||
|
|
22b70ac378 | ||
|
|
40a685aeb2 | ||
|
|
a580998f25 | ||
|
|
faa888ff36 | ||
|
|
afc34fc387 | ||
|
|
45335be4ed | ||
|
|
b834c8fd89 | ||
|
|
24162b7350 | ||
|
|
3bb7e3514f | ||
|
|
58b75ee203 | ||
|
|
d32178480d | ||
|
|
38e08be752 | ||
|
|
9573c4ed94 | ||
|
|
56e5e87238 | ||
|
|
ef47ee62a3 | ||
|
|
a9a2380287 | ||
|
|
5bd9ec963f | ||
|
|
427e8cf11c | ||
|
|
de9c224a23 | ||
|
|
5532d20657 | ||
|
|
b7ce69ee2d | ||
|
|
00b3525503 | ||
|
|
1065c9eec9 | ||
|
|
8d3dd9d8e9 | ||
|
|
0f799d5922 | ||
|
|
3a8bed6976 | ||
|
|
aadf4b7a53 | ||
|
|
ed7ba17bc7 | ||
|
|
526c36728f | ||
|
|
c7a35ebc58 | ||
|
|
5735ffd222 | ||
|
|
d11508282f | ||
|
|
30e11ad593 | ||
|
|
040954bb70 | ||
|
|
eec0051997 | ||
|
|
fab74a9061 | ||
|
|
8e9edcb673 | ||
|
|
2bd66bf4b6 | ||
|
|
ce6e89338f | ||
|
|
9e423d9769 | ||
|
|
52bb6b8218 | ||
|
|
5748bd4074 | ||
|
|
6155645436 | ||
|
|
85a839e86b | ||
|
|
0760e6e021 | ||
|
|
fbd3ebbd4e | ||
|
|
24b8e004ec | ||
|
|
542246c142 | ||
|
|
31bea94d9c | ||
|
|
5669deeb80 | ||
|
|
34cafe0d4d | ||
|
|
e319f5e270 | ||
|
|
133decf453 | ||
|
|
541615d405 | ||
|
|
1042298541 | ||
|
|
6aca61deee | ||
|
|
9266454f82 | ||
|
|
21de630f8e | ||
|
|
8a0e037c60 | ||
|
|
326e7bcc2a | ||
|
|
cc8839ab31 | ||
|
|
fcffa3df5c | ||
|
|
27d0ba0526 | ||
|
|
18d329f3ee | ||
|
|
9e064eb564 | ||
|
|
2764185132 | ||
|
|
79d7142e5f | ||
|
|
2a3838771a | ||
|
|
8d712c47f0 | ||
|
|
a3ccc83cf3 | ||
|
|
fe30b8de8d | ||
|
|
d8671dd114 | ||
|
|
603036a5e9 | ||
|
|
9eb617bcb0 | ||
|
|
82e1b069eb | ||
|
|
89da6d5851 | ||
|
|
1491f283a8 | ||
|
|
a8c3b21ee6 | ||
|
|
1b27e1fd09 | ||
|
|
d99222450c | ||
|
|
ef075787dc | ||
|
|
ec411a7dff | ||
|
|
fd4f649db3 | ||
|
|
0c93f1daa5 | ||
|
|
7da2806ab4 | ||
|
|
f88685833d | ||
|
|
c0e77698aa | ||
|
|
9fd7b2553c | ||
|
|
bb5ca8a804 | ||
|
|
cd8921e78e | ||
|
|
9260db330e | ||
|
|
3b32dcb407 | ||
|
|
549a0302b7 | ||
|
|
4d1a428acf | ||
|
|
cc83dab97b | ||
|
|
65ff765219 | ||
|
|
d5cb5c1c51 | ||
|
|
4974208a65 | ||
|
|
e88ede2d8b | ||
|
|
2b6fd41d5a | ||
|
|
e26208a5e9 | ||
|
|
12a545ddbf | ||
|
|
4ad5c7299e | ||
|
|
c04371dfae | ||
|
|
d810494211 | ||
|
|
18dd207d3c | ||
|
|
a34c8661bd | ||
|
|
f9516860e3 | ||
|
|
ded7b547e1 | ||
|
|
77607263a9 | ||
|
|
c55e05e7b2 | ||
|
|
d529cbf269 | ||
|
|
c578154b5e | ||
|
|
6c398109f4 | ||
|
|
bbfdb0ff0e | ||
|
|
48de155201 | ||
|
|
a2cfe00113 | ||
|
|
e0c8557d5c | ||
|
|
74691ce34a | ||
|
|
ef6ac3848f | ||
|
|
5c490834cf | ||
|
|
d6aa1fb48b | ||
|
|
2190db77ad | ||
|
|
7b4f76d51d | ||
|
|
16010b2223 | ||
|
|
8c2aba8eb1 | ||
|
|
c834c5e43e | ||
|
|
94f268a62d | ||
|
|
1f81ccb686 | ||
|
|
356180dbf9 | ||
|
|
ea2d5b77c0 | ||
|
|
81b0b77e2b | ||
|
|
af1209cb04 | ||
|
|
b6ec8e14ec | ||
|
|
63cf4bdc21 | ||
|
|
1d025d5b97 | ||
|
|
cf8c5430d1 | ||
|
|
1dde495f61 | ||
|
|
c3650817ef | ||
|
|
3f10523e66 | ||
|
|
8ddc167f93 | ||
|
|
54d2f38841 | ||
|
|
ad9e463923 | ||
|
|
5f2859fae2 | ||
|
|
2b03d01a15 | ||
|
|
2366f6ba50 | ||
|
|
10d6728c82 | ||
|
|
778cdaabb6 | ||
|
|
efa4f80b99 | ||
|
|
f25ab5f293 | ||
|
|
ae15c7ccd0 | ||
|
|
025f43953a | ||
|
|
bf95205af6 | ||
|
|
cb11305416 | ||
|
|
5e0c6a527a | ||
|
|
8540e71145 | ||
|
|
cb2746b741 | ||
|
|
209644d500 | ||
|
|
273dc4b83c | ||
|
|
75574f663b | ||
|
|
462870fe6b | ||
|
|
f365fe5317 | ||
|
|
19eb755157 | ||
|
|
26e4badc1b | ||
|
|
fb00f4eef9 | ||
|
|
2ef85bb9fa | ||
|
|
2dfba3f7d0 | ||
|
|
5259d55f23 | ||
|
|
af237c4fc0 | ||
|
|
13a915e1f4 | ||
|
|
df33a24951 | ||
|
|
e62fc14b6d | ||
|
|
591b8afcb0 | ||
|
|
35ee8c33b3 | ||
|
|
9ca47627d2 | ||
|
|
4c27cb831e | ||
|
|
a3b6656be7 | ||
|
|
b0076cd5da | ||
|
|
b6dbf93de2 | ||
|
|
e8217b68a5 | ||
|
|
ebfe487a3a | ||
|
|
ce34567939 | ||
|
|
3f42c4d56b | ||
|
|
55ce551868 | ||
|
|
d16e77bba3 | ||
|
|
c9efd095e7 | ||
|
|
8384344c5a | ||
|
|
cb5f707b2d | ||
|
|
e525552e10 | ||
|
|
78bd0a1b76 | ||
|
|
1316fe9509 | ||
|
|
c45fd23227 | ||
|
|
d6856e8a23 | ||
|
|
5fcad37fb9 | ||
|
|
ab58a3dfd3 | ||
|
|
a81695e973 | ||
|
|
8d1a36c669 | ||
|
|
0fce450418 | ||
|
|
3a92d69c77 | ||
|
|
fa1a372468 | ||
|
|
62b341a614 | ||
|
|
0058632d71 | ||
|
|
339408e12a | ||
|
|
8bffa6b304 | ||
|
|
7c80364bcf | ||
|
|
ac950e0e92 | ||
|
|
77700c4873 | ||
|
|
d035a29f24 | ||
|
|
4c51b90663 | ||
|
|
8d33a89e39 | ||
|
|
41260a4370 | ||
|
|
7d15a8010d | ||
|
|
606a7e6eec | ||
|
|
83e400fbe9 | ||
|
|
d3ae73e6b2 | ||
|
|
b4b2531e33 | ||
|
|
225e482814 | ||
|
|
ddd1f5de5b | ||
|
|
446601c6e0 | ||
|
|
9f3e2dbcd3 | ||
|
|
3163a142a9 | ||
|
|
dcb0d5087f | ||
|
|
e2544947f7 | ||
|
|
19743da558 | ||
|
|
08c5fd8f64 | ||
|
|
a28cdaa930 | ||
|
|
3f31962a9a | ||
|
|
651b1b1c50 | ||
|
|
ca2ca972b9 | ||
|
|
215c4e49ca | ||
|
|
d1dbce84c7 | ||
|
|
fb4a0b0ae8 | ||
|
|
854a39fe6b | ||
|
|
a01ee4a48d | ||
|
|
8ed867314d | ||
|
|
abf4eeb39a | ||
|
|
40a85634ef | ||
|
|
a5c41a1cd8 | ||
|
|
c6668e1d6a | ||
|
|
5384abc780 | ||
|
|
444d5fb7c3 | ||
|
|
3b76a020c7 | ||
|
|
804187afb9 | ||
|
|
ef2ed0f4d1 | ||
|
|
68084fc9b5 | ||
|
|
a684a46404 | ||
|
|
1556bf02ba | ||
|
|
b29c36d01d | ||
|
|
68cb94547e | ||
|
|
a0e1894262 | ||
|
|
ee584375c5 | ||
|
|
ac288b794f | ||
|
|
a1f296b2ae | ||
|
|
9b1e399730 | ||
|
|
dd6b435417 | ||
|
|
3d5c08118c | ||
|
|
29a3e79804 | ||
|
|
d78d4aed9d | ||
|
|
9f34531956 | ||
|
|
d1f3ead8b9 | ||
|
|
a33f17932f | ||
|
|
eb05b83009 | ||
|
|
367c022e6c | ||
|
|
b9af4325c9 | ||
|
|
1ae2a624f7 | ||
|
|
d4c9a3c846 | ||
|
|
0b481a44a1 | ||
|
|
513d0e982e | ||
|
|
2629471a56 | ||
|
|
c1ce4f1b73 | ||
|
|
faa21abe54 | ||
|
|
72f4d00cb3 | ||
|
|
c9f5b0a2c1 | ||
|
|
8453422c9c | ||
|
|
ed060a400d | ||
|
|
6c3e5d976c | ||
|
|
443050ae28 | ||
|
|
d81b833951 | ||
|
|
510602e117 | ||
|
|
4008883627 | ||
|
|
4081a55207 | ||
|
|
c9ca395e6d | ||
|
|
c3ac215493 | ||
|
|
10cabd032b | ||
|
|
7b9a04ede1 | ||
|
|
1845d5060a | ||
|
|
9375fb4d2d | ||
|
|
f9b8ac6f30 | ||
|
|
ad577eaa2a | ||
|
|
42ba93bdc1 | ||
|
|
fd5f5d49b7 | ||
|
|
80f4e63850 | ||
|
|
7efa8ffbe0 | ||
|
|
85016d6582 | ||
|
|
06bf2f3427 | ||
|
|
c38832ef06 | ||
|
|
c1fa5c42c4 | ||
|
|
2faa78bc32 | ||
|
|
b5633cd579 | ||
|
|
4275d144ca | ||
|
|
7f794f35a6 | ||
|
|
8d778f902f | ||
|
|
f83f22a6fb | ||
|
|
03b6ebd861 | ||
|
|
0d4607a922 | ||
|
|
067100d375 | ||
|
|
306baee94d | ||
|
|
cb1989b2ea | ||
|
|
7ce99cb1fb | ||
|
|
3a36663d94 | ||
|
|
9118cd7c5b | ||
|
|
8b0cf599f4 | ||
|
|
7d6bb6b9c8 | ||
|
|
2a6fedc6b3 | ||
|
|
e3a7e9fe33 | ||
|
|
548fdd823b | ||
|
|
2b486ffa36 | ||
|
|
c3f9d9ddd6 | ||
|
|
740f3b4ef7 | ||
|
|
932a496f47 | ||
|
|
60beeddb66 | ||
|
|
c61c34f10e | ||
|
|
96a04da1ff | ||
|
|
bd8472b34e | ||
|
|
09228e4637 | ||
|
|
40a79c51ce | ||
|
|
2edfe0f42c | ||
|
|
a2eb8dfe83 | ||
|
|
4c60545057 | ||
|
|
d06b3285bd | ||
|
|
4dcfe8e0f6 | ||
|
|
42e679d5ba | ||
|
|
4db1c7dfca | ||
|
|
64fb84dd54 | ||
|
|
a17a9b71a2 | ||
|
|
0a10e78bfd | ||
|
|
50777bd681 | ||
|
|
2f5558c311 | ||
|
|
21c3fe5d8e | ||
|
|
acb453bd4b | ||
|
|
9c6b9a5359 | ||
|
|
0d07a9e50c | ||
|
|
f9e1940c7b | ||
|
|
72c0625823 | ||
|
|
6926f6fd0b | ||
|
|
e30c476e5c | ||
|
|
509122bf4b | ||
|
|
84fab951ba | ||
|
|
f34be2a884 | ||
|
|
bed1650350 | ||
|
|
1f8a477939 | ||
|
|
6699d9ad80 | ||
|
|
253eb72dbf | ||
|
|
fe772f85bf | ||
|
|
0e55caf721 | ||
|
|
434ef2b333 | ||
|
|
e0ab208c52 | ||
|
|
5b1f3d266e | ||
|
|
4c83f5fe60 | ||
|
|
2f658a9a14 | ||
|
|
c3f487eced | ||
|
|
a8a12dd1f8 | ||
|
|
7f7e3c47ec | ||
|
|
eafeb5c5d2 | ||
|
|
caca8bf802 | ||
|
|
338091578b | ||
|
|
188bfa4525 | ||
|
|
037a9848bc | ||
|
|
b6543169de | ||
|
|
14b3b058fe | ||
|
|
fa4763309d | ||
|
|
adcc59642c | ||
|
|
d18fd4948c | ||
|
|
4e4258f9dc | ||
|
|
ab6cf78822 | ||
|
|
d105c18bf7 | ||
|
|
6bbf4e4778 | ||
|
|
3101f5e6ae |
30
.github/ISSUE_TEMPLATE/bug_report.md
vendored
30
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,32 +1,32 @@
|
||||
---
|
||||
name: Problem Report
|
||||
name: Problem Report/Change Request
|
||||
about: Create a Report to help us improve
|
||||
---
|
||||
|
||||
<!-- Thanks for reporting a problem for this project. READ THIS FIRST:
|
||||
<!-- Thanks for reporting an issue for this project. READ THIS FIRST:
|
||||
|
||||
Please DO NOT OPEN AN ISSUE if your EMS-ESP version is not the latest from the dev branch, please update your device before submitting your issue. Your problem might already be solved. The latest precompiled binaries of EMS-ESP can be downloaded from https://github.com/emsesp/EMS-ESP32/releases/tag/latest
|
||||
Please DO NOT OPEN AN ISSUE if your EMS-ESP version is not the latest from the dev branch, please update your device before submitting your issue. Your issue might already be solved. The latest precompiled binaries of EMS-ESP can be downloaded from https://github.com/emsesp/EMS-ESP32/releases/tag/latest
|
||||
|
||||
Please take a few minutes to complete the requested information below.
|
||||
|
||||
-->
|
||||
|
||||
### PROBLEM DESCRIPTION
|
||||
### DESCRIPTION
|
||||
|
||||
_A clear and concise description of what the problem is._
|
||||
_A clear and concise description of what the problem is or the change requested._
|
||||
|
||||
### REQUESTED INFORMATION
|
||||
|
||||
_Make sure your have performed every step and checked the applicable boxes before submitting your issue. Thank you!_
|
||||
|
||||
- [ ] Searched the problem in [issues](https://github.com/emsesp/EMS-ESP32/issues)
|
||||
- [ ] Searched the problem in [discussions](https://github.com/emsesp/EMS-ESP32/discussions)
|
||||
- [ ] Searched the problem in the [docs](https://emsesp.github.io/docs/Troubleshooting/)
|
||||
- [ ] Searched the problem in the [chat](https://discord.gg/3J3GgnzpyT)
|
||||
- [ ] Provide the output of http://ems-esp.local/api/system :
|
||||
- [ ] 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 the [docs](https://emsesp.org/Troubleshooting/)
|
||||
- [ ] Searched the issue in the [chat](https://discord.gg/3J3GgnzpyT)
|
||||
- [ ] Provide the System information in the area below, taken from `http://<IP>/api/system`
|
||||
|
||||
```lua
|
||||
System information output here:
|
||||
```json
|
||||
Paste System information here....
|
||||
|
||||
|
||||
```
|
||||
@@ -41,10 +41,10 @@ _A clear and concise description of what you expected to happen._
|
||||
|
||||
### SCREENSHOTS
|
||||
|
||||
_If applicable, add screenshots to help explain your problem._
|
||||
_If applicable, add screenshots to help explain your issue._
|
||||
|
||||
### ADDITIONAL CONTEXT
|
||||
|
||||
_Add any other context about the problem here._
|
||||
_Add any other context about the issue here._
|
||||
|
||||
**(Please, remember to close the issue when the problem has been addressed)**
|
||||
**(Please remember to close the issue when it has been addressed)**
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,7 +1,7 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: EMS-ESP Docs
|
||||
url: https://emsesp.github.io/docs/
|
||||
url: https://emsesp.org
|
||||
about: All the information related to EMS-ESP.
|
||||
- name: EMS-ESP Discussions and Support
|
||||
url: https://github.com/emsesp/EMS-ESP32/discussions
|
||||
|
||||
@@ -11,7 +11,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Github Releases To Discord
|
||||
|
||||
- name: GitHub Releases To Discord
|
||||
uses: SethCohen/github-releases-to-discord@v1.13.1
|
||||
with:
|
||||
webhook_url: ${{ secrets.WEBHOOK_URL }}
|
||||
|
||||
33
.github/workflows/pre_release.yml
vendored
33
.github/workflows/pre_release.yml
vendored
@@ -10,17 +10,24 @@ jobs:
|
||||
pre-release:
|
||||
name: 'Automatic pre-release build'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v4
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Install python 3.11
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
|
||||
- name: Get EMS-ESP source code and version
|
||||
- name: Install Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
|
||||
- name: Get EMS-ESP version
|
||||
id: build_info
|
||||
run: |
|
||||
version=`grep -E '^#define EMSESP_APP_VERSION' ./src/version.h | awk -F'"' '{print $2}'`
|
||||
@@ -40,17 +47,13 @@ jobs:
|
||||
yarn build
|
||||
yarn webUI
|
||||
|
||||
- name: Build firmware
|
||||
- name: Build all PIO target environments from default_envs
|
||||
run: |
|
||||
platformio run -e ci
|
||||
platformio run
|
||||
|
||||
- name: Build S3 firmware
|
||||
run: |
|
||||
platformio run -e ci_s3
|
||||
|
||||
- name: Create a GH Release
|
||||
- name: Create GitHub Release
|
||||
id: 'automatic_releases'
|
||||
uses: 'marvinpinto/action-automatic-releases@latest'
|
||||
uses: emsesp/action-automatic-releases@v1.0.0
|
||||
with:
|
||||
repo_token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
title: Development Build v${{steps.build_info.outputs.VERSION}}
|
||||
|
||||
24
.github/workflows/sonar_check.yml
vendored
24
.github/workflows/sonar_check.yml
vendored
@@ -1,30 +1,34 @@
|
||||
# see https://github.com/marketplace/actions/sonarcloud-scan-for-c-and-c#usage
|
||||
name: Sonar Check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
# pull_request:
|
||||
# types: [opened, synchronize, reopened]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and analyze
|
||||
if: github.repository == 'emsesp/EMS-ESP32'
|
||||
runs-on: ubuntu-latest
|
||||
# if: github.repository_owner == 'emsesp'
|
||||
# if: github.repository == 'emsesp/EMS-ESP32'
|
||||
env:
|
||||
BUILD_WRAPPER_OUT_DIR: bw-output
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install sonar-scanner and build-wrapper
|
||||
uses: SonarSource/sonarcloud-github-c-cpp@v2
|
||||
|
||||
- name: Run build-wrapper
|
||||
run: |
|
||||
build-wrapper-linux-x86-64 --out-dir ${{ env.BUILD_WRAPPER_OUT_DIR }} make all
|
||||
run: build-wrapper-linux-x86-64 --out-dir ${{ env.BUILD_WRAPPER_OUT_DIR }} make all
|
||||
|
||||
- name: Run sonar-scanner
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
run: |
|
||||
sonar-scanner --define sonar.cfamily.build-wrapper-output="${{ env.BUILD_WRAPPER_OUT_DIR }}"
|
||||
run: sonar-scanner --define sonar.cfamily.compile-commands="${{ env.BUILD_WRAPPER_OUT_DIR }}/compile_commands.json"
|
||||
|
||||
32
.github/workflows/tagged_release.yml
vendored
32
.github/workflows/tagged_release.yml
vendored
@@ -1,6 +1,7 @@
|
||||
name: 'tagged-release'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
@@ -9,22 +10,27 @@ jobs:
|
||||
tagged-release:
|
||||
name: 'Tagged Release'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v4
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Install python 3.11
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- uses: actions/setup-node@v3
|
||||
|
||||
- name: Install Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
node-version: '20.x'
|
||||
|
||||
- name: Install PlatformIO
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -U platformio
|
||||
platformio upgrade
|
||||
pio pkg update
|
||||
|
||||
- name: Build WebUI
|
||||
run: |
|
||||
@@ -35,16 +41,12 @@ jobs:
|
||||
yarn build
|
||||
yarn webUI
|
||||
|
||||
- name: Build firmware
|
||||
- name: Build all PIO target environments from default_envs
|
||||
run: |
|
||||
platformio run -e ci
|
||||
platformio run
|
||||
|
||||
- name: Build S3 firmware
|
||||
run: |
|
||||
platformio run -e ci_s3
|
||||
|
||||
- name: Release
|
||||
uses: 'marvinpinto/action-automatic-releases@latest'
|
||||
- name: Create GitHub Release
|
||||
uses: emsesp/action-automatic-releases@v1.0.0
|
||||
with:
|
||||
repo_token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
prerelease: false
|
||||
|
||||
27
.github/workflows/test_release.yml
vendored
27
.github/workflows/test_release.yml
vendored
@@ -10,27 +10,26 @@ jobs:
|
||||
pre-release:
|
||||
name: 'Automatic test-release build'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v4
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- uses: actions/setup-node@v3
|
||||
- name: Use Node.js 20.x
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
|
||||
node-version: '20.x'
|
||||
- name: Get EMS-ESP source code and version
|
||||
id: build_info
|
||||
run: |
|
||||
version=`grep -E '^#define EMSESP_APP_VERSION' ./src/version.h | awk -F'"' '{print $2}'`
|
||||
echo "VERSION=$version" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Install PlatformIO
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -U platformio
|
||||
|
||||
- name: Build WebUI
|
||||
run: |
|
||||
cd interface
|
||||
@@ -39,18 +38,12 @@ jobs:
|
||||
sed -i "s/= 'pl'/= 'en'/" ./src/i18n/i18n-util.ts
|
||||
yarn build
|
||||
yarn webUI
|
||||
|
||||
- name: Build firmware
|
||||
- name: Build all target environments from default_envs
|
||||
run: |
|
||||
platformio run -e ci
|
||||
|
||||
- name: Build S3 firmware
|
||||
run: |
|
||||
platformio run -e ci_s3
|
||||
|
||||
- name: Create a GH Release
|
||||
platformio run
|
||||
- name: Create GitHub Release
|
||||
id: 'automatic_releases'
|
||||
uses: 'marvinpinto/action-automatic-releases@latest'
|
||||
uses: emsesp/action-automatic-releases@v1.0.0
|
||||
with:
|
||||
repo_token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
title: Test Build v${{steps.build_info.outputs.VERSION}}
|
||||
|
||||
37
.gitignore
vendored
37
.gitignore
vendored
@@ -2,7 +2,7 @@
|
||||
.vscode/c_cpp_properties.json
|
||||
.vscode/extensions.json
|
||||
.vscode/launch.json
|
||||
# .vscode/settings.json
|
||||
.vscode/settings.json
|
||||
|
||||
# c++ compiling
|
||||
.clang_complete
|
||||
@@ -12,11 +12,11 @@ cppcheck.out.xml
|
||||
# platformio
|
||||
.pio
|
||||
pio_local.ini
|
||||
*_old
|
||||
|
||||
# OS specific
|
||||
.DS_Store
|
||||
*Thumbs.db
|
||||
emsesp
|
||||
|
||||
# web specfic
|
||||
build/
|
||||
@@ -30,20 +30,16 @@ stats.html
|
||||
*.sln
|
||||
*.sw?
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
*/.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
yarn.lock
|
||||
interface/analyse.html
|
||||
|
||||
# scripts
|
||||
test.sh
|
||||
scripts/run.sh
|
||||
scripts/__pycache__
|
||||
/scripts/stackdmp.txt
|
||||
analyse.html
|
||||
interface/vite.config.ts.timestamp*
|
||||
*.local
|
||||
|
||||
# i18n generated files
|
||||
interface/src/i18n/i18n-react.tsx
|
||||
@@ -52,12 +48,27 @@ interface/src/i18n/i18n-util.ts
|
||||
interface/src/i18n/i18n-util.sync.ts
|
||||
interface/src/i18n/i18n-util.async.ts
|
||||
|
||||
# scripts
|
||||
test.sh
|
||||
scripts/run.sh
|
||||
scripts/__pycache__
|
||||
scripts/stackdmp.txt
|
||||
|
||||
# sonar
|
||||
.scannerwork/
|
||||
sonar/
|
||||
bw-output/
|
||||
|
||||
# entity dump results
|
||||
# dump_entities.csv
|
||||
# dump_entities.xls*
|
||||
# standalone executable for testing
|
||||
emsesp
|
||||
interface/tsconfig.tsbuildinfo
|
||||
|
||||
# python virtual environment
|
||||
venv/
|
||||
|
||||
# cspell
|
||||
words-found-verbose.txt
|
||||
|
||||
# sonarlint
|
||||
compile_commands.json
|
||||
package.json
|
||||
|
||||
6
.prettierignore
Normal file
6
.prettierignore
Normal file
@@ -0,0 +1,6 @@
|
||||
*/node_modules/
|
||||
build/
|
||||
dist/*
|
||||
interface/src/i18n/*
|
||||
|
||||
.typesafe-i18n.json
|
||||
11
.prettierrc
11
.prettierrc
@@ -1,8 +1,13 @@
|
||||
{
|
||||
"plugins": ["@trivago/prettier-plugin-sort-imports"],
|
||||
"trailingComma": "none",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"printWidth": 120,
|
||||
"bracketSpacing": true
|
||||
}
|
||||
"printWidth": 85,
|
||||
"bracketSpacing": true,
|
||||
"importOrder": ["^react", "^@mui/(.*)$", "^api*/(.*)$", "<THIRD_PARTY_MODULES>", "^[./]"],
|
||||
"importOrderSeparation": true,
|
||||
"importOrderSortSpecifiers": true,
|
||||
"importOrderGroupNamespaceSpecifiers": true
|
||||
}
|
||||
4
.sonarlint/connectedMode.json
Normal file
4
.sonarlint/connectedMode.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"sonarCloudOrganization": "emsesp",
|
||||
"projectKey": "emsesp_EMS-ESP32"
|
||||
}
|
||||
12
.vscode/extensions.json
vendored
12
.vscode/extensions.json
vendored
@@ -1,12 +0,0 @@
|
||||
{
|
||||
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||
// for the documentation about the extensions.json format
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"platformio.platformio-ide"
|
||||
],
|
||||
"unwantedRecommendations": [
|
||||
"ms-vscode.cpptools-extension-pack"
|
||||
]
|
||||
}
|
||||
188
.vscode/settings.json
vendored
188
.vscode/settings.json
vendored
@@ -1,89 +1,101 @@
|
||||
{
|
||||
"search.exclude": {
|
||||
"**/.yarn": true,
|
||||
"**/.pnp.*": true
|
||||
},
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": true
|
||||
// "source.organizeImports": true
|
||||
},
|
||||
"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": ["!cpp"]
|
||||
}
|
||||
"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"
|
||||
}
|
||||
}
|
||||
18
.vscode/tasks.json
vendored
18
.vscode/tasks.json
vendored
@@ -1,18 +0,0 @@
|
||||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "shell",
|
||||
"label": "build standalone emsesp",
|
||||
"command": "make",
|
||||
"args": [],
|
||||
"problemMatcher": ["$gcc"],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
139
CHANGELOG.md
139
CHANGELOG.md
@@ -5,6 +5,143 @@ 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/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [3.7.0] October 27 2024
|
||||
|
||||
## **IMPORTANT! BREAKING CHANGES with 3.6.5**
|
||||
|
||||
- "ww" and "wwc" has been renamed to "dhw". It is nested JSON object in both the MQTT and API outputs. The old prefix has also been removed from MQTT topics ([#1634](https://github.com/emsesp/EMS-ESP32/issues/1634)). This will impact historical data in home automation systems like Home Assistant and IOBroker. To preserve the current value of dhw energy (was previously nrgww) refer to this issue [#1938](https://github.com/emsesp/EMS-ESP32/issues/1938).
|
||||
- dhw entities from the MM100/SM100 have been moved under a new Device called 'water'.
|
||||
- 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.
|
||||
|
||||
For more details go to [www.emsesp.org](https://www.emsesp.org/).
|
||||
|
||||
## Added
|
||||
|
||||
- some more entities for dhw with SM100 module
|
||||
- thermostat second dhw circuit [#1634](https://github.com/emsesp/EMS-ESP32/issues/1634)
|
||||
- remote thermostat emulation for RC100H, RC200 and FB10 [#1287](https://github.com/emsesp/EMS-ESP32/discussions/1287), [#1602](https://github.com/emsesp/EMS-ESP32/discussions/1602), [#1551](https://github.com/emsesp/EMS-ESP32/discussions/1551)
|
||||
- heatpump dhw stop temperatures [#1624](https://github.com/emsesp/EMS-ESP32/issues/1624)
|
||||
- reset history [#1695](https://github.com/emsesp/EMS-ESP32/issues/1695)
|
||||
- heatpump entities `fan` and `shutdown` [#1690](https://github.com/emsesp/EMS-ESP32/discussions/1690)
|
||||
- mqtt HA-mode 3 for v3.6 compatible HA entities, set on update v3.6->v3.7
|
||||
- HP input states [#1723](https://github.com/emsesp/EMS-ESP32/discussions/1723)
|
||||
- holiday settings for rego 3000 [#1735](https://github.com/emsesp/EMS-ESP32/issues/1735)
|
||||
- Added scripts for OTA (scripts/upload.py and upload_cli.py) [#1738](https://github.com/emsesp/EMS-ESP32/issues/1738)
|
||||
- timeout for remote thermostat emulation [#1680](https://github.com/emsesp/EMS-ESP32/discussions/1680), [#1774](https://github.com/emsesp/EMS-ESP32/issues/1774)
|
||||
- CR120 thermostat as own model() [#1779](https://github.com/emsesp/EMS-ESP32/discussions/1779)
|
||||
- modules - external linkable module library [#1778](https://github.com/emsesp/EMS-ESP32/issues/1778)
|
||||
- scheduler onChange and Conditions [#1806](https://github.com/emsesp/EMS-ESP32/issues/1806)
|
||||
- make remote control timeout editable [#1774](https://github.com/emsesp/EMS-ESP32/issues/1774)
|
||||
- added extra pump characteristics (mode and pressure for EMS+) by @SLTKA [#1802](https://github.com/emsesp/EMS-ESP32/pull/1802)
|
||||
- allow device name to be customized [#1174](https://github.com/emsesp/EMS-ESP32/issues/1174)
|
||||
- Modbus support by @mheyse [#1744](https://github.com/emsesp/EMS-ESP32/issues/1744)
|
||||
- System Message command [#1854](https://github.com/emsesp/EMS-ESP32/issues/1854)
|
||||
- scheduler can use web get/post for values and commands [#1806](https://github.com/emsesp/EMS-ESP32/issues/1806)
|
||||
- RT800 remote emulation [#1867](https://github.com/emsesp/EMS-ESP32/issues/1867)
|
||||
- RC310 cooling parameters [#1857](https://github.com/emsesp/EMS-ESP32/issues/1857)
|
||||
- command `api/device/entities` [#1897](https://github.com/emsesp/EMS-ESP32/issues/1897)
|
||||
- switchprogmode [#1903](https://github.com/emsesp/EMS-ESP32/discussions/1903)
|
||||
- autodetect and download firmware upgrades via the WebUI
|
||||
- command 'show log' that lists out the current weblog buffer, showing last messages.
|
||||
- default web log buffer to 25 lines for ESP32s with no PSRAM
|
||||
- try and determine correct board profile if none is set during boot
|
||||
- auto Scroll in WebLog UI - reduced delay so incoming logs are faster
|
||||
- uploading custom support info, shown to Guest users in Help page [#2054](https://github.com/emsesp/EMS-ESP32/issues/2054)
|
||||
- feature: Dashboard showing all data (favorites, sensors, custom) [#1958](https://github.com/emsesp/EMS-ESP32/issues/1958)
|
||||
- entity for low-temperature boilers pump start temp (pumpOnTemp) #2088 [#2088](https://github.com/emsesp/EMS-ESP32/issues/2088)
|
||||
- internal ESP32 temperature sensor on the S3 [#2077](https://github.com/emsesp/EMS-ESP32/issues/2077)
|
||||
- MQTT status topic (used in connect and last will) set to Retain [#2086](https://github.com/emsesp/EMS-ESP32/discussions/2086)
|
||||
- Czech language [2096](https://github.com/emsesp/EMS-ESP32/issues/2096)
|
||||
- Developer Mode and send EMS Read Commands from WebUI [#2116](https://github.com/emsesp/EMS-ESP32/issues/2116)
|
||||
- Scheduler functions [#2115](https://github.com/emsesp/EMS-ESP32/issues/2115)
|
||||
- Set device custom name from telegram 0x01 [#2073](https://github.com/emsesp/EMS-ESP32/issues/2073)
|
||||
|
||||
## Fixed
|
||||
|
||||
- remote thermostat emulation for RC200 on Rego2000/3000 thermostats [#1691](https://github.com/emsesp/EMS-ESP32/discussions/1691)
|
||||
- log shows data for F7/F9 requests
|
||||
- Detection of LittleFS for factory setting wasn't working
|
||||
- Check for bad GPIOs with Ethernet before the ethernet is initialized
|
||||
- Show values with factor 50 on webUI [#2064](https://github.com/emsesp/EMS-ESP32/issues/2064)
|
||||
- Rendering of values between -1 and 0
|
||||
- Value for 32bit times not-set [#2109](https://github.com/emsesp/EMS-ESP32/issues/2109)
|
||||
|
||||
## Changed
|
||||
|
||||
- use flag for BC400 compatible thermostats, manage different mode settings
|
||||
- use factory partition for 16M flash
|
||||
- store digital out states to nvs
|
||||
- Refresh UI - moving settings to one location [#1665](https://github.com/emsesp/EMS-ESP32/issues/1665)
|
||||
- rename DeviceValueTypes, add UINT32 for custom entities
|
||||
- dynamic register dhw circuits for thermostat
|
||||
- removed OTA feature [#1738](https://github.com/emsesp/EMS-ESP32/issues/1738)
|
||||
- added shower min duration [#1801](https://github.com/emsesp/EMS-ESP32/issues/1801)
|
||||
- Include TXT file along with the generated CSV for Device Data export/download
|
||||
- thermostat/remotetemp as command [#1835](https://github.com/emsesp/EMS-ESP32/discussions/1835)
|
||||
- temperaturesensor id notation with underscore [#1794](https://github.com/emsesp/EMS-ESP32/discussions/1794)
|
||||
- Change key-names in JSON to be compliant and consistent [#1860](https://github.com/emsesp/EMS-ESP32/issues/1860)
|
||||
- Updates to webUI [#1920](https://github.com/emsesp/EMS-ESP32/issues/1920)
|
||||
- Correct firmware naming #1933 [#1933](https://github.com/emsesp/EMS-ESP32/issues/1933)
|
||||
- Don't start Serial console if not connected to a Serial port. Will initiate manually after a CTRL-C/CTRL-S
|
||||
- WebLog UI matches color schema of the terminal console correctly
|
||||
- Updated Web libraries, ArduinoJson
|
||||
- Help page doesn't show detailed tech info if the user is not 'admin' role [#2054](https://github.com/emsesp/EMS-ESP32/issues/2054)
|
||||
- removed system command `allvalues` and moved to an action called `export`
|
||||
- Show ems-esp internal devices in device list of system/info
|
||||
- Scheduler and mqtt run async on systems with psram
|
||||
- Show IPv6 address type (local/global/ula) in log
|
||||
|
||||
## [3.6.5] March 23 2024
|
||||
|
||||
## **IMPORTANT! BREAKING CHANGES**
|
||||
|
||||
- The Wifi Tx Power setting in Network Settings will be reset to Auto
|
||||
|
||||
## Added
|
||||
|
||||
- thermostat boost mode and boost time [#1446](https://github.com/emsesp/EMS-ESP32/issues/1446)
|
||||
- heatpump energy meters [#1463](https://github.com/emsesp/EMS-ESP32/issues/1463)
|
||||
- heatpump max power [#1475](https://github.com/emsesp/EMS-ESP32/issues/1475)
|
||||
- checkbox for MQTT-TLS enable [#1474](https://github.com/emsesp/EMS-ESP32/issues/1474)
|
||||
- added SK (Slovak) language. Thanks @misa1515
|
||||
- CPU info [#1497](https://github.com/emsesp/EMS-ESP32/pull/1497)
|
||||
- Show network hostname in Web UI under Network Status
|
||||
- Improved HA Discovery so each section (EMS device, Scheduler, Analog, Temperature, Custom, Shower) have their own section
|
||||
- boiler Bosch C1200W, id 12, [#1536](https://github.com/emsesp/EMS-ESP32/issues/1536)
|
||||
- mixer MM100 telegram 0x2CC [#1554](https://github.com/emsesp/EMS-ESP32/issues/1554)
|
||||
- boiler hpSetDiffPressure [#1563](https://github.com/emsesp/EMS-ESP32/issues/1563)
|
||||
- custom variables [#1423](https://github.com/emsesp/EMS-ESP32/issues/1423)
|
||||
- weather compensation [#1642](https://github.com/emsesp/EMS-ESP32/issues/1642)
|
||||
- env and partitions for DevKitC-1-N32R8 [#1635](https://github.com/emsesp/EMS-ESP32/discussions/1635)
|
||||
- command `restart partitionname` and button long press to start with other partition [#1657](https://github.com/emsesp/EMS-ESP32/issues/1657)
|
||||
- command `set service <mqtt|ota|ntp|ap> <enable|disable>` [#1663](https://github.com/emsesp/EMS-ESP32/issues/1663)
|
||||
|
||||
## Fixed
|
||||
|
||||
- exhaust temperature for some boilers
|
||||
- add back boil2hyst [#1477](https://github.com/emsesp/EMS-ESP32/issues/1477)
|
||||
- subscribed MQTT topics not detecting changes by EMS-ESP [#1494](https://github.com/emsesp/EMS-ESP32/issues/1494)
|
||||
- changed HA name and grouping to be consistent [#1528](https://github.com/emsesp/EMS-ESP32/issues/1528)
|
||||
- MQTT autodiscovery in Domoticz not working [#1360](https://github.com/emsesp/EMS-ESP32/issues/1528)
|
||||
- dhw comfort for new ems+, [#1495](https://github.com/emsesp/EMS-ESP32/issues/1495)
|
||||
- added writeable icon to Web's Custom Entity page for each entity shown in the table
|
||||
- Wifi Tx Power not adjusted [#1614](https://github.com/emsesp/EMS-ESP32/issues/1614)
|
||||
- MQTT discovery of custom entity doesn't consider type of data [#1587](https://github.com/emsesp/EMS-ESP32/issues/1587)
|
||||
- WiFi TxPower wasn't correctly used. Added an 'Auto' setting, which is the default.
|
||||
- dns w/wo IPv6 [#1644](https://github.com/emsesp/EMS-ESP32/issues/1644)
|
||||
|
||||
## Changed
|
||||
|
||||
- HA don't set entity_category to Diagnostic/Configuration for EMS entities [#1459](https://github.com/emsesp/EMS-ESP32/discussions/1459)
|
||||
- upgraded ArduinoJson to 7.0.0 #1538 and then 7.0.2
|
||||
- small changes to the API for analog and temperature sensors
|
||||
- Length of mqtt Broker adress [#1619](https://github.com/emsesp/EMS-ESP32/issues/1619)
|
||||
- C++ optimizations - see <https://github.com/emsesp/EMS-ESP32/pull/1615>
|
||||
- Send MQTT heartbeat immediately after connection [#1628](https://github.com/emsesp/EMS-ESP32/issues/1628)
|
||||
- 16MB partitions with second nvs, larger FS, Coredump, optional factory partition
|
||||
- stop fetching empty telegrams after 5 min
|
||||
|
||||
## [3.6.4] November 24 2023
|
||||
|
||||
## **IMPORTANT! BREAKING CHANGES**
|
||||
@@ -236,7 +373,7 @@ There are breaking changes between 3.5.x and earlier versions of 3.6.0. Please r
|
||||
|
||||
- fix Table resizing in WebUI [#519](https://github.com/emsesp/EMS-ESP32/issues/519)
|
||||
- allow larger customization files [#570](https://github.com/emsesp/EMS-ESP32/issues/570)
|
||||
- losing entitiy wwcomfort [#581](https://github.com/emsesp/EMS-ESP32/issues/581)
|
||||
- losing entity wwcomfort [#581](https://github.com/emsesp/EMS-ESP32/issues/581)
|
||||
|
||||
## Changed
|
||||
|
||||
|
||||
@@ -1,11 +1 @@
|
||||
# Changelog
|
||||
|
||||
## [3.6.5]
|
||||
|
||||
## **IMPORTANT! BREAKING CHANGES**
|
||||
|
||||
## Added
|
||||
|
||||
## Fixed
|
||||
|
||||
## Changed
|
||||
|
||||
@@ -6,7 +6,7 @@ Everybody is welcome and invited to contribute to the EMS-ESP Project by:
|
||||
|
||||
- providing Pull Requests (Features, Fixes, suggestions)
|
||||
- testing new released features and report issues on your EMS equipment
|
||||
- contributing to missing [documentation](https://emsesp.github.io/docs)
|
||||
- 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.
|
||||
|
||||
|
||||
19
Makefile
19
Makefile
@@ -1,9 +1,11 @@
|
||||
#
|
||||
# GNUMakefile for EMS-ESP
|
||||
# This is mainly used to generate the .o files for SonarQube analysis
|
||||
#
|
||||
|
||||
NUMJOBS=${NUMJOBS:-" -j4 "}
|
||||
MAKEFLAGS+="j "
|
||||
# NUMJOBS=${NUMJOBS:-" -j10 "}
|
||||
# MAKEFLAGS+="j "
|
||||
|
||||
#----------------------------------------------------------------------
|
||||
# Project Structure
|
||||
#----------------------------------------------------------------------
|
||||
@@ -29,8 +31,7 @@ CHECKFLAGS = -q --force --std=c++11
|
||||
# Languages Standard
|
||||
#----------------------------------------------------------------------
|
||||
C_STANDARD := -std=c17
|
||||
# CXX_STANDARD := -std=c++17
|
||||
CXX_STANDARD := -std=gnu++11
|
||||
CXX_STANDARD := -std=gnu++14
|
||||
|
||||
# C_STANDARD := -std=c11
|
||||
# CXX_STANDARD := -std=c++11
|
||||
@@ -38,11 +39,11 @@ CXX_STANDARD := -std=gnu++11
|
||||
#----------------------------------------------------------------------
|
||||
# Defined Symbols
|
||||
#----------------------------------------------------------------------
|
||||
DEFINES += -DARDUINOJSON_ENABLE_STD_STRING=1 -DARDUINOJSON_ENABLE_PROGMEM=1 -DARDUINOJSON_ENABLE_ARDUINO_STRING -DARDUINOJSON_USE_DOUBLE=0
|
||||
DEFINES += -DEMSESP_DEBUG -DEMSESP_STANDALONE -DEMSESP_TEST -D__linux__ -DEMC_RX_BUFFER_SIZE=1500
|
||||
DEFINES += -DARDUINOJSON_ENABLE -DARDUINOJSON_ENABLE_ARDUINO_STRING -DARDUINOJSON_USE_DOUBLE=0
|
||||
DEFINES += -DEMSESP_STANDALONE -DEMSESP_TEST -DEMSESP_DEBUG -DEMC_RX_BUFFER_SIZE=1500
|
||||
DEFINES += $(ARGS)
|
||||
|
||||
DEFAULTS = -DEMSESP_DEFAULT_LOCALE=\"en\" -DEMSESP_DEFAULT_TX_MODE=8 -DEMSESP_DEFAULT_VERSION=\"3.6.4-dev\" -DEMSESP_DEFAULT_BOARD_PROFILE=\"S32\"
|
||||
DEFAULTS = -DEMSESP_DEFAULT_LOCALE=\"en\" -DEMSESP_DEFAULT_TX_MODE=8 -DEMSESP_DEFAULT_VERSION=\"3.7.0-dev\" -DEMSESP_DEFAULT_BOARD_PROFILE=\"S32\"
|
||||
|
||||
#----------------------------------------------------------------------
|
||||
# Sources & Files
|
||||
@@ -81,8 +82,8 @@ CPPFLAGS += -g3
|
||||
CPPFLAGS += -Os
|
||||
|
||||
CFLAGS += $(CPPFLAGS)
|
||||
CFLAGS += -Wall -Wextra -Werror -Wswitch-enum -Wno-unused-parameter -Wno-inconsistent-missing-override -Wno-missing-braces -Wno-unused-lambda-capture -Wno-sign-compare
|
||||
|
||||
CFLAGS += -Wall -Wextra -Werror -Wswitch-enum
|
||||
CFLAGS += -Wno-tautological-constant-out-of-range-compare -Wno-unused-parameter -Wno-inconsistent-missing-override -Wno-missing-braces -Wno-unused-lambda-capture -Wno-sign-compare
|
||||
CXXFLAGS += $(CFLAGS) -MMD
|
||||
|
||||
#----------------------------------------------------------------------
|
||||
|
||||
71
README.md
71
README.md
@@ -4,7 +4,7 @@
|
||||
[](https://github.com/emsesp/EMS-ESP32/commits/main)
|
||||
[](LICENSE)
|
||||
[](https://sonarcloud.io/summary/new_code?id=emsesp_EMS-ESP32)
|
||||
[](https://www.codacy.com/gh/emsesp/EMS-ESP32/dashboard?utm_source=github.com&utm_medium=referral&utm_content=emsesp/EMS-ESP32&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://discord.gg/3J3GgnzpyT)
|
||||
|
||||
@@ -12,41 +12,60 @@
|
||||
[](https://github.com/emsesp/EMS-ES32P/network)
|
||||
[](https://www.paypal.com/paypalme/prderbyshire/2)
|
||||
|
||||
**EMS-ESP** is an open-source firmware for the Espressif ESP32 microcontroller that communicates with **EMS** (Energy Management System) based equipment from manufacturers like Bosch, Buderus, Nefit, Junkers, Worcester and Sieger. It requires a small gateway circuit to interface with the EMS bus which can be purchased from <https://bbqkees-electronics.nl> or custom built.
|
||||
**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.
|
||||
|
||||
## **Features**
|
||||
It requires a small circuit to interface with the EMS bus which can be purchased from <https://bbqkees-electronics.nl> or custom built.
|
||||
|
||||
- A multi-user, multi-language secure web interface to change settings and monitor incoming data
|
||||
- A console, accessible via Serial and Telnet for more advanced monitoring
|
||||
- Native support for Home Assistant, Domoticz and openHAB via [MQTT Discovery](https://www.home-assistant.io/docs/mqtt/discovery/)
|
||||
- Can run standalone as an independent WiFi Access Point or join an existing WiFi network
|
||||
- Easy first-time configuration via a web Captive Portal
|
||||
- Support for more than [110+ EMS devices](https://emsesp.github.io/docs/All-Devices/) (boilers, thermostats, solar modules, mixer modules, heat pumps, gateways, switches, heat sources)
|
||||
## **Key Features**
|
||||
|
||||
- Compatible with EMS, EMS+, EMS2, EMS Plus, Logamatic EMS, Junkers 2-wire, Heatronic 3 and 4
|
||||
- Supporting over 120 different EMS compatible devices such as thermostats, boilers, heat pumps, mixing units, solar modules, connect modules, ventilation units, switches and more
|
||||
- Easy to add external Temperature and Analog sensors that are attached to GPIO pins on the ESP32 board
|
||||
- A multi-user, multi-language web interface to change settings and monitor incoming data
|
||||
- A simple to use console, accessible via Serial/USB or Telnet for advanced operations and detailed monitoring
|
||||
- Native integration with Home Assistant, Domoticz, openHAB and Modbus
|
||||
- Easy setup and install with automatic updates
|
||||
- Simulation of remote thermostats
|
||||
- Localized in 11 languages, and customizable to rename any device or sensor
|
||||
- Extendable by adding own custom EMS entities
|
||||
- Expandable via adding user-built external modules
|
||||
- A powerful Scheduler to automate tasks and trigger events based data changes
|
||||
- A Notification service to alert you of important events
|
||||
|
||||
## **Installing**
|
||||
|
||||
For a quick install of the latest stable release go to [https://install.emsesp.org](https://install.emsesp.org). For other methods of installing and upgrading, and switching over to the development version go to [this section](https://emsesp.org/Getting-Started/#first-time-install) in the documentation.
|
||||
|
||||
If you're upgrading a BBQKees Electronics EMS Gateway and unsure which firmware to use, please refer to the [this overview](https://emsesp.org/Getting-Started/#bbqkees-electronics-ems-gateway).
|
||||
|
||||
## **Documentation**
|
||||
|
||||
For the complete documentation on how to install, configure and get support visit the [EMS-ESP Wiki](https://emsesp.github.io/docs).
|
||||
Visit [emsesp.org](https://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.
|
||||
|
||||
## **Support**
|
||||
## **Getting Support**
|
||||
|
||||
To chat with the community reach out on our [Discord Server](https://discord.gg/3J3GgnzpyT).
|
||||
|
||||
If you like **EMS-ESP**, please give it a star, or fork it and contribute or offer a small donation!
|
||||
If you find an issue or have a request, see [here](https://emsesp.org/Support/) on how to submit a bug report or feature request.
|
||||
|
||||
## **Demo**
|
||||
## **Live Demo**
|
||||
|
||||
For a live demo of the Web UI click [here](https://ems-esp.derbyshire.nl) and log in with any username/password.
|
||||
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.
|
||||
|
||||
## **Contributors ✨**
|
||||
## **Contributors**
|
||||
|
||||
EMS-ESP is a project owned and maintained by [proddy](https://github.com/proddy) and [MichaelDvP](https://github.com/MichaelDvP).
|
||||
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).
|
||||
|
||||
You can contact us using [this form](https://emsesp.org/Contact/).
|
||||
|
||||
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.
|
||||
|
||||
## **Libraries used**
|
||||
|
||||
- [esp8266-react](https://github.com/rjwats/esp8266-react) by @rjwats for the framework that provides the core of the Web UI
|
||||
- [uuid-\*](https://github.com/nomis/mcu-uuid-console) from @nomis. The console, syslog, telnet and logging are based off these open source libraries
|
||||
- [ArduinoJson](https://github.com/bblanchon/ArduinoJson) for all the JSON
|
||||
- [espMqttClient](https://github.com/bertmelis/espMqttClient) for the MQTT client, with custom modifications from @MichaelDvP and @proddy
|
||||
- [esp8266-react](https://github.com/rjwats/esp8266-react) by @rjwats for the core framework that provides the Web UI, which has been heavily modified
|
||||
- [uuid-\*](https://github.com/nomis/mcu-uuid-console) from @nomis. The console, syslog, telnet and logging are based off these awesome open source libraries
|
||||
- [ArduinoJson](https://github.com/bblanchon/ArduinoJson) for all the JSON processing
|
||||
- [espMqttClient](https://github.com/bertmelis/espMqttClient) for the MQTT client
|
||||
- ESPAsyncWebServer and AsyncTCP for the Web server and TCP backends, with custom modifications for performance
|
||||
|
||||
## **License**
|
||||
@@ -59,14 +78,14 @@ This program is licensed under GPL-3.0
|
||||
|
||||
| | |
|
||||
| ---------------------------------- | -------------------------------- |
|
||||
| <img src="media/web_settings.png"> | <img src="media/web_status.png"> |
|
||||
| <img src="media/web_devices.png"> | <img src="media/web_mqtt.png"> |
|
||||
| <img src="media/web_edit.png"> | <img src="media/web_log.png"> |
|
||||
|  |  |
|
||||
|  |  |
|
||||
|  |  |
|
||||
|
||||
### Telnet Console
|
||||
|
||||
<img src="media/console0.png" width=80% height=80%>
|
||||

|
||||
|
||||
### In Home Assistant
|
||||
### Home Assistant
|
||||
|
||||
<img src="media/ha_lovelace.png" width=80% height=80%>
|
||||

|
||||
|
||||
13
cspell.json
Normal file
13
cspell.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json",
|
||||
"version": "0.2",
|
||||
"dictionaryDefinitions": [
|
||||
{
|
||||
"name": "project-words",
|
||||
"path": "./project-words.txt",
|
||||
"addWords": true
|
||||
}
|
||||
],
|
||||
"dictionaries": ["project-words"],
|
||||
"ignorePaths": ["node_modules", "compile_commands.json", "WWWData.h", "**/venv/**", "lib/eModbus", "lib/ESPAsyncWebServer", "lib/espMqttClient", "analyse.html", "dist", "**/*.csv", "locale_translations.h", "TZ.tsx", "**/*.txt","build/**", "**/i18n/**", "/project-words.txt"]
|
||||
}
|
||||
5473
docs/Modbus-Entity-Registers.md
Normal file
5473
docs/Modbus-Entity-Registers.md
Normal file
File diff suppressed because it is too large
Load Diff
9443
dump_entities.csv
9443
dump_entities.csv
File diff suppressed because it is too large
Load Diff
212
dump_telegrams.csv
Normal file
212
dump_telegrams.csv
Normal file
@@ -0,0 +1,212 @@
|
||||
telegram_type_id,name,is_fetched
|
||||
0x04,UBAFactory,fetched
|
||||
0x06,RCTime,
|
||||
0x0A,EasyMonitor,fetched
|
||||
0x10,UBAErrorMessage1,
|
||||
0x11,UBAErrorMessage2,
|
||||
0x12,RCErrorMessage,
|
||||
0x13,RCErrorMessage2,
|
||||
0x14,UBATotalUptime,fetched
|
||||
0x15,UBAMaintenanceData,
|
||||
0x16,UBAParameters,fetched
|
||||
0x18,UBAMonitorFast,
|
||||
0x19,UBAMonitorSlow,
|
||||
0x1A,UBASetPoints,
|
||||
0x1C,UBAMaintenanceStatus,
|
||||
0x1E,WM10TempMessage,
|
||||
0x23,JunkersSetMixer,fetched
|
||||
0x26,UBASettingsWW,fetched
|
||||
0x28,WeatherComp,fetched
|
||||
0x2A,MC110Status,
|
||||
0x2E,Meters,
|
||||
0x33,UBAParameterWW,fetched
|
||||
0x34,UBAMonitorWW,
|
||||
0x35,UBAFlags,
|
||||
0x37,WWSettings,fetched
|
||||
0x38,WWTimer,fetched
|
||||
0x39,WWCircTimer,fetched
|
||||
0x3A,RC30WWSettings,fetched
|
||||
0x3B,Energy,
|
||||
0x3D,RC35Set,
|
||||
0x3E,RC35Monitor,
|
||||
0x3F,RC35Timer,
|
||||
0x40,RC30Temp,
|
||||
0x41,RC30Monitor,
|
||||
0x42,RC35Timer2,
|
||||
0x47,RC35Set,
|
||||
0x48,RC35Monitor,
|
||||
0x49,RC35Timer,
|
||||
0x4C,RC35Timer2,
|
||||
0x51,RC35Set,
|
||||
0x52,RC35Monitor,
|
||||
0x53,RC35Timer,
|
||||
0x56,RC35Timer2,
|
||||
0x5B,RC35Set,
|
||||
0x5C,RC35Monitor,
|
||||
0x5D,RC35Timer,
|
||||
0x60,RC35Timer2,
|
||||
0x96,SM10Config,fetched
|
||||
0x97,SM10Monitor,
|
||||
0x9C,WM10MonitorMessage,
|
||||
0x9D,WM10SetMessage,
|
||||
0xA2,RCError,
|
||||
0xA3,RCOutdoorTemp,
|
||||
0xA5,IBASettings,fetched
|
||||
0xA7,RC30Set,
|
||||
0xA9,RC30Vacation,fetched
|
||||
0xAA,MMConfigMessage,fetched
|
||||
0xAB,MMStatusMessage,
|
||||
0xAC,MMSetMessage,
|
||||
0xAF,RC20Remote,
|
||||
0xB0,RC10Set,
|
||||
0xB1,RC10Monitor,
|
||||
0xBB,HybridSettings,fetched
|
||||
0xBF,ErrorMessage,
|
||||
0xC2,UBAErrorMessage3,
|
||||
0xD1,UBAOutdoorTemp,
|
||||
0xE3,UBAMonitorSlowPlus2,
|
||||
0xE4,UBAMonitorFastPlus,
|
||||
0xE5,UBAMonitorSlowPlus,
|
||||
0xE6,UBAParametersPlus,fetched
|
||||
0xE9,UBAMonitorWWPlus,
|
||||
0xEA,UBAParameterWWPlus,fetched
|
||||
0x0101,ISM1Set,fetched
|
||||
0x0103,ISM1StatusMessage,fetched
|
||||
0x0104,ISM2StatusMessage,
|
||||
0x010C,IPMStatusMessage,
|
||||
0x011E,IPMTempMessage,
|
||||
0x0165,JunkersSet,
|
||||
0x0166,JunkersSet,
|
||||
0x0167,JunkersSet,
|
||||
0x0168,JunkersSet,
|
||||
0x016F,JunkersMonitor,
|
||||
0x0170,JunkersMonitor,
|
||||
0x0171,JunkersMonitor,
|
||||
0x0172,JunkersMonitor,
|
||||
0x0179,JunkersSet,
|
||||
0x017A,JunkersSet,
|
||||
0x017B,JunkersSet,
|
||||
0x017C,JunkersSet,
|
||||
0x01D3,JunkersDhw,fetched
|
||||
0x023A,RC300OutdoorTemp,fetched
|
||||
0x023E,PVSettings,fetched
|
||||
0x0240,RC300Settings,fetched
|
||||
0x0267,RC300Floordry,
|
||||
0x0269,RC300Holiday1,fetched
|
||||
0x0291,HPMode,fetched
|
||||
0x0292,HPMode,fetched
|
||||
0x0293,HPMode,fetched
|
||||
0x0294,HPMode,fetched
|
||||
0x029B,RC300Curves,
|
||||
0x029C,RC300Curves,
|
||||
0x029D,RC300Curves,
|
||||
0x029E,RC300Curves,
|
||||
0x029F,RC300Curves,
|
||||
0x02A0,RC300Curves,
|
||||
0x02A1,RC300Curves,
|
||||
0x02A2,RC300Curves,
|
||||
0x02A5,RC300Monitor,
|
||||
0x02A6,RC300Monitor,
|
||||
0x02A7,RC300Monitor,
|
||||
0x02A8,RC300Monitor,
|
||||
0x02A9,RC300Monitor,
|
||||
0x02AA,RC300Monitor,
|
||||
0x02AB,RC300Monitor,
|
||||
0x02AC,RC300Monitor,
|
||||
0x02AF,RC300Summer,
|
||||
0x02B0,RC300Summer,
|
||||
0x02B1,RC300Summer,
|
||||
0x02B2,RC300Summer,
|
||||
0x02B3,RC300Summer,
|
||||
0x02B4,RC300Summer,
|
||||
0x02B5,RC300Summer,
|
||||
0x02B6,RC300Summer,
|
||||
0x02B9,RC300Set,
|
||||
0x02BA,RC300Set,
|
||||
0x02BB,RC300Set,
|
||||
0x02BC,RC300Set,
|
||||
0x02BD,RC300Set,
|
||||
0x02BE,RC300Set,
|
||||
0x02BF,RC300Set,
|
||||
0x02C0,RC300Set,
|
||||
0x02CC,RC300Set2,
|
||||
0x02CD,MMPLUSConfigMessage,fetched
|
||||
0x02CE,RC300Set2,
|
||||
0x02D0,RC300Set2,
|
||||
0x02D2,RC300Set2,
|
||||
0x02D5,MMPLUSConfigMessage,fetched
|
||||
0x02D6,HPPump2,fetched
|
||||
0x02D7,MMPLUSStatusMessage,
|
||||
0x02DF,MMPLUSStatusMessage,
|
||||
0x02F5,RC300WWmode,fetched
|
||||
0x02F6,RC300WW2mode,fetched
|
||||
0x031B,RC300WWtemp,fetched
|
||||
0x031D,RC300WWmode2,
|
||||
0x031E,RC300WWmode2,
|
||||
0x0358,SM100SystemConfig,fetched
|
||||
0x035A,SM100CircuitConfig,fetched
|
||||
0x035C,SM100HeatAssist,fetched
|
||||
0x035D,SM100Circuit2Config,fetched
|
||||
0x035F,SM100Config1,fetched
|
||||
0x0361,SM100Differential,fetched
|
||||
0x0362,SM100Monitor,
|
||||
0x0363,SM100Monitor2,
|
||||
0x0364,SM100Status,
|
||||
0x0366,SM100Config,
|
||||
0x036A,SM100Status2,
|
||||
0x0380,SM100CollectorConfig,fetched
|
||||
0x038E,SM100Energy,fetched
|
||||
0x0391,SM100Time,fetched
|
||||
0x0467,HPSet,
|
||||
0x0468,HPSet,
|
||||
0x0469,HPSet,
|
||||
0x046A,HPSet,
|
||||
0x0471,RC300Summer2,
|
||||
0x0472,RC300Summer2,
|
||||
0x0473,RC300Summer2,
|
||||
0x0474,RC300Summer2,
|
||||
0x0475,RC300Summer2,
|
||||
0x0476,RC300Summer2,
|
||||
0x0477,RC300Summer2,
|
||||
0x0478,RC300Summer2,
|
||||
0x047B,HP2,
|
||||
0x0484,HPSilentMode,fetched
|
||||
0x0485,HpCooling,fetched
|
||||
0x0486,HpInConfig,fetched
|
||||
0x0488,HPValve,fetched
|
||||
0x048A,HpPool,fetched
|
||||
0x048B,HPPumps,fetched
|
||||
0x048D,HpPower,fetched
|
||||
0x048F,HpTemperatures,
|
||||
0x0491,HPAdditionalHeater,fetched
|
||||
0x0492,HpHeaterConfig,fetched
|
||||
0x0494,UBAEnergySupplied,
|
||||
0x0495,UBAInformation,
|
||||
0x0499,HPDhwSettings,fetched
|
||||
0x049C,HPSettings2,fetched
|
||||
0x049D,HPSettings3,fetched
|
||||
0x04A2,HpInput,fetched
|
||||
0x04A5,HPFan,fetched
|
||||
0x04A7,HPPowerLimit,fetched
|
||||
0x04AA,HPPower2,fetched
|
||||
0x04AE,HPEnergy,fetched
|
||||
0x04AF,HPMeters,fetched
|
||||
0x056B,VentilationMode,fetched
|
||||
0x0583,VentilationMonitor,
|
||||
0x0585,Blowerspeed,
|
||||
0x0587,Bypass,
|
||||
0x05BA,HpPoolStatus,fetched
|
||||
0x05D9,Airquality,
|
||||
0x0772,HIUSettings,
|
||||
0x0779,HIUMonitor,
|
||||
0x0935,EM100SetMessage,fetched
|
||||
0x0936,EM100OutMessage,
|
||||
0x0937,EM100TempMessage,
|
||||
0x0938,EM100InputMessage,
|
||||
0x0939,EM100MonitorMessage,
|
||||
0x093A,EM100ConfigMessage,
|
||||
0x0998,HPSettings,fetched
|
||||
0x0999,HPFunctionTest,fetched
|
||||
0x099B,HPFlowTemp,
|
||||
0x099C,HPComp,
|
||||
0x09A0,HPTemperature,
|
||||
|
@@ -1,6 +1,9 @@
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
nvs, data, nvs, 0x9000, 0x5000,
|
||||
otadata, data, ota, , 0x2000,
|
||||
app0, app, ota_0, , 0x7F0000,
|
||||
app1, app, ota_1, , 0x7F0000,
|
||||
spiffs, data, spiffs, , 64K,
|
||||
nvs, data, nvs, 0x9000, 0x005000,
|
||||
otadata, data, ota, , 0x002000,
|
||||
boot, app, factory, , 0x480000,
|
||||
app0, app, ota_0, , 0x490000,
|
||||
app1, app, ota_1, , 0x490000,
|
||||
nvs1, data, nvs, , 0x040000,
|
||||
spiffs, data, spiffs, , 0x200000,
|
||||
coredump, data, coredump,, 0x010000,
|
||||
|
8
esp32_partition_32M.csv
Normal file
8
esp32_partition_32M.csv
Normal file
@@ -0,0 +1,8 @@
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
nvs, data, nvs, 0x9000, 0x005000,
|
||||
otadata, data, ota, , 0x002000,
|
||||
app0, app, ota_0, , 0xDD0000,
|
||||
app1, app, ota_1, , 0xDD0000,
|
||||
nvs1, data, nvs, , 0x040000,
|
||||
spiffs, data, spiffs, , 0x400000,
|
||||
coredump, data, coredump,, 0x010000,
|
||||
|
@@ -25,18 +25,12 @@ build_flags =
|
||||
-D FACTORY_NTP_TIME_ZONE_FORMAT=\"CET-1CEST,M3.5.0,M10.5.0/3\"
|
||||
-D FACTORY_NTP_SERVER=\"time.google.com\"
|
||||
|
||||
; OTA settings
|
||||
-D FACTORY_OTA_PORT=8266
|
||||
-D FACTORY_OTA_PASSWORD=\"ems-esp-neo\"
|
||||
-D FACTORY_OTA_ENABLED=false
|
||||
|
||||
; MQTT settings
|
||||
-D FACTORY_MQTT_ENABLED=false
|
||||
-D FACTORY_MQTT_HOST=\"\"
|
||||
-D FACTORY_MQTT_PORT=1883
|
||||
-D FACTORY_MQTT_USERNAME=\"\"
|
||||
-D FACTORY_MQTT_PASSWORD=\"\"
|
||||
-D FACTORY_MQTT_CLIENT_ID=\"ems-esp\"
|
||||
-D FACTORY_MQTT_KEEP_ALIVE=60
|
||||
-D FACTORY_MQTT_CLEAN_SESSION=false
|
||||
-D FACTORY_MQTT_MAX_TOPIC_LENGTH=128
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
node_modules/
|
||||
build/
|
||||
dist/
|
||||
.yarn/
|
||||
|
||||
.prettierrc
|
||||
.eslintrc*
|
||||
env.d.ts
|
||||
progmem-generator.js
|
||||
unpack.ts
|
||||
vite.config.ts
|
||||
package.json
|
||||
@@ -1,108 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"airbnb/hooks",
|
||||
"airbnb-typescript",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react/jsx-runtime",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
||||
"plugin:prettier/recommended",
|
||||
"plugin:import/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
},
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module",
|
||||
"tsconfigRootDir": ".",
|
||||
"project": ["tsconfig.json"]
|
||||
},
|
||||
"plugins": ["react", "@typescript-eslint", "autofix", "react-hooks"],
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
"typescript": {
|
||||
"project": "./tsconfig.json"
|
||||
}
|
||||
},
|
||||
"react": {
|
||||
"version": "18.x"
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"object-shorthand": "error",
|
||||
"no-console": "warn",
|
||||
"@typescript-eslint/consistent-type-definitions": ["off", "type"],
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/no-unsafe-call": "off",
|
||||
"@typescript-eslint/no-unsafe-enum-comparison": "off",
|
||||
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||
"@typescript-eslint/no-unsafe-return": "off",
|
||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||
"@typescript-eslint/naming-convention": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-unsafe-argument": "off",
|
||||
"@typescript-eslint/restrict-plus-operands": "off",
|
||||
"@typescript-eslint/no-unused-expressions": "off",
|
||||
"@typescript-eslint/no-implied-eval": "off",
|
||||
"@typescript-eslint/no-misused-promises": "off",
|
||||
"arrow-body-style": ["error", "as-needed"],
|
||||
"react-hooks/exhaustive-deps": "warn",
|
||||
"@typescript-eslint/consistent-type-imports": [
|
||||
"error",
|
||||
{
|
||||
"prefer": "type-imports"
|
||||
}
|
||||
],
|
||||
"import/order": [
|
||||
"warn",
|
||||
{
|
||||
"groups": ["builtin", "external", "parent", "sibling", "index", "object", "type"],
|
||||
"pathGroups": [
|
||||
{
|
||||
"pattern": "@/**/**",
|
||||
"group": "parent",
|
||||
"position": "before"
|
||||
}
|
||||
],
|
||||
"alphabetize": { "order": "asc" }
|
||||
}
|
||||
],
|
||||
// "autofix/no-unused-vars": [
|
||||
// "error",
|
||||
// {
|
||||
// "argsIgnorePattern": "^_",
|
||||
// "ignoreRestSiblings": true,
|
||||
// "destructuredArrayIgnorePattern": "^_"
|
||||
// }
|
||||
// ],
|
||||
"react/self-closing-comp": [
|
||||
"error",
|
||||
{
|
||||
"component": true,
|
||||
"html": true
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/ban-types": [
|
||||
"error",
|
||||
{
|
||||
"extendDefaults": true,
|
||||
"types": {
|
||||
"{}": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"prettier/prettier": [
|
||||
"error",
|
||||
{
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
4
interface/.gitattributes
vendored
Normal file
4
interface/.gitattributes
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/.yarn/** linguist-vendored
|
||||
/.yarn/releases/* binary
|
||||
/.yarn/plugins/**/* binary
|
||||
/.pnp.* binary linguist-generated
|
||||
7
interface/.gitignore
vendored
7
interface/.gitignore
vendored
@@ -1,7 +0,0 @@
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
@@ -1,6 +1,8 @@
|
||||
node_modules/
|
||||
build/
|
||||
dist/
|
||||
src/i18n/*
|
||||
|
||||
.prettierrc
|
||||
.yarn/
|
||||
.typesafe-i18n.json
|
||||
893
interface/.yarn/releases/yarn-4.0.2.cjs
vendored
893
interface/.yarn/releases/yarn-4.0.2.cjs
vendored
File diff suppressed because one or more lines are too long
@@ -1,7 +1 @@
|
||||
compressionLevel: mixed
|
||||
|
||||
enableGlobalCache: false
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.0.2.cjs
|
||||
|
||||
44
interface/eslint.config.js
Normal file
44
interface/eslint.config.js
Normal file
@@ -0,0 +1,44 @@
|
||||
// @ts-check
|
||||
import eslint from '@eslint/js';
|
||||
import prettierConfig from 'eslint-config-prettier';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default tseslint.config(
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
prettierConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: true,
|
||||
tsconfigRootDir: import.meta.dirname
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
ignores: [
|
||||
'dist/*',
|
||||
'*.mjs',
|
||||
'build/*',
|
||||
'*.js',
|
||||
'**/*.cjs',
|
||||
'**/unpack.ts',
|
||||
'i18n*.*'
|
||||
]
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-unsafe-enum-comparison': 'off',
|
||||
'@typescript-eslint/no-unused-expressions': 'off',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
'@typescript-eslint/no-unsafe-call': 'off',
|
||||
'@typescript-eslint/no-misused-promises': [
|
||||
'error',
|
||||
{
|
||||
checksVoidReturn: false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -1,78 +1,67 @@
|
||||
{
|
||||
"name": "EMS-ESP",
|
||||
"version": "3.6.5",
|
||||
"description": "build EMS-ESP WebUI",
|
||||
"homepage": "https://emsesp.github.io/docs",
|
||||
"author": "proddy",
|
||||
"version": "3.7.0",
|
||||
"description": "EMS-ESP WebUI",
|
||||
"homepage": "https://emsesp.org",
|
||||
"author": "proddy, emsesp.org",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"build-hosted": "typesafe-i18n --no-watch && vite build --mode hosted",
|
||||
"preview-standalone": "typesafe-i18n --no-watch && vite build && concurrently -c \"auto\" \"npm:mock-api\" \"vite preview\"",
|
||||
"mock-api": "node --watch ../mock-api ../mock-api/server.js",
|
||||
"standalone": "concurrently -c \"auto\" \"typesafe-i18n\" \"npm:mock-api\" \"vite\"",
|
||||
"preview-standalone": "typesafe-i18n --no-watch && vite build && concurrently -c \"auto\" \"npm:mock-rest\" \"vite preview\"",
|
||||
"mock-rest": "bun --watch ../mock-api/rest_server.ts",
|
||||
"standalone": "concurrently -c \"auto\" \"typesafe-i18n\" \"npm:mock-rest\" \"vite\"",
|
||||
"typesafe-i18n": "typesafe-i18n --no-watch",
|
||||
"webUI": "node progmem-generator.js",
|
||||
"format": "prettier --write '**/*.{ts,tsx,js,css,json,md}'",
|
||||
"lint": "eslint . --cache --fix"
|
||||
"format": "prettier -l -w '**/*.{ts,tsx,js,css,json,md}'",
|
||||
"lint": "eslint . --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@alova/adapter-xhr": "^1.0.1",
|
||||
"@babel/core": "^7.23.3",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/icons-material": "^5.14.18",
|
||||
"@mui/material": "^5.14.18",
|
||||
"@alova/adapter-xhr": "2.0.9",
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@mui/icons-material": "^6.1.5",
|
||||
"@mui/material": "^6.1.5",
|
||||
"@table-library/react-table-library": "4.1.7",
|
||||
"@types/imagemin": "^8.0.5",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^20.10.0",
|
||||
"@types/react": "^18.2.38",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"alova": "^2.14.0",
|
||||
"alova": "3.1.1",
|
||||
"async-validator": "^4.2.5",
|
||||
"history": "^5.3.0",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mime-types": "^2.1.35",
|
||||
"react": "latest",
|
||||
"react-dom": "latest",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-icons": "^4.12.0",
|
||||
"react-router-dom": "^6.20.0",
|
||||
"react-toastify": "^9.1.3",
|
||||
"sockette": "^2.0.6",
|
||||
"preact": "^10.24.3",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-icons": "^5.3.0",
|
||||
"react-router-dom": "^6.27.0",
|
||||
"react-toastify": "^10.0.6",
|
||||
"typesafe-i18n": "^5.26.2",
|
||||
"typescript": "^5.3.2"
|
||||
"typescript": "^5.6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@preact/compat": "^17.1.2",
|
||||
"@preact/preset-vite": "^2.7.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.12.0",
|
||||
"@typescript-eslint/parser": "^6.12.0",
|
||||
"concurrently": "^8.2.2",
|
||||
"eslint": "^8.54.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^17.1.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.1",
|
||||
"eslint-plugin-autofix": "^1.1.0",
|
||||
"eslint-plugin-import": "^2.29.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.8.0",
|
||||
"eslint-plugin-prettier": "alpha",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"preact": "^10.19.2",
|
||||
"prettier": "^3.1.0",
|
||||
"rollup-plugin-visualizer": "^5.9.3",
|
||||
"terser": "^5.24.0",
|
||||
"vite": "^5.0.2",
|
||||
"@babel/core": "^7.26.0",
|
||||
"@eslint/js": "^9.13.0",
|
||||
"@preact/compat": "^18.3.1",
|
||||
"@preact/preset-vite": "^2.9.1",
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||
"@types/formidable": "^3",
|
||||
"@types/node": "^22.8.1",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"concurrently": "^9.0.1",
|
||||
"eslint": "^9.13.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"formidable": "^3.5.2",
|
||||
"prettier": "^3.3.3",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"terser": "^5.36.0",
|
||||
"typescript-eslint": "8.11.0",
|
||||
"vite": "^5.4.10",
|
||||
"vite-plugin-imagemin": "^0.6.1",
|
||||
"vite-tsconfig-paths": "^4.2.1"
|
||||
"vite-tsconfig-paths": "^5.0.1"
|
||||
},
|
||||
"packageManager": "yarn@4.0.2"
|
||||
"packageManager": "yarn@4.5.1"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { readdirSync, existsSync, unlinkSync, readFileSync, createWriteStream } from 'fs';
|
||||
import { resolve, relative, sep } from 'path';
|
||||
import zlib from 'zlib';
|
||||
import crypto from 'crypto';
|
||||
import {
|
||||
createWriteStream,
|
||||
existsSync,
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
unlinkSync
|
||||
} from 'fs';
|
||||
import mime from 'mime-types';
|
||||
import { relative, resolve, sep } from 'path';
|
||||
import zlib from 'zlib';
|
||||
|
||||
const ARDUINO_INCLUDES = '#include <Arduino.h>\n\n';
|
||||
const INDENT = ' ';
|
||||
@@ -11,14 +18,13 @@ const bytesPerLine = 20;
|
||||
var totalSize = 0;
|
||||
|
||||
const generateWWWClass = () =>
|
||||
`typedef std::function<void(const String& uri, const String& contentType, const uint8_t * content, size_t len)> RouteRegistrationHandler;
|
||||
`typedef std::function<void(const char * uri, const String & contentType, const uint8_t * content, size_t len, const String & hash)> RouteRegistrationHandler;
|
||||
// Total size is ${totalSize} bytes
|
||||
|
||||
class WWWData {
|
||||
${indent}public:
|
||||
${indent.repeat(2)}static void registerRoutes(RouteRegistrationHandler handler) {
|
||||
${fileInfo
|
||||
.map((file) => `${indent.repeat(3)}handler("${file.uri}", "${file.mimeType}", ${file.variable}, ${file.size});`)
|
||||
.join('\n')}
|
||||
${fileInfo.map((file) => `${indent.repeat(3)}handler("${file.uri}", "${file.mimeType}", ${file.variable}, ${file.size}, "${file.hash}");`).join('\n')}
|
||||
${indent.repeat(2)}}
|
||||
};
|
||||
`;
|
||||
@@ -49,6 +55,12 @@ const writeFile = (relativeFilePath, buffer) => {
|
||||
writeStream.write('const uint8_t ' + variable + '[] = {');
|
||||
// const zipBuffer = zlib.brotliCompressSync(buffer, { quality: 1 });
|
||||
const zipBuffer = zlib.gzipSync(buffer, { level: 9 });
|
||||
|
||||
// create sha
|
||||
const hashSum = crypto.createHash('sha256');
|
||||
hashSum.update(zipBuffer);
|
||||
const hash = hashSum.digest('hex');
|
||||
|
||||
zipBuffer.forEach((b) => {
|
||||
if (!(size % bytesPerLine)) {
|
||||
writeStream.write('\n');
|
||||
@@ -57,15 +69,19 @@ const writeFile = (relativeFilePath, buffer) => {
|
||||
writeStream.write('0x' + ('00' + b.toString(16).toUpperCase()).slice(-2) + ',');
|
||||
size++;
|
||||
});
|
||||
|
||||
if (size % bytesPerLine) {
|
||||
writeStream.write('\n');
|
||||
}
|
||||
|
||||
writeStream.write('};\n\n');
|
||||
|
||||
fileInfo.push({
|
||||
uri: '/' + relativeFilePath.replace(sep, '/'),
|
||||
mimeType,
|
||||
variable,
|
||||
size
|
||||
size,
|
||||
hash
|
||||
});
|
||||
|
||||
// console.log(relativeFilePath + ' (size ' + size + ' bytes)');
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* Uses font-size 400 (normal) only and Latin (plus extra unicode chars) to keep flash memory to a minimum
|
||||
* Uses font-weight 400 (normal) only, no bold, and Latin with a few extra unicode chars.
|
||||
* This is to keep flash memory to a minimum
|
||||
* View fonts on https://fonts.google.com/
|
||||
* Download woff2 using e.g. https://fonts.googleapis.com/css2?family=Lato or https://fonts.googleapis.com/css2?family=Roboto
|
||||
*/
|
||||
@@ -12,7 +13,8 @@
|
||||
local('Roboto'),
|
||||
local('Roboto-Regular'),
|
||||
url(../fonts/re.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0104-0107, U+0118-0119, U+011E-011F, U+0130-0131, U+0141-0144, U+0152-0153, U+015A-015B,
|
||||
U+015E-015F, U+0179-017C, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193,
|
||||
unicode-range: U+0000-00FF, U+0104-0107, U+0118-0119, U+011E-011F, U+0130-0131,
|
||||
U+0141-0144, U+0152-0153, U+015A-015B, U+015E-015F, U+0179-017C, U+02BB-02BC,
|
||||
U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193,
|
||||
U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -1,21 +1,17 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ToastContainer, Slide } from 'react-toastify';
|
||||
|
||||
import { Slide, ToastContainer } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.min.css';
|
||||
|
||||
import { localStorageDetector } from 'typesafe-i18n/detectors';
|
||||
import { FeaturesLoader } from './contexts/features';
|
||||
import type { FC } from 'react';
|
||||
import AppRouting from 'AppRouting';
|
||||
import CustomTheme from 'CustomTheme';
|
||||
|
||||
import TypesafeI18n from 'i18n/i18n-react';
|
||||
import { detectLocale } from 'i18n/i18n-util';
|
||||
import { loadLocaleAsync } from 'i18n/i18n-util.async';
|
||||
import { localStorageDetector } from 'typesafe-i18n/detectors';
|
||||
|
||||
const detectedLocale = detectLocale(localStorageDetector);
|
||||
|
||||
const App: FC = () => {
|
||||
const App = () => {
|
||||
const [wasLoaded, setWasLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -27,9 +23,7 @@ const App: FC = () => {
|
||||
return (
|
||||
<TypesafeI18n locale={detectedLocale}>
|
||||
<CustomTheme>
|
||||
<FeaturesLoader>
|
||||
<AppRouting />
|
||||
</FeaturesLoader>
|
||||
<AppRouting />
|
||||
<ToastContainer
|
||||
position="bottom-left"
|
||||
autoClose={3000}
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { useContext, useEffect } from 'react';
|
||||
|
||||
import { Route, Routes, Navigate, useLocation } from 'react-router-dom';
|
||||
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import AuthenticatedRouting from 'AuthenticatedRouting';
|
||||
import SignIn from 'SignIn';
|
||||
import { RequireAuthenticated, RequireUnauthenticated } from 'components';
|
||||
|
||||
import { Authentication, AuthenticationContext } from 'contexts/authentication';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
@@ -17,7 +13,7 @@ interface SecurityRedirectProps {
|
||||
signOut?: boolean;
|
||||
}
|
||||
|
||||
const RootRedirect: FC<SecurityRedirectProps> = ({ message, signOut }) => {
|
||||
const RootRedirect = ({ message, signOut }: SecurityRedirectProps) => {
|
||||
const authenticationContext = useContext(AuthenticationContext);
|
||||
useEffect(() => {
|
||||
signOut && authenticationContext.signOut(false);
|
||||
@@ -26,29 +22,20 @@ const RootRedirect: FC<SecurityRedirectProps> = ({ message, signOut }) => {
|
||||
return <Navigate to="/" />;
|
||||
};
|
||||
|
||||
export const RemoveTrailingSlashes = () => {
|
||||
const location = useLocation();
|
||||
return (
|
||||
location.pathname.match('/.*/$') && (
|
||||
<Navigate
|
||||
to={{
|
||||
pathname: location.pathname.replace(/\/+$/, ''),
|
||||
search: location.search
|
||||
}}
|
||||
/>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const AppRouting: FC = () => {
|
||||
const AppRouting = () => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
return (
|
||||
<Authentication>
|
||||
<RemoveTrailingSlashes />
|
||||
<Routes>
|
||||
<Route path="/unauthorized" element={<RootRedirect message={LL.PLEASE_SIGNIN()} signOut />} />
|
||||
<Route path="/fileUpdated" element={<RootRedirect message={LL.UPLOAD_SUCCESSFUL()} />} />
|
||||
<Route
|
||||
path="/unauthorized"
|
||||
element={<RootRedirect message={LL.PLEASE_SIGNIN()} signOut />}
|
||||
/>
|
||||
<Route
|
||||
path="/fileUpdated"
|
||||
element={<RootRedirect message={LL.UPLOAD_SUCCESSFUL()} />}
|
||||
/>
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
|
||||
@@ -1,64 +1,76 @@
|
||||
import { Navigate, Routes, Route } from 'react-router-dom';
|
||||
import Dashboard from './project/Dashboard';
|
||||
import Help from './project/Help';
|
||||
import Settings from './project/Settings';
|
||||
import type { FC } from 'react';
|
||||
import { useContext } from 'react';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import { Layout, RequireAdmin } from 'components';
|
||||
import AccessPoint from 'framework/ap/AccessPoint';
|
||||
import Mqtt from 'framework/mqtt/Mqtt';
|
||||
import NetworkConnection from 'framework/network/NetworkConnection';
|
||||
import NetworkTime from 'framework/ntp/NetworkTime';
|
||||
import Security from 'framework/security/Security';
|
||||
import System from 'framework/system/System';
|
||||
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 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';
|
||||
|
||||
const AuthenticatedRouting: FC = () => (
|
||||
// const location = useLocation();
|
||||
// const navigate = useNavigate();
|
||||
// const handleApiResponseError = useCallback(
|
||||
// (error: AxiosError) => {
|
||||
// if (error.response && error.response.status === 401) {
|
||||
// AuthenticationApi.storeLoginRedirect(location);
|
||||
// navigate('/unauthorized');
|
||||
// }
|
||||
// return Promise.reject(error);
|
||||
// },
|
||||
// [location, navigate]
|
||||
// );
|
||||
// useEffect(() => {
|
||||
// const axiosHandlerId = AXIOS.interceptors.response.use((response) => response, handleApiResponseError);
|
||||
// return () => AXIOS.interceptors.response.eject(axiosHandlerId);
|
||||
// }, [handleApiResponseError]);
|
||||
const AuthenticatedRouting = () => {
|
||||
const { me } = useContext(AuthenticatedContext);
|
||||
return (
|
||||
<Layout>
|
||||
<Routes>
|
||||
<Route path="/dashboard/*" element={<Dashboard />} />
|
||||
<Route path="/devices/*" element={<Devices />} />
|
||||
<Route path="/sensors/*" element={<Sensors />} />
|
||||
<Route path="/status/*" element={<Status />} />
|
||||
<Route path="/help/*" element={<Help />} />
|
||||
<Route path="/*" element={<Navigate to="/" />} />
|
||||
|
||||
<Layout>
|
||||
<Routes>
|
||||
<Route path="/dashboard/*" element={<Dashboard />} />
|
||||
<Route
|
||||
path="/settings/*"
|
||||
element={
|
||||
<RequireAdmin>
|
||||
<Settings />
|
||||
</RequireAdmin>
|
||||
}
|
||||
/>
|
||||
<Route path="/help/*" element={<Help />} />
|
||||
<Route path="/status/hardwarestatus/*" element={<HardwareStatus />} />
|
||||
<Route path="/status/activity" element={<Activity />} />
|
||||
<Route path="/status/log" element={<SystemLog />} />
|
||||
<Route path="/status/mqtt" element={<MqttStatus />} />
|
||||
<Route path="/status/ntp" element={<NTPStatus />} />
|
||||
<Route path="/status/ap" element={<APStatus />} />
|
||||
<Route path="/status/network" element={<NetworkStatus />} />
|
||||
|
||||
<Route path="/network/*" element={<NetworkConnection />} />
|
||||
<Route path="/ap/*" element={<AccessPoint />} />
|
||||
<Route path="/ntp/*" element={<NetworkTime />} />
|
||||
<Route path="/mqtt/*" element={<Mqtt />} />
|
||||
<Route
|
||||
path="/security/*"
|
||||
element={
|
||||
<RequireAdmin>
|
||||
<Security />
|
||||
</RequireAdmin>
|
||||
}
|
||||
/>
|
||||
<Route path="/system/*" element={<System />} />
|
||||
<Route path="/*" element={<Navigate to="/" />} />
|
||||
</Routes>
|
||||
</Layout>
|
||||
);
|
||||
{me.admin && (
|
||||
<>
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
<Route path="/settings/version" element={<Version />} />
|
||||
<Route path="/settings/application" element={<ApplicationSettings />} />
|
||||
<Route path="/settings/mqtt" element={<MqttSettings />} />
|
||||
<Route path="/settings/ntp" element={<NTPSettings />} />
|
||||
<Route path="/settings/ap" element={<APSettings />} />
|
||||
<Route path="/settings/modules" element={<Modules />} />
|
||||
<Route path="/settings/upload" element={<DownloadUpload />} />
|
||||
|
||||
<Route path="/settings/network/*" element={<Network />} />
|
||||
<Route path="/settings/security/*" element={<Security />} />
|
||||
|
||||
<Route path="/customizations" element={<Customizations />} />
|
||||
<Route path="/scheduler" element={<Scheduler />} />
|
||||
<Route path="/customentities" element={<CustomEntities />} />
|
||||
</>
|
||||
)}
|
||||
</Routes>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthenticatedRouting;
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { CssBaseline } from '@mui/material';
|
||||
import { createTheme, responsiveFontSizes, ThemeProvider } from '@mui/material/styles';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { CssBaseline } from '@mui/material';
|
||||
import {
|
||||
ThemeProvider,
|
||||
createTheme,
|
||||
responsiveFontSizes
|
||||
} from '@mui/material/styles';
|
||||
|
||||
import type { RequiredChildrenProps } from 'utils';
|
||||
|
||||
export const dialogStyle = {
|
||||
@@ -10,8 +15,7 @@ export const dialogStyle = {
|
||||
borderColor: '#565656',
|
||||
borderStyle: 'solid',
|
||||
borderWidth: '1px'
|
||||
},
|
||||
backdropFilter: 'blur(1px)'
|
||||
}
|
||||
};
|
||||
|
||||
const theme = responsiveFontSizes(
|
||||
|
||||
@@ -1,40 +1,28 @@
|
||||
import ForwardIcon from '@mui/icons-material/Forward';
|
||||
import { Box, Paper, Typography, MenuItem, TextField, Button } from '@mui/material';
|
||||
import { useRequest } from 'alova';
|
||||
import { useContext, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { FeaturesContext } from './contexts/features';
|
||||
|
||||
import ForwardIcon from '@mui/icons-material/Forward';
|
||||
import { Box, Button, Paper, Typography } from '@mui/material';
|
||||
|
||||
import * as AuthenticationApi from 'components/routing/authentication';
|
||||
import { useRequest } from 'alova/client';
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
|
||||
import type { Locales } from 'i18n/i18n-types';
|
||||
import type { ChangeEventHandler, FC } from 'react';
|
||||
import type { SignInRequest } from 'types';
|
||||
import * as AuthenticationApi from 'api/authentication';
|
||||
import { PROJECT_NAME } from 'api/env';
|
||||
|
||||
import { ValidatedPasswordField, ValidatedTextField } from 'components';
|
||||
import {
|
||||
LanguageSelector,
|
||||
ValidatedPasswordField,
|
||||
ValidatedTextField
|
||||
} from 'components';
|
||||
import { AuthenticationContext } from 'contexts/authentication';
|
||||
|
||||
import DEflag from 'i18n/DE.svg';
|
||||
import FRflag from 'i18n/FR.svg';
|
||||
import GBflag from 'i18n/GB.svg';
|
||||
import ITflag from 'i18n/IT.svg';
|
||||
import NLflag from 'i18n/NL.svg';
|
||||
import NOflag from 'i18n/NO.svg';
|
||||
import PLflag from 'i18n/PL.svg';
|
||||
import SVflag from 'i18n/SV.svg';
|
||||
import TRflag from 'i18n/TR.svg';
|
||||
import { I18nContext } from 'i18n/i18n-react';
|
||||
import { loadLocaleAsync } from 'i18n/i18n-util.async';
|
||||
import { PROJECT_NAME } from 'env';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import type { SignInRequest } from 'types';
|
||||
import { onEnterCallback, updateValue } from 'utils';
|
||||
import { SIGN_IN_REQUEST_VALIDATOR, validate } from 'validators';
|
||||
|
||||
const SignIn: FC = () => {
|
||||
const SignIn = () => {
|
||||
const authenticationContext = useContext(AuthenticationContext);
|
||||
|
||||
const { LL, setLocale, locale } = useContext(I18nContext);
|
||||
|
||||
const { features } = useContext(FeaturesContext);
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const [signInRequest, setSignInRequest] = useState<SignInRequest>({
|
||||
username: '',
|
||||
@@ -43,11 +31,12 @@ const SignIn: FC = () => {
|
||||
const [processing, setProcessing] = useState<boolean>(false);
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
|
||||
const { send: callSignIn, onSuccess } = useRequest((request: SignInRequest) => AuthenticationApi.signIn(request), {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
onSuccess((response) => {
|
||||
const { send: callSignIn } = useRequest(
|
||||
(request: SignInRequest) => AuthenticationApi.signIn(request),
|
||||
{
|
||||
immediate: false
|
||||
}
|
||||
).onSuccess((response) => {
|
||||
if (response.data) {
|
||||
authenticationContext.signIn(response.data.access_token);
|
||||
}
|
||||
@@ -56,7 +45,7 @@ const SignIn: FC = () => {
|
||||
const updateLoginRequestValue = updateValue(setSignInRequest);
|
||||
|
||||
const signIn = async () => {
|
||||
await callSignIn(signInRequest).catch((event) => {
|
||||
await callSignIn(signInRequest).catch((event: Error) => {
|
||||
if (event.message === 'Unauthorized') {
|
||||
toast.warning(LL.INVALID_LOGIN());
|
||||
} else {
|
||||
@@ -74,21 +63,14 @@ const SignIn: FC = () => {
|
||||
try {
|
||||
await validate(SIGN_IN_REQUEST_VALIDATOR, signInRequest);
|
||||
await signIn();
|
||||
} catch (errors: any) {
|
||||
setFieldErrors(errors);
|
||||
} catch (error) {
|
||||
setFieldErrors(error as ValidateFieldsError);
|
||||
setProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const submitOnEnter = onEnterCallback(signIn);
|
||||
|
||||
const onLocaleSelected: ChangeEventHandler<HTMLInputElement> = async ({ target }) => {
|
||||
const loc = target.value as Locales;
|
||||
localStorage.setItem('lang', loc);
|
||||
await loadLocaleAsync(loc);
|
||||
setLocale(loc);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
display="flex"
|
||||
@@ -111,46 +93,8 @@ const SignIn: FC = () => {
|
||||
})}
|
||||
>
|
||||
<Typography variant="h4">{PROJECT_NAME}</Typography>
|
||||
<Typography variant="subtitle2">{features.version}</Typography>
|
||||
|
||||
<TextField name="locale" variant="outlined" value={locale} onChange={onLocaleSelected} size="small" select>
|
||||
<MenuItem key="de" value="de">
|
||||
<img src={DEflag} style={{ width: 16, verticalAlign: 'middle' }} />
|
||||
DE
|
||||
</MenuItem>
|
||||
<MenuItem key="en" value="en">
|
||||
<img src={GBflag} style={{ width: 16, verticalAlign: 'middle' }} />
|
||||
EN
|
||||
</MenuItem>
|
||||
<MenuItem key="fr" value="fr">
|
||||
<img src={FRflag} style={{ width: 16, verticalAlign: 'middle' }} />
|
||||
FR
|
||||
</MenuItem>
|
||||
<MenuItem key="it" value="it">
|
||||
<img src={ITflag} style={{ width: 16, verticalAlign: 'middle' }} />
|
||||
IT
|
||||
</MenuItem>
|
||||
<MenuItem key="nl" value="nl">
|
||||
<img src={NLflag} style={{ width: 16, verticalAlign: 'middle' }} />
|
||||
NL
|
||||
</MenuItem>
|
||||
<MenuItem key="no" value="no">
|
||||
<img src={NOflag} style={{ width: 16, verticalAlign: 'middle' }} />
|
||||
NO
|
||||
</MenuItem>
|
||||
<MenuItem key="pl" value="pl">
|
||||
<img src={PLflag} style={{ width: 16, verticalAlign: 'middle' }} />
|
||||
PL
|
||||
</MenuItem>
|
||||
<MenuItem key="sv" value="sv">
|
||||
<img src={SVflag} style={{ width: 16, verticalAlign: 'middle' }} />
|
||||
SV
|
||||
</MenuItem>
|
||||
<MenuItem key="tr" value="tr">
|
||||
<img src={TRflag} style={{ width: 16, verticalAlign: 'middle' }} />
|
||||
TR
|
||||
</MenuItem>
|
||||
</TextField>
|
||||
<LanguageSelector />
|
||||
|
||||
<Box display="flex" flexDirection="column" alignItems="center">
|
||||
<ValidatedTextField
|
||||
@@ -165,6 +109,12 @@ const SignIn: FC = () => {
|
||||
onChange={updateLoginRequestValue}
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
slotProps={{
|
||||
input: {
|
||||
autoCapitalize: 'none',
|
||||
autoCorrect: 'off'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<ValidatedPasswordField
|
||||
fieldErrors={fieldErrors}
|
||||
@@ -181,7 +131,13 @@ const SignIn: FC = () => {
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Button variant="contained" color="primary" sx={{ mt: 2 }} onClick={validateAndSignIn} disabled={processing}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{ mt: 2 }}
|
||||
onClick={validateAndSignIn}
|
||||
disabled={processing}
|
||||
>
|
||||
<ForwardIcon sx={{ mr: 1 }} />
|
||||
{LL.SIGN_IN()}
|
||||
</Button>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { APSettingsType, APStatusType } from 'types';
|
||||
|
||||
import { alovaInstance } from './endpoints';
|
||||
|
||||
import type { APSettings, APStatus } from 'types';
|
||||
|
||||
export const readAPStatus = () => alovaInstance.Get<APStatus>('/rest/apStatus');
|
||||
export const readAPSettings = () => alovaInstance.Get<APSettings>('/rest/apSettings');
|
||||
export const updateAPSettings = (data: APSettings) => alovaInstance.Post<APSettings>('/rest/apSettings', data);
|
||||
export const readAPStatus = () => alovaInstance.Get<APStatusType>('/rest/apStatus');
|
||||
export const readAPSettings = () =>
|
||||
alovaInstance.Get<APSettingsType>('/rest/apSettings');
|
||||
export const updateAPSettings = (data: APSettingsType) =>
|
||||
alovaInstance.Post<APSettingsType>('/rest/apSettings', data);
|
||||
|
||||
151
interface/src/api/app.ts
Normal file
151
interface/src/api/app.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { alovaInstance } from 'api/endpoints';
|
||||
|
||||
import type {
|
||||
APIcall,
|
||||
Action,
|
||||
Activity,
|
||||
CoreData,
|
||||
DashboardItem,
|
||||
DeviceData,
|
||||
DeviceEntity,
|
||||
Entities,
|
||||
EntityItem,
|
||||
ModuleItem,
|
||||
Modules,
|
||||
Schedule,
|
||||
ScheduleItem,
|
||||
SensorData,
|
||||
Settings,
|
||||
WriteAnalogSensor,
|
||||
WriteTemperatureSensor
|
||||
} from '../app/main/types';
|
||||
|
||||
// Dashboard
|
||||
export const readDashboard = () =>
|
||||
alovaInstance.Get<DashboardItem[]>('/rest/dashboardData', {
|
||||
responseType: 'arraybuffer' // uses msgpack
|
||||
});
|
||||
|
||||
// Devices
|
||||
export const readCoreData = () => alovaInstance.Get<CoreData>(`/rest/coreData`);
|
||||
export const readDeviceData = (id: number) =>
|
||||
alovaInstance.Get<DeviceData>('/rest/deviceData', {
|
||||
// alovaInstance.Get<DeviceData>(`/rest/deviceData/${id}`, {
|
||||
params: { id },
|
||||
responseType: 'arraybuffer' // uses msgpack
|
||||
});
|
||||
export const writeDeviceValue = (data: { id: number; c: string; v: unknown }) =>
|
||||
alovaInstance.Post('/rest/writeDeviceValue', data);
|
||||
|
||||
// Application Settings
|
||||
export const readSettings = () => alovaInstance.Get<Settings>('/rest/settings');
|
||||
export const writeSettings = (data: Settings) =>
|
||||
alovaInstance.Post('/rest/settings', data);
|
||||
export const getBoardProfile = (boardProfile: string) =>
|
||||
alovaInstance.Get('/rest/boardProfile', {
|
||||
params: { boardProfile }
|
||||
});
|
||||
|
||||
// Sensors
|
||||
export const readSensorData = () =>
|
||||
alovaInstance.Get<SensorData>('/rest/sensorData');
|
||||
export const writeTemperatureSensor = (ts: WriteTemperatureSensor) =>
|
||||
alovaInstance.Post('/rest/writeTemperatureSensor', ts);
|
||||
export const writeAnalogSensor = (as: WriteAnalogSensor) =>
|
||||
alovaInstance.Post('/rest/writeAnalogSensor', as);
|
||||
|
||||
// Activity
|
||||
export const readActivity = () => alovaInstance.Get<Activity>('/rest/activity');
|
||||
|
||||
// API
|
||||
export const API = (apiCall: APIcall) => alovaInstance.Post('/api', apiCall);
|
||||
|
||||
// Generic action
|
||||
export const callAction = (action: Action) =>
|
||||
alovaInstance.Post('/rest/action', action);
|
||||
|
||||
// SettingsCustomization
|
||||
export const readDeviceEntities = (id: number) =>
|
||||
// alovaInstance.Get<DeviceEntity[]>(`/rest/deviceEntities/${id}`, {
|
||||
alovaInstance.Get<DeviceEntity[]>(`/rest/deviceEntities`, {
|
||||
params: { id },
|
||||
responseType: 'arraybuffer',
|
||||
transform(data) {
|
||||
return (data as DeviceEntity[]).map((de: DeviceEntity) => ({
|
||||
...de,
|
||||
o_m: de.m,
|
||||
o_cn: de.cn,
|
||||
o_mi: de.mi,
|
||||
o_ma: de.ma
|
||||
}));
|
||||
}
|
||||
});
|
||||
export const resetCustomizations = () =>
|
||||
alovaInstance.Post('/rest/resetCustomizations');
|
||||
export const writeCustomizationEntities = (data: {
|
||||
id: number;
|
||||
entity_ids: string[];
|
||||
}) => alovaInstance.Post('/rest/customizationEntities', data);
|
||||
export const writeDeviceName = (data: { id: number; name: string }) =>
|
||||
alovaInstance.Post('/rest/writeDeviceName', data);
|
||||
|
||||
// SettingsScheduler
|
||||
export const readSchedule = () =>
|
||||
alovaInstance.Get<ScheduleItem[]>('/rest/schedule', {
|
||||
transform(data) {
|
||||
return (data as Schedule).schedule.map((si: ScheduleItem) => ({
|
||||
...si,
|
||||
o_id: si.id,
|
||||
o_active: si.active,
|
||||
o_deleted: si.deleted,
|
||||
o_flags: si.flags,
|
||||
o_time: si.time,
|
||||
o_cmd: si.cmd,
|
||||
o_value: si.value,
|
||||
o_name: si.name
|
||||
}));
|
||||
}
|
||||
});
|
||||
export const writeSchedule = (data: Schedule) =>
|
||||
alovaInstance.Post('/rest/schedule', data);
|
||||
|
||||
// Modules
|
||||
export const readModules = () =>
|
||||
alovaInstance.Get<ModuleItem[]>('/rest/modules', {
|
||||
transform(data) {
|
||||
return (data as Modules).modules.map((mi: ModuleItem) => ({
|
||||
...mi,
|
||||
o_enabled: mi.enabled,
|
||||
o_license: mi.license
|
||||
}));
|
||||
}
|
||||
});
|
||||
export const writeModules = (data: {
|
||||
key: string;
|
||||
enabled: boolean;
|
||||
license: string;
|
||||
}) => alovaInstance.Post('/rest/modules', data);
|
||||
|
||||
// CustomEntities
|
||||
export const readCustomEntities = () =>
|
||||
alovaInstance.Get<EntityItem[]>('/rest/customEntities', {
|
||||
transform(data) {
|
||||
return (data as Entities).entities.map((ei: EntityItem) => ({
|
||||
...ei,
|
||||
o_id: ei.id,
|
||||
o_ram: ei.ram,
|
||||
o_device_id: ei.device_id,
|
||||
o_type_id: ei.type_id,
|
||||
o_offset: ei.offset,
|
||||
o_factor: ei.factor,
|
||||
o_uom: ei.uom,
|
||||
o_value_type: ei.value_type,
|
||||
o_name: ei.name,
|
||||
o_writeable: ei.writeable,
|
||||
o_value: ei.value,
|
||||
o_deleted: ei.deleted
|
||||
}));
|
||||
}
|
||||
});
|
||||
export const writeCustomEntities = (data: Entities) =>
|
||||
alovaInstance.Post('/rest/customEntities', data);
|
||||
@@ -1,33 +1,36 @@
|
||||
import { xhrRequestAdapter } from '@alova/adapter-xhr';
|
||||
import { type AlovaXHRResponse, xhrRequestAdapter } from '@alova/adapter-xhr';
|
||||
import { createAlova } from 'alova';
|
||||
import ReactHook from 'alova/react';
|
||||
import { unpack } from '../api/unpack';
|
||||
|
||||
import { unpack } from './unpack';
|
||||
|
||||
export const ACCESS_TOKEN = 'access_token';
|
||||
|
||||
const host = window.location.host;
|
||||
export const WEB_SOCKET_ROOT = 'ws://' + host + '/ws/';
|
||||
export const EVENT_SOURCE_ROOT = 'http://' + host + '/es/';
|
||||
|
||||
export const alovaInstance = createAlova({
|
||||
statesHook: ReactHook,
|
||||
timeout: 3000, // 3 seconds but throwing a timeout error
|
||||
localCache: null,
|
||||
// localCache: {
|
||||
// timeout: 3000, // 3 seconds before throwing a timeout error, default is 0 = none
|
||||
cacheFor: null, // disable cache
|
||||
// cacheFor: {
|
||||
// GET: {
|
||||
// mode: 'placeholder', // see https://alova.js.org/learning/response-cache/#cache-replaceholder-mode
|
||||
// expire: 2000
|
||||
// mode: 'memory',
|
||||
// expire: 60 * 10 * 1000 // 60 seconds in cache
|
||||
// }
|
||||
// },
|
||||
requestAdapter: xhrRequestAdapter(),
|
||||
beforeRequest(method) {
|
||||
if (localStorage.getItem(ACCESS_TOKEN)) {
|
||||
method.config.headers.Authorization = 'Bearer ' + localStorage.getItem(ACCESS_TOKEN);
|
||||
method.config.headers.Authorization =
|
||||
'Bearer ' + localStorage.getItem(ACCESS_TOKEN);
|
||||
}
|
||||
// for simulating vrey slow networks
|
||||
// return new Promise((resolve) => {
|
||||
// const random = 3000 + Math.random() * 2000;
|
||||
// setTimeout(resolve, Math.floor(random));
|
||||
// });
|
||||
},
|
||||
|
||||
responded: {
|
||||
onSuccess: async (response) => {
|
||||
onSuccess: async (response: AlovaXHRResponse) => {
|
||||
// if (response.status === 202) {
|
||||
// throw new Error('Wait'); // wifi scan in progress
|
||||
// } else
|
||||
@@ -38,9 +41,9 @@ export const alovaInstance = createAlova({
|
||||
} else if (response.status >= 400) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
const data = await response.data;
|
||||
const data: ArrayBuffer = (await response.data) as ArrayBuffer;
|
||||
if (response.data instanceof ArrayBuffer) {
|
||||
return unpack(data);
|
||||
return unpack(data) as ArrayBuffer;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { alovaInstance } from './endpoints';
|
||||
|
||||
import type { Features } from 'types';
|
||||
|
||||
export const readFeatures = () => alovaInstance.Get<Features>('/rest/features');
|
||||
@@ -1,6 +1,10 @@
|
||||
import { alovaInstance } from './endpoints';
|
||||
import type { MqttSettings, MqttStatus } from 'types';
|
||||
import type { MqttSettingsType, MqttStatusType } from 'types';
|
||||
|
||||
export const readMqttStatus = () => alovaInstance.Get<MqttStatus>('/rest/mqttStatus');
|
||||
export const readMqttSettings = () => alovaInstance.Get<MqttSettings>('/rest/mqttSettings');
|
||||
export const updateMqttSettings = (data: MqttSettings) => alovaInstance.Post<MqttSettings>('/rest/mqttSettings', data);
|
||||
import { alovaInstance } from './endpoints';
|
||||
|
||||
export const readMqttStatus = () =>
|
||||
alovaInstance.Get<MqttStatusType>('/rest/mqttStatus');
|
||||
export const readMqttSettings = () =>
|
||||
alovaInstance.Get<MqttSettingsType>('/rest/mqttSettings');
|
||||
export const updateMqttSettings = (data: MqttSettingsType) =>
|
||||
alovaInstance.Post<MqttSettingsType>('/rest/mqttSettings', data);
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { NetworkSettingsType, NetworkStatusType, WiFiNetworkList } from 'types';
|
||||
|
||||
import { alovaInstance } from './endpoints';
|
||||
|
||||
import type { WiFiNetworkList, NetworkSettings, NetworkStatus } from 'types';
|
||||
|
||||
export const readNetworkStatus = () => alovaInstance.Get<NetworkStatus>('/rest/networkStatus');
|
||||
export const readNetworkStatus = () =>
|
||||
alovaInstance.Get<NetworkStatusType>('/rest/networkStatus');
|
||||
export const scanNetworks = () => alovaInstance.Get('/rest/scanNetworks');
|
||||
export const listNetworks = () =>
|
||||
alovaInstance.Get<WiFiNetworkList>('/rest/listNetworks', {
|
||||
name: 'listNetworks',
|
||||
timeout: 20000 // timeout 20 seconds
|
||||
timeout: 20000 // 20 seconds
|
||||
});
|
||||
export const readNetworkSettings = () =>
|
||||
alovaInstance.Get<NetworkSettings>('/rest/networkSettings', { name: 'networkSettings' });
|
||||
export const updateNetworkSettings = (wifiSettings: NetworkSettings) =>
|
||||
alovaInstance.Post<NetworkSettings>('/rest/networkSettings', wifiSettings);
|
||||
alovaInstance.Get<NetworkSettingsType>('/rest/networkSettings');
|
||||
export const updateNetworkSettings = (wifiSettings: NetworkSettingsType) =>
|
||||
alovaInstance.Post<NetworkSettingsType>('/rest/networkSettings', wifiSettings);
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import type { NTPSettingsType, NTPStatusType, Time } from 'types';
|
||||
|
||||
import { alovaInstance } from './endpoints';
|
||||
import type { NTPSettings, NTPStatus, Time } from 'types';
|
||||
|
||||
export const readNTPStatus = () => alovaInstance.Get<NTPStatus>('/rest/ntpStatus');
|
||||
export const readNTPStatus = () =>
|
||||
alovaInstance.Get<NTPStatusType>('/rest/ntpStatus');
|
||||
|
||||
export const readNTPSettings = () =>
|
||||
alovaInstance.Get<NTPSettings>('/rest/ntpSettings', {
|
||||
name: 'ntpSettings'
|
||||
});
|
||||
export const updateNTPSettings = (data: NTPSettings) => alovaInstance.Post<NTPSettings>('/rest/ntpSettings', data);
|
||||
alovaInstance.Get<NTPSettingsType>('/rest/ntpSettings', {});
|
||||
export const updateNTPSettings = (data: NTPSettingsType) =>
|
||||
alovaInstance.Post<NTPSettingsType>('/rest/ntpSettings', data);
|
||||
|
||||
export const updateTime = (data: Time) => alovaInstance.Post<Time>('/rest/time', data);
|
||||
export const updateTime = (data: Time) =>
|
||||
alovaInstance.Post<Time>('/rest/time', data);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { SecuritySettingsType, Token } from 'types';
|
||||
|
||||
import { alovaInstance } from './endpoints';
|
||||
|
||||
import type { SecuritySettings, Token } from 'types';
|
||||
export const readSecuritySettings = () =>
|
||||
alovaInstance.Get<SecuritySettingsType>('/rest/securitySettings');
|
||||
|
||||
export const readSecuritySettings = () => alovaInstance.Get<SecuritySettings>('/rest/securitySettings');
|
||||
|
||||
export const updateSecuritySettings = (securitySettings: SecuritySettings) =>
|
||||
export const updateSecuritySettings = (securitySettings: SecuritySettingsType) =>
|
||||
alovaInstance.Post('/rest/securitySettings', securitySettings);
|
||||
|
||||
export const generateToken = (username?: string) =>
|
||||
|
||||
@@ -1,33 +1,28 @@
|
||||
import type { LogSettings, SystemStatus } from 'types';
|
||||
|
||||
import { alovaInstance, alovaInstanceGH } from './endpoints';
|
||||
import type { OTASettings, SystemStatus, LogSettings } from 'types';
|
||||
|
||||
// SystemStatus - also used to ping in Restart monitor for pinging
|
||||
export const readSystemStatus = () => alovaInstance.Get<SystemStatus>('/rest/systemStatus');
|
||||
|
||||
// commands
|
||||
export const restart = () => alovaInstance.Post('/rest/restart');
|
||||
export const partition = () => alovaInstance.Post('/rest/partition');
|
||||
export const factoryReset = () => alovaInstance.Post('/rest/factoryReset');
|
||||
|
||||
// OTA
|
||||
export const readOTASettings = () => alovaInstance.Get<OTASettings>(`/rest/otaSettings`);
|
||||
export const updateOTASettings = (data: any) => alovaInstance.Post('/rest/otaSettings', data);
|
||||
// systemStatus - also used to ping in Restart monitor for pinging
|
||||
export const readSystemStatus = () =>
|
||||
alovaInstance.Get<SystemStatus>('/rest/systemStatus');
|
||||
|
||||
// SystemLog
|
||||
export const readLogSettings = () => alovaInstance.Get<LogSettings>(`/rest/logSettings`);
|
||||
export const updateLogSettings = (data: any) => alovaInstance.Post('/rest/logSettings', data);
|
||||
export const fetchLog = () => alovaInstance.Post('/rest/fetchLog');
|
||||
export const readLogSettings = () =>
|
||||
alovaInstance.Get<LogSettings>(`/rest/logSettings`);
|
||||
export const updateLogSettings = (data: LogSettings) =>
|
||||
alovaInstance.Post('/rest/logSettings', data);
|
||||
export const fetchLogES = () => alovaInstance.Get('/es/log');
|
||||
|
||||
// Get versions from github
|
||||
// Get versions from GitHub
|
||||
export const getStableVersion = () =>
|
||||
alovaInstanceGH.Get('latest', {
|
||||
transformData(response: any) {
|
||||
transform(response: { data: { name: string } }) {
|
||||
return response.data.name.substring(1);
|
||||
}
|
||||
});
|
||||
export const getDevVersion = () =>
|
||||
alovaInstanceGH.Get('tags/latest', {
|
||||
transformData(response: any) {
|
||||
transform(response: { data: { name: string } }) {
|
||||
return response.data.name.split(/\s+/).splice(-1)[0].substring(1);
|
||||
}
|
||||
});
|
||||
@@ -36,7 +31,6 @@ export const uploadFile = (file: File) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
return alovaInstance.Post('/rest/uploadFile', formData, {
|
||||
timeout: 60000, // override timeout for uploading firmware - 1 minute
|
||||
enableUpload: true
|
||||
timeout: 60000 // override timeout for uploading firmware - 1 minute
|
||||
});
|
||||
};
|
||||
|
||||
@@ -38,7 +38,8 @@ try {
|
||||
export class Unpackr {
|
||||
constructor(options) {
|
||||
if (options) {
|
||||
if (options.useRecords === false && options.mapsAsObjects === undefined) options.mapsAsObjects = true;
|
||||
if (options.useRecords === false && options.mapsAsObjects === undefined)
|
||||
options.mapsAsObjects = true;
|
||||
if (options.sequential && options.trusted !== false) {
|
||||
options.trusted = true;
|
||||
if (!options.structures && options.useRecords != false) {
|
||||
@@ -46,7 +47,8 @@ export class Unpackr {
|
||||
if (!options.maxSharedStructures) options.maxSharedStructures = 0;
|
||||
}
|
||||
}
|
||||
if (options.structures) options.structures.sharedLength = options.structures.length;
|
||||
if (options.structures)
|
||||
options.structures.sharedLength = options.structures.length;
|
||||
else if (options.getStructures) {
|
||||
(options.structures = []).uninitialized = true; // this is what we use to denote an uninitialized structures
|
||||
options.structures.sharedLength = 0;
|
||||
@@ -63,11 +65,14 @@ export class Unpackr {
|
||||
// re-entrant execution, save the state and restore it after we do this unpack
|
||||
return saveState(() => {
|
||||
clearSource();
|
||||
return this ? this.unpack(source, options) : Unpackr.prototype.unpack.call(defaultOptions, source, options);
|
||||
return this
|
||||
? this.unpack(source, options)
|
||||
: Unpackr.prototype.unpack.call(defaultOptions, source, options);
|
||||
});
|
||||
}
|
||||
if (!source.buffer && source.constructor === ArrayBuffer)
|
||||
source = typeof Buffer !== 'undefined' ? Buffer.from(source) : new Uint8Array(source);
|
||||
source =
|
||||
typeof Buffer !== 'undefined' ? Buffer.from(source) : new Uint8Array(source);
|
||||
if (typeof options === 'object') {
|
||||
srcEnd = options.end || source.length;
|
||||
position = options.start || 0;
|
||||
@@ -86,14 +91,21 @@ export class Unpackr {
|
||||
// new ones
|
||||
try {
|
||||
dataView =
|
||||
source.dataView || (source.dataView = new DataView(source.buffer, source.byteOffset, source.byteLength));
|
||||
source.dataView ||
|
||||
(source.dataView = new DataView(
|
||||
source.buffer,
|
||||
source.byteOffset,
|
||||
source.byteLength
|
||||
));
|
||||
} catch (error) {
|
||||
// if it doesn't have a buffer, maybe it is the wrong type of object
|
||||
src = null;
|
||||
if (source instanceof Uint8Array) throw error;
|
||||
throw new Error(
|
||||
'Source must be a Uint8Array or Buffer but was a ' +
|
||||
(source && typeof source == 'object' ? source.constructor.name : typeof source)
|
||||
(source && typeof source == 'object'
|
||||
? source.constructor.name
|
||||
: typeof source)
|
||||
);
|
||||
}
|
||||
if (this instanceof Unpackr) {
|
||||
@@ -117,7 +129,9 @@ export class Unpackr {
|
||||
try {
|
||||
sequentialMode = true;
|
||||
const size = source.length;
|
||||
const value = this ? this.unpack(source, size) : defaultUnpackr.unpack(source, size);
|
||||
const value = this
|
||||
? this.unpack(source, size)
|
||||
: defaultUnpackr.unpack(source, size);
|
||||
if (forEach) {
|
||||
if (forEach(value) === false) return;
|
||||
while (position < size) {
|
||||
@@ -145,9 +159,11 @@ export class Unpackr {
|
||||
}
|
||||
|
||||
_mergeStructures(loadedStructures, existingStructures) {
|
||||
if (onLoadedStructures) loadedStructures = onLoadedStructures.call(this, loadedStructures);
|
||||
if (onLoadedStructures)
|
||||
loadedStructures = onLoadedStructures.call(this, loadedStructures);
|
||||
loadedStructures = loadedStructures || [];
|
||||
if (Object.isFrozen(loadedStructures)) loadedStructures = loadedStructures.map((structure) => structure.slice(0));
|
||||
if (Object.isFrozen(loadedStructures))
|
||||
loadedStructures = loadedStructures.map((structure) => structure.slice(0));
|
||||
for (let i = 0, l = loadedStructures.length; i < l; i++) {
|
||||
const structure = loadedStructures[i];
|
||||
if (structure) {
|
||||
@@ -162,7 +178,8 @@ export class Unpackr {
|
||||
const existing = existingStructures[id];
|
||||
if (existing) {
|
||||
if (structure)
|
||||
(loadedStructures.restoreStructures || (loadedStructures.restoreStructures = []))[id] = structure;
|
||||
(loadedStructures.restoreStructures ||
|
||||
(loadedStructures.restoreStructures = []))[id] = structure;
|
||||
loadedStructures[id] = existing;
|
||||
}
|
||||
}
|
||||
@@ -181,10 +198,16 @@ export function checkedRead(options: any) {
|
||||
try {
|
||||
if (!currentUnpackr.trusted && !sequentialMode) {
|
||||
const sharedLength = currentStructures.sharedLength || 0;
|
||||
if (sharedLength < currentStructures.length) currentStructures.length = sharedLength;
|
||||
if (sharedLength < currentStructures.length)
|
||||
currentStructures.length = sharedLength;
|
||||
}
|
||||
let result;
|
||||
if (currentUnpackr.randomAccessStructure && src[position] < 0x40 && src[position] >= 0x20 && readStruct) {
|
||||
if (
|
||||
currentUnpackr.randomAccessStructure &&
|
||||
src[position] < 0x40 &&
|
||||
src[position] >= 0x20 &&
|
||||
readStruct
|
||||
) {
|
||||
result = readStruct(src, position, srcEnd, currentUnpackr);
|
||||
src = null; // dispose of this so that recursive unpack calls don't save state
|
||||
if (!(options && options.lazy) && result) result = result.toJSON();
|
||||
@@ -198,7 +221,8 @@ export function checkedRead(options: any) {
|
||||
|
||||
if (position == srcEnd) {
|
||||
// finished reading this source, cleanup references
|
||||
if (currentStructures && currentStructures.restoreStructures) restoreStructures();
|
||||
if (currentStructures && currentStructures.restoreStructures)
|
||||
restoreStructures();
|
||||
currentStructures = null;
|
||||
src = null;
|
||||
if (referenceMap) referenceMap = null;
|
||||
@@ -208,10 +232,9 @@ export function checkedRead(options: any) {
|
||||
} else if (!sequentialMode) {
|
||||
let jsonView;
|
||||
try {
|
||||
jsonView = JSON.stringify(result, (_, value) => (typeof value === 'bigint' ? `${value}n` : value)).slice(
|
||||
0,
|
||||
100
|
||||
);
|
||||
jsonView = JSON.stringify(result, (_, value) =>
|
||||
typeof value === 'bigint' ? `${value}n` : value
|
||||
).slice(0, 100);
|
||||
} catch (error) {
|
||||
jsonView = '(JSON view not available ' + error + ')';
|
||||
}
|
||||
@@ -220,9 +243,14 @@ export function checkedRead(options: any) {
|
||||
// else more to read, but we are reading sequentially, so don't clear source yet
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (currentStructures && currentStructures.restoreStructures) restoreStructures();
|
||||
if (currentStructures && currentStructures.restoreStructures)
|
||||
restoreStructures();
|
||||
clearSource();
|
||||
if (error instanceof RangeError || error.message.startsWith('Unexpected end of buffer') || position > srcEnd) {
|
||||
if (
|
||||
error instanceof RangeError ||
|
||||
error.message.startsWith('Unexpected end of buffer') ||
|
||||
position > srcEnd
|
||||
) {
|
||||
error.incomplete = true;
|
||||
}
|
||||
throw error;
|
||||
@@ -243,7 +271,8 @@ export function read() {
|
||||
if (token < 0x40) return token;
|
||||
else {
|
||||
const structure =
|
||||
currentStructures[token & 0x3f] || (currentUnpackr.getStructures && loadStructures()[token & 0x3f]);
|
||||
currentStructures[token & 0x3f] ||
|
||||
(currentUnpackr.getStructures && loadStructures()[token & 0x3f]);
|
||||
if (structure) {
|
||||
if (!structure.read) {
|
||||
structure.read = createStructureReader(structure, token & 0x3f);
|
||||
@@ -282,7 +311,10 @@ export function read() {
|
||||
// fixstr
|
||||
const length = token - 0xa0;
|
||||
if (srcStringEnd >= position) {
|
||||
return srcString.slice(position - srcStringStart, (position += length) - srcStringStart);
|
||||
return srcString.slice(
|
||||
position - srcStringStart,
|
||||
(position += length) - srcStringStart
|
||||
);
|
||||
}
|
||||
if (srcStringEnd == 0 && srcEnd < 140) {
|
||||
// for small blocks, avoiding the overhead of the extract call is helpful
|
||||
@@ -298,8 +330,16 @@ export function read() {
|
||||
case 0xc1:
|
||||
if (bundledStrings) {
|
||||
value = read(); // followed by the length of the string in characters (not bytes!)
|
||||
if (value > 0) return bundledStrings[1].slice(bundledStrings.position1, (bundledStrings.position1 += value));
|
||||
else return bundledStrings[0].slice(bundledStrings.position0, (bundledStrings.position0 -= value));
|
||||
if (value > 0)
|
||||
return bundledStrings[1].slice(
|
||||
bundledStrings.position1,
|
||||
(bundledStrings.position1 += value)
|
||||
);
|
||||
else
|
||||
return bundledStrings[0].slice(
|
||||
bundledStrings.position0,
|
||||
(bundledStrings.position0 -= value)
|
||||
);
|
||||
}
|
||||
return C1; // "never-used", return special object to denote that
|
||||
case 0xc2:
|
||||
@@ -338,7 +378,8 @@ export function read() {
|
||||
value = dataView.getFloat32(position);
|
||||
if (currentUnpackr.useFloat32 > 2) {
|
||||
// this does rounding of numbers that were encoded in 32-bit float to nearest significant decimal digit that could be preserved
|
||||
const multiplier = mult10[((src[position] & 0x7f) << 1) | (src[position + 1] >> 7)];
|
||||
const multiplier =
|
||||
mult10[((src[position] & 0x7f) << 1) | (src[position + 1] >> 7)];
|
||||
position += 4;
|
||||
return ((multiplier * value + (value > 0 ? 0.5 : -0.5)) >> 0) / multiplier;
|
||||
}
|
||||
@@ -391,7 +432,8 @@ export function read() {
|
||||
value = dataView.getBigInt64(position).toString();
|
||||
} else if (currentUnpackr.int64AsType === 'auto') {
|
||||
value = dataView.getBigInt64(position);
|
||||
if (value >= BigInt(-2) << BigInt(52) && value <= BigInt(2) << BigInt(52)) value = Number(value);
|
||||
if (value >= BigInt(-2) << BigInt(52) && value <= BigInt(2) << BigInt(52))
|
||||
value = Number(value);
|
||||
} else value = dataView.getBigInt64(position);
|
||||
position += 8;
|
||||
return value;
|
||||
@@ -433,7 +475,10 @@ export function read() {
|
||||
// str 8
|
||||
value = src[position++];
|
||||
if (srcStringEnd >= position) {
|
||||
return srcString.slice(position - srcStringStart, (position += value) - srcStringStart);
|
||||
return srcString.slice(
|
||||
position - srcStringStart,
|
||||
(position += value) - srcStringStart
|
||||
);
|
||||
}
|
||||
return readString8(value);
|
||||
case 0xda:
|
||||
@@ -441,7 +486,10 @@ export function read() {
|
||||
value = dataView.getUint16(position);
|
||||
position += 2;
|
||||
if (srcStringEnd >= position) {
|
||||
return srcString.slice(position - srcStringStart, (position += value) - srcStringStart);
|
||||
return srcString.slice(
|
||||
position - srcStringStart,
|
||||
(position += value) - srcStringStart
|
||||
);
|
||||
}
|
||||
return readString16(value);
|
||||
case 0xdb:
|
||||
@@ -449,7 +497,10 @@ export function read() {
|
||||
value = dataView.getUint32(position);
|
||||
position += 4;
|
||||
if (srcStringEnd >= position) {
|
||||
return srcString.slice(position - srcStringStart, (position += value) - srcStringStart);
|
||||
return srcString.slice(
|
||||
position - srcStringStart,
|
||||
(position += value) - srcStringStart
|
||||
);
|
||||
}
|
||||
return readString32(value);
|
||||
case 0xdc:
|
||||
@@ -504,7 +555,8 @@ function createStructureReader(structure, firstId) {
|
||||
.join(',') +
|
||||
'})}'
|
||||
)(read));
|
||||
if (structure.highByte === 0) structure.read = createSecondByteReader(firstId, structure.read);
|
||||
if (structure.highByte === 0)
|
||||
structure.read = createSecondByteReader(firstId, structure.read);
|
||||
return readObject(); // second byte is already read, if there is one so immediately read object
|
||||
}
|
||||
const object = {};
|
||||
@@ -527,7 +579,8 @@ const createSecondByteReader = (firstId, read0) =>
|
||||
function () {
|
||||
const highByte = src[position++];
|
||||
if (highByte === 0) return read0();
|
||||
const id = firstId < 32 ? -(firstId + (highByte << 5)) : firstId + (highByte << 5);
|
||||
const id =
|
||||
firstId < 32 ? -(firstId + (highByte << 5)) : firstId + (highByte << 5);
|
||||
const structure = currentStructures[id] || loadStructures()[id];
|
||||
if (!structure) {
|
||||
throw new Error('Record id is not defined for ' + id);
|
||||
@@ -542,7 +595,10 @@ export function loadStructures() {
|
||||
src = null;
|
||||
return currentUnpackr.getStructures();
|
||||
});
|
||||
return (currentStructures = currentUnpackr._mergeStructures(loadedStructures, currentStructures));
|
||||
return (currentStructures = currentUnpackr._mergeStructures(
|
||||
loadedStructures,
|
||||
currentStructures
|
||||
));
|
||||
}
|
||||
|
||||
var readFixedString = readStringJS;
|
||||
@@ -563,7 +619,11 @@ export function setExtractor(extractStrings) {
|
||||
if (string == null) {
|
||||
if (bundledStrings) return readStringJS(length);
|
||||
const byteOffset = src.byteOffset;
|
||||
const extraction = extractStrings(position - headerLength + byteOffset, srcEnd + byteOffset, src.buffer);
|
||||
const extraction = extractStrings(
|
||||
position - headerLength + byteOffset,
|
||||
srcEnd + byteOffset,
|
||||
src.buffer
|
||||
);
|
||||
if (typeof extraction == 'string') {
|
||||
string = extraction;
|
||||
strings = EMPTY_ARRAY;
|
||||
@@ -593,7 +653,8 @@ function readStringJS(length) {
|
||||
if (length < 16) {
|
||||
if ((result = shortStringInJS(length))) return result;
|
||||
}
|
||||
if (length > 64 && decoder) return decoder.decode(src.subarray(position, (position += length)));
|
||||
if (length > 64 && decoder)
|
||||
return decoder.decode(src.subarray(position, (position += length)));
|
||||
const end = position + length;
|
||||
const units = [];
|
||||
result = '';
|
||||
@@ -616,7 +677,8 @@ function readStringJS(length) {
|
||||
const byte2 = src[position++] & 0x3f;
|
||||
const byte3 = src[position++] & 0x3f;
|
||||
const byte4 = src[position++] & 0x3f;
|
||||
let unit = ((byte1 & 0x07) << 0x12) | (byte2 << 0x0c) | (byte3 << 0x06) | byte4;
|
||||
let unit =
|
||||
((byte1 & 0x07) << 0x12) | (byte2 << 0x0c) | (byte3 << 0x06) | byte4;
|
||||
if (unit > 0xffff) {
|
||||
unit -= 0x10000;
|
||||
units.push(((unit >>> 10) & 0x3ff) | 0xd800);
|
||||
@@ -810,7 +872,8 @@ function shortStringInJS(length) {
|
||||
position -= 14;
|
||||
return;
|
||||
}
|
||||
if (length < 15) return fromCharCode(a, b, c, d, e, f, g, h, i, j, k, l, m, n);
|
||||
if (length < 15)
|
||||
return fromCharCode(a, b, c, d, e, f, g, h, i, j, k, l, m, n);
|
||||
const o = src[position++];
|
||||
if ((o & 0x80) > 0) {
|
||||
position -= 15;
|
||||
@@ -862,14 +925,17 @@ function readExt(length) {
|
||||
const type = src[position++];
|
||||
if (currentExtensions[type]) {
|
||||
let end;
|
||||
return currentExtensions[type](src.subarray(position, (end = position += length)), (readPosition) => {
|
||||
position = readPosition;
|
||||
try {
|
||||
return read();
|
||||
} finally {
|
||||
position = end;
|
||||
return currentExtensions[type](
|
||||
src.subarray(position, (end = position += length)),
|
||||
(readPosition) => {
|
||||
position = readPosition;
|
||||
try {
|
||||
return read();
|
||||
} finally {
|
||||
position = end;
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
} else throw new Error('Unknown extension type ' + type);
|
||||
}
|
||||
|
||||
@@ -881,14 +947,20 @@ function readKey() {
|
||||
length = length - 0xa0;
|
||||
if (srcStringEnd >= position)
|
||||
// if it has been extracted, must use it (and faster anyway)
|
||||
return srcString.slice(position - srcStringStart, (position += length) - srcStringStart);
|
||||
return srcString.slice(
|
||||
position - srcStringStart,
|
||||
(position += length) - srcStringStart
|
||||
);
|
||||
else if (!(srcStringEnd == 0 && srcEnd < 180)) return readFixedString(length);
|
||||
} else {
|
||||
// not cacheable, go back and do a standard read
|
||||
position--;
|
||||
return read().toString();
|
||||
}
|
||||
const key = ((length << 5) ^ (length > 1 ? dataView.getUint16(position) : length > 0 ? src[position] : 0)) & 0xfff;
|
||||
const key =
|
||||
((length << 5) ^
|
||||
(length > 1 ? dataView.getUint16(position) : length > 0 ? src[position] : 0)) &
|
||||
0xfff;
|
||||
let entry = keyCache[key];
|
||||
let checkPosition = position;
|
||||
let end = position + length - 3;
|
||||
@@ -947,7 +1019,8 @@ const recordDefinition = (id, highByte) => {
|
||||
}
|
||||
const existingStructure = currentStructures[id];
|
||||
if (existingStructure && existingStructure.isShared) {
|
||||
(currentStructures.restoreStructures || (currentStructures.restoreStructures = []))[id] = existingStructure;
|
||||
(currentStructures.restoreStructures ||
|
||||
(currentStructures.restoreStructures = []))[id] = existingStructure;
|
||||
}
|
||||
currentStructures[id] = structure;
|
||||
structure.read = createStructureReader(structure, firstByte);
|
||||
@@ -1009,7 +1082,8 @@ export const typedArrays = [
|
||||
currentExtensions[0x74] = (data) => {
|
||||
const typeCode = data[0];
|
||||
const typedArrayName = typedArrays[typeCode];
|
||||
if (!typedArrayName) throw new Error('Could not find typed array for code ' + typeCode);
|
||||
if (!typedArrayName)
|
||||
throw new Error('Could not find typed array for code ' + typeCode);
|
||||
// we have to always slice/copy here to get a new ArrayBuffer that is word/byte aligned
|
||||
return new glbl[typedArrayName](Uint8Array.prototype.slice.call(data, 1).buffer);
|
||||
};
|
||||
@@ -1033,11 +1107,20 @@ currentExtensions[0x62] = (data) => {
|
||||
|
||||
currentExtensions[0xff] = (data) => {
|
||||
// 32-bit date extension
|
||||
if (data.length == 4) return new Date((data[0] * 0x1000000 + (data[1] << 16) + (data[2] << 8) + data[3]) * 1000);
|
||||
if (data.length == 4)
|
||||
return new Date(
|
||||
(data[0] * 0x1000000 + (data[1] << 16) + (data[2] << 8) + data[3]) * 1000
|
||||
);
|
||||
else if (data.length == 8)
|
||||
return new Date(
|
||||
((data[0] << 22) + (data[1] << 14) + (data[2] << 6) + (data[3] >> 2)) / 1000000 +
|
||||
((data[3] & 0x3) * 0x100000000 + data[4] * 0x1000000 + (data[5] << 16) + (data[6] << 8) + data[7]) * 1000
|
||||
((data[0] << 22) + (data[1] << 14) + (data[2] << 6) + (data[3] >> 2)) /
|
||||
1000000 +
|
||||
((data[3] & 0x3) * 0x100000000 +
|
||||
data[4] * 0x1000000 +
|
||||
(data[5] << 16) +
|
||||
(data[6] << 8) +
|
||||
data[7]) *
|
||||
1000
|
||||
);
|
||||
else if (data.length == 12)
|
||||
return new Date(
|
||||
@@ -1070,7 +1153,10 @@ function saveState(callback) {
|
||||
|
||||
const savedSrc = new Uint8Array(src.slice(0, srcEnd)); // we copy the data in case it changes while external data is processed
|
||||
const savedStructures = currentStructures;
|
||||
const savedStructuresContents = currentStructures.slice(0, currentStructures.length);
|
||||
const savedStructuresContents = currentStructures.slice(
|
||||
0,
|
||||
currentStructures.length
|
||||
);
|
||||
const savedPackr = currentUnpackr;
|
||||
const savedSequentialMode = sequentialMode;
|
||||
const value = callback();
|
||||
@@ -1122,7 +1208,10 @@ const u8Array = new Uint8Array(f32Array.buffer, 0, 4);
|
||||
export function roundFloat32(float32Number) {
|
||||
f32Array[0] = float32Number;
|
||||
const multiplier = mult10[((u8Array[3] & 0x7f) << 1) | (u8Array[2] >> 7)];
|
||||
return ((multiplier * float32Number + (float32Number > 0 ? 0.5 : -0.5)) >> 0) / multiplier;
|
||||
return (
|
||||
((multiplier * float32Number + (float32Number > 0 ? 0.5 : -0.5)) >> 0) /
|
||||
multiplier
|
||||
);
|
||||
}
|
||||
export function setReadStruct(updatedReadStruct, loadedStructs, saveState) {
|
||||
readStruct = updatedReadStruct;
|
||||
|
||||
@@ -1,27 +1,41 @@
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
import { Button, Typography, Box } from '@mui/material';
|
||||
import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table';
|
||||
import { useTheme } from '@table-library/react-table-library/theme';
|
||||
// eslint-disable-next-line import/named
|
||||
import { updateState, useRequest } from 'alova';
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useBlocker } from 'react-router-dom';
|
||||
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import SettingsEntitiesDialog from './SettingsEntitiesDialog';
|
||||
import * as EMSESP from './api';
|
||||
import { DeviceValueTypeNames, DeviceValueUOM_s } from './types';
|
||||
import { entityItemValidation } from './validators';
|
||||
import type { EntityItem } from './types';
|
||||
import type { FC } from 'react';
|
||||
import { ButtonRow, FormLoader, SectionContent, BlockNavigation } from 'components';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
import { Box, Button, Typography } from '@mui/material';
|
||||
|
||||
import {
|
||||
Body,
|
||||
Cell,
|
||||
Header,
|
||||
HeaderCell,
|
||||
HeaderRow,
|
||||
Row,
|
||||
Table
|
||||
} from '@table-library/react-table-library/table';
|
||||
import { useTheme } from '@table-library/react-table-library/theme';
|
||||
import { updateState, useRequest } from 'alova/client';
|
||||
import {
|
||||
BlockNavigation,
|
||||
ButtonRow,
|
||||
FormLoader,
|
||||
SectionContent,
|
||||
useLayoutTitle
|
||||
} from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { useInterval } from 'utils';
|
||||
|
||||
const SettingsEntities: FC = () => {
|
||||
import { readCustomEntities, writeCustomEntities } from '../../api/app';
|
||||
import SettingsCustomEntitiesDialog from './CustomEntitiesDialog';
|
||||
import { DeviceValueTypeNames, DeviceValueUOM_s } from './types';
|
||||
import type { Entities, EntityItem } from './types';
|
||||
import { entityItemValidation } from './validators';
|
||||
|
||||
const CustomEntities = () => {
|
||||
const { LL } = useI18nContext();
|
||||
const [numChanges, setNumChanges] = useState<number>(0);
|
||||
const blocker = useBlocker(numChanges !== 0);
|
||||
@@ -29,20 +43,31 @@ const SettingsEntities: FC = () => {
|
||||
const [creating, setCreating] = useState<boolean>(false);
|
||||
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||
|
||||
useLayoutTitle(LL.CUSTOM_ENTITIES(0));
|
||||
|
||||
const {
|
||||
data: entities,
|
||||
send: fetchEntities,
|
||||
error
|
||||
} = useRequest(EMSESP.readCustomEntities, {
|
||||
initialData: [],
|
||||
force: true
|
||||
} = useRequest(readCustomEntities, {
|
||||
initialData: []
|
||||
});
|
||||
|
||||
const { send: writeEntities } = useRequest((data) => EMSESP.writeCustomEntities(data), { immediate: false });
|
||||
useInterval(() => {
|
||||
if (!dialogOpen && !numChanges) {
|
||||
void fetchEntities();
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
const { send: writeEntities } = useRequest(
|
||||
(data: Entities) => writeCustomEntities(data),
|
||||
{ immediate: false }
|
||||
);
|
||||
|
||||
function hasEntityChanged(ei: EntityItem) {
|
||||
return (
|
||||
ei.id !== ei.o_id ||
|
||||
ei.ram !== ei.o_ram ||
|
||||
(ei?.name || '') !== (ei?.o_name || '') ||
|
||||
ei.device_id !== ei.o_device_id ||
|
||||
ei.type_id !== ei.o_type_id ||
|
||||
@@ -51,7 +76,8 @@ const SettingsEntities: FC = () => {
|
||||
ei.factor !== ei.o_factor ||
|
||||
ei.value_type !== ei.o_value_type ||
|
||||
ei.writeable !== ei.o_writeable ||
|
||||
ei.deleted !== ei.o_deleted
|
||||
ei.deleted !== ei.o_deleted ||
|
||||
(ei.value || '') !== (ei.o_value || '')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -99,15 +125,10 @@ const SettingsEntities: FC = () => {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
.td {
|
||||
border-top: 1px solid #565656;
|
||||
border-bottom: 1px solid #565656;
|
||||
}
|
||||
&:hover .td {
|
||||
border-top: 1px solid #177ac9;
|
||||
border-bottom: 1px solid #177ac9;
|
||||
}
|
||||
&:nth-of-type(odd) .td {
|
||||
background-color: #303030;
|
||||
background-color: #177ac9;
|
||||
}
|
||||
`
|
||||
});
|
||||
@@ -118,6 +139,7 @@ const SettingsEntities: FC = () => {
|
||||
.filter((ei) => !ei.deleted)
|
||||
.map((condensed_ei) => ({
|
||||
id: condensed_ei.id,
|
||||
ram: condensed_ei.ram,
|
||||
name: condensed_ei.name,
|
||||
device_id: condensed_ei.device_id,
|
||||
type_id: condensed_ei.type_id,
|
||||
@@ -125,14 +147,15 @@ const SettingsEntities: FC = () => {
|
||||
factor: condensed_ei.factor,
|
||||
uom: condensed_ei.uom,
|
||||
writeable: condensed_ei.writeable,
|
||||
value_type: condensed_ei.value_type
|
||||
value_type: condensed_ei.value_type,
|
||||
value: condensed_ei.value
|
||||
}))
|
||||
})
|
||||
.then(() => {
|
||||
toast.success(LL.ENTITIES_UPDATED());
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
.catch((error: Error) => {
|
||||
toast.error(error.message);
|
||||
})
|
||||
.finally(async () => {
|
||||
await fetchEntities();
|
||||
@@ -158,11 +181,15 @@ const SettingsEntities: FC = () => {
|
||||
|
||||
const onDialogSave = (updatedItem: EntityItem) => {
|
||||
setDialogOpen(false);
|
||||
|
||||
updateState('entities', (data) => {
|
||||
void updateState(readCustomEntities(), (data: EntityItem[]) => {
|
||||
const new_data = creating
|
||||
? [...data.filter((ei) => creating || ei.o_id !== updatedItem.o_id), updatedItem]
|
||||
: data.map((ei) => (ei.id === updatedItem.id ? { ...ei, ...updatedItem } : ei));
|
||||
? [
|
||||
...data.filter((ei) => creating || ei.o_id !== updatedItem.o_id),
|
||||
updatedItem
|
||||
]
|
||||
: data.map((ei) =>
|
||||
ei.id === updatedItem.id ? { ...ei, ...updatedItem } : ei
|
||||
);
|
||||
setNumChanges(new_data.filter((ei) => hasEntityChanged(ei)).length);
|
||||
return new_data;
|
||||
});
|
||||
@@ -173,24 +200,27 @@ const SettingsEntities: FC = () => {
|
||||
setSelectedEntityItem({
|
||||
id: Math.floor(Math.random() * (Math.floor(200) - 100) + 100),
|
||||
name: '',
|
||||
device_id: '',
|
||||
type_id: '',
|
||||
ram: 0,
|
||||
device_id: '0',
|
||||
type_id: '0',
|
||||
offset: 0,
|
||||
factor: 1,
|
||||
uom: 0,
|
||||
value_type: 0,
|
||||
writeable: false,
|
||||
deleted: false
|
||||
deleted: false,
|
||||
value: ''
|
||||
});
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
function formatValue(value: any, uom: number) {
|
||||
return value === undefined || uom === undefined
|
||||
function formatValue(value: unknown, uom: number) {
|
||||
return value === undefined
|
||||
? ''
|
||||
: typeof value === 'number'
|
||||
? new Intl.NumberFormat().format(value) + (uom === 0 ? '' : ' ' + DeviceValueUOM_s[uom])
|
||||
: value;
|
||||
? new Intl.NumberFormat().format(value) +
|
||||
(uom === 0 ? '' : ' ' + DeviceValueUOM_s[uom])
|
||||
: (value as string);
|
||||
}
|
||||
|
||||
function showHex(value: number, digit: number) {
|
||||
@@ -203,8 +233,16 @@ const SettingsEntities: FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Table data={{ nodes: entities.filter((ei) => !ei.deleted) }} theme={entity_theme} layout={{ custom: true }}>
|
||||
{(tableList: any) => (
|
||||
<Table
|
||||
data={{
|
||||
nodes: entities
|
||||
.filter((ei) => !ei.deleted)
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
}}
|
||||
theme={entity_theme}
|
||||
layout={{ custom: true }}
|
||||
>
|
||||
{(tableList: EntityItem[]) => (
|
||||
<>
|
||||
<Header>
|
||||
<HeaderRow>
|
||||
@@ -212,18 +250,27 @@ const SettingsEntities: FC = () => {
|
||||
<HeaderCell stiff>{LL.ID_OF(LL.DEVICE())}</HeaderCell>
|
||||
<HeaderCell stiff>{LL.ID_OF(LL.TYPE(1))}</HeaderCell>
|
||||
<HeaderCell stiff>{LL.OFFSET()}</HeaderCell>
|
||||
<HeaderCell stiff>{LL.VALUE(1) + ' ' + LL.TYPE(1)}</HeaderCell>
|
||||
<HeaderCell stiff>{LL.VALUE(1)}</HeaderCell>
|
||||
<HeaderCell stiff>{LL.TYPE(0)}</HeaderCell>
|
||||
<HeaderCell stiff>{LL.VALUE(0)}</HeaderCell>
|
||||
</HeaderRow>
|
||||
</Header>
|
||||
<Body>
|
||||
{tableList.map((ei: EntityItem) => (
|
||||
<Row key={ei.name} item={ei} onClick={() => editEntityItem(ei)}>
|
||||
<Cell>{ei.name}</Cell>
|
||||
<Cell>{showHex(ei.device_id as number, 2)}</Cell>
|
||||
<Cell>{showHex(ei.type_id as number, 3)}</Cell>
|
||||
<Cell>{ei.offset}</Cell>
|
||||
<Cell>{DeviceValueTypeNames[ei.value_type]}</Cell>
|
||||
<Cell>
|
||||
{ei.name}
|
||||
{ei.writeable && (
|
||||
<EditOutlinedIcon color="primary" sx={{ fontSize: 12 }} />
|
||||
)}
|
||||
</Cell>
|
||||
<Cell>
|
||||
{ei.ram === 1 ? '' : showHex(ei.device_id as number, 2)}
|
||||
</Cell>
|
||||
<Cell>{ei.ram === 1 ? '' : showHex(ei.type_id as number, 3)}</Cell>
|
||||
<Cell>{ei.ram === 1 ? '' : ei.offset}</Cell>
|
||||
<Cell>
|
||||
{ei.ram === 1 ? 'RAM' : DeviceValueTypeNames[ei.value_type]}
|
||||
</Cell>
|
||||
<Cell>{formatValue(ei.value, ei.uom)}</Cell>
|
||||
</Row>
|
||||
))}
|
||||
@@ -235,30 +282,35 @@ const SettingsEntities: FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.CUSTOM_ENTITIES(0)} titleGutter>
|
||||
<SectionContent>
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
<Box mb={2} color="warning.main">
|
||||
<Typography variant="body2">{LL.ENTITIES_HELP_1()}</Typography>
|
||||
<Typography variant="body1">{LL.ENTITIES_HELP_1()}.</Typography>
|
||||
</Box>
|
||||
|
||||
{renderEntity()}
|
||||
|
||||
{selectedEntityItem && (
|
||||
<SettingsEntitiesDialog
|
||||
<SettingsCustomEntitiesDialog
|
||||
open={dialogOpen}
|
||||
creating={creating}
|
||||
onClose={onDialogClose}
|
||||
onSave={onDialogSave}
|
||||
selectedItem={selectedEntityItem}
|
||||
validator={entityItemValidation()}
|
||||
validator={entityItemValidation(entities, selectedEntityItem)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Box display="flex" flexWrap="wrap">
|
||||
<Box mt={1} display="flex" flexWrap="wrap">
|
||||
<Box flexGrow={1}>
|
||||
{numChanges > 0 && (
|
||||
<ButtonRow>
|
||||
<Button startIcon={<CancelIcon />} variant="outlined" onClick={onDialogCancel} color="secondary">
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
onClick={onDialogCancel}
|
||||
color="secondary"
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
@@ -273,15 +325,18 @@ const SettingsEntities: FC = () => {
|
||||
)}
|
||||
</Box>
|
||||
<Box flexWrap="nowrap" whiteSpace="nowrap">
|
||||
<ButtonRow>
|
||||
<Button startIcon={<AddIcon />} variant="outlined" color="secondary" onClick={addEntityItem}>
|
||||
{LL.ADD(0)}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
<Button
|
||||
startIcon={<AddIcon />}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={addEntityItem}
|
||||
>
|
||||
{LL.ADD(0)}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsEntities;
|
||||
export default CustomEntities;
|
||||
342
interface/src/app/main/CustomEntitiesDialog.tsx
Normal file
342
interface/src/app/main/CustomEntitiesDialog.tsx
Normal file
@@ -0,0 +1,342 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import DoneIcon from '@mui/icons-material/Done';
|
||||
import RemoveIcon from '@mui/icons-material/RemoveCircleOutline';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Checkbox,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
InputAdornment,
|
||||
MenuItem,
|
||||
TextField
|
||||
} from '@mui/material';
|
||||
import Grid from '@mui/material/Grid2';
|
||||
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import type Schema from 'async-validator';
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import { BlockFormControlLabel, ValidatedTextField } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { numberValue, updateValue } from 'utils';
|
||||
import { validate } from 'validators';
|
||||
|
||||
import { DeviceValueType, DeviceValueTypeNames, DeviceValueUOM_s } from './types';
|
||||
import type { EntityItem } from './types';
|
||||
|
||||
interface CustomEntitiesDialogProps {
|
||||
open: boolean;
|
||||
creating: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (ei: EntityItem) => void;
|
||||
selectedItem: EntityItem;
|
||||
validator: Schema;
|
||||
}
|
||||
|
||||
const CustomEntitiesDialog = ({
|
||||
open,
|
||||
creating,
|
||||
onClose,
|
||||
onSave,
|
||||
selectedItem,
|
||||
validator
|
||||
}: CustomEntitiesDialogProps) => {
|
||||
const { LL } = useI18nContext();
|
||||
const [editItem, setEditItem] = useState<EntityItem>(selectedItem);
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
const updateFormValue = updateValue(setEditItem);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setFieldErrors(undefined);
|
||||
setEditItem(selectedItem);
|
||||
// convert to hex strings straight away
|
||||
setEditItem({
|
||||
...selectedItem,
|
||||
device_id: selectedItem.device_id.toString(16).toUpperCase(),
|
||||
type_id: selectedItem.type_id.toString(16).toUpperCase()
|
||||
});
|
||||
}
|
||||
}, [open, selectedItem]);
|
||||
|
||||
const handleClose = (_event, reason: 'backdropClick' | 'escapeKeyDown') => {
|
||||
if (reason !== 'backdropClick') {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
try {
|
||||
setFieldErrors(undefined);
|
||||
await validate(validator, editItem);
|
||||
if (typeof editItem.device_id === 'string') {
|
||||
editItem.device_id = parseInt(editItem.device_id, 16);
|
||||
}
|
||||
if (typeof editItem.type_id === 'string') {
|
||||
editItem.type_id = parseInt(editItem.type_id, 16);
|
||||
}
|
||||
onSave(editItem);
|
||||
} catch (error) {
|
||||
setFieldErrors(error as ValidateFieldsError);
|
||||
}
|
||||
};
|
||||
|
||||
const remove = () => {
|
||||
editItem.deleted = true;
|
||||
onSave(editItem);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog sx={dialogStyle} open={open} onClose={handleClose}>
|
||||
<DialogTitle>
|
||||
{creating ? LL.ADD(1) + ' ' + LL.NEW(1) : LL.EDIT()} {LL.ENTITY()}
|
||||
</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Box display="flex" flexWrap="wrap" mb={1}>
|
||||
<Box flexWrap="nowrap" whiteSpace="nowrap" />
|
||||
</Box>
|
||||
<Grid container spacing={2} rowSpacing={0}>
|
||||
<Grid size={12}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="name"
|
||||
label={LL.NAME(0)}
|
||||
value={editItem.name}
|
||||
margin="normal"
|
||||
fullWidth
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<TextField
|
||||
name="ram"
|
||||
label={LL.VALUE(0) + ' ' + LL.TYPE(1)}
|
||||
value={editItem.ram}
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
fullWidth
|
||||
select
|
||||
>
|
||||
<MenuItem value={0}>EMS-{LL.VALUE(1)}</MenuItem>
|
||||
<MenuItem value={1}>RAM-{LL.VALUE(1)}</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
{editItem.ram === 1 && (
|
||||
<Grid>
|
||||
<TextField
|
||||
name="value"
|
||||
label={LL.DEFAULT(0) + ' ' + LL.VALUE(0)}
|
||||
type="string"
|
||||
value={editItem.value as string}
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
{editItem.ram === 0 && (
|
||||
<>
|
||||
<Grid mt={3} size={9}>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={editItem.writeable}
|
||||
onChange={updateFormValue}
|
||||
name="writeable"
|
||||
/>
|
||||
}
|
||||
label={LL.WRITEABLE()}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="device_id"
|
||||
label={LL.ID_OF(LL.DEVICE())}
|
||||
margin="normal"
|
||||
sx={{ width: '11ch' }}
|
||||
type="string"
|
||||
value={editItem.device_id as string}
|
||||
onChange={updateFormValue}
|
||||
slotProps={{
|
||||
input: {
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">0x</InputAdornment>
|
||||
)
|
||||
},
|
||||
htmlInput: { style: { textTransform: 'uppercase' } }
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="type_id"
|
||||
label={LL.ID_OF(LL.TYPE(1))}
|
||||
margin="normal"
|
||||
sx={{ width: '11ch' }}
|
||||
type="string"
|
||||
value={editItem.type_id as string}
|
||||
onChange={updateFormValue}
|
||||
slotProps={{
|
||||
input: {
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">0x</InputAdornment>
|
||||
)
|
||||
},
|
||||
htmlInput: { style: { textTransform: 'uppercase' } }
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="offset"
|
||||
label={LL.OFFSET()}
|
||||
margin="normal"
|
||||
sx={{ width: '11ch' }}
|
||||
type="number"
|
||||
value={numberValue(editItem.offset)}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<TextField
|
||||
name="value_type"
|
||||
label={LL.VALUE(0) + ' ' + LL.TYPE(1)}
|
||||
value={editItem.value_type}
|
||||
variant="outlined"
|
||||
sx={{ width: '11ch' }}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
select
|
||||
>
|
||||
<MenuItem value={DeviceValueType.BOOL}>
|
||||
{DeviceValueTypeNames[DeviceValueType.BOOL]}
|
||||
</MenuItem>
|
||||
<MenuItem value={DeviceValueType.INT8}>
|
||||
{DeviceValueTypeNames[DeviceValueType.INT8]}
|
||||
</MenuItem>
|
||||
<MenuItem value={DeviceValueType.UINT8}>
|
||||
{DeviceValueTypeNames[DeviceValueType.UINT8]}
|
||||
</MenuItem>
|
||||
<MenuItem value={DeviceValueType.INT16}>
|
||||
{DeviceValueTypeNames[DeviceValueType.INT16]}
|
||||
</MenuItem>
|
||||
<MenuItem value={DeviceValueType.UINT16}>
|
||||
{DeviceValueTypeNames[DeviceValueType.UINT16]}
|
||||
</MenuItem>
|
||||
<MenuItem value={DeviceValueType.UINT24}>
|
||||
{DeviceValueTypeNames[DeviceValueType.UINT24]}
|
||||
</MenuItem>
|
||||
<MenuItem value={DeviceValueType.TIME}>
|
||||
{DeviceValueTypeNames[DeviceValueType.TIME]}
|
||||
</MenuItem>
|
||||
<MenuItem value={DeviceValueType.UINT32}>
|
||||
{DeviceValueTypeNames[DeviceValueType.UINT32]}
|
||||
</MenuItem>
|
||||
<MenuItem value={DeviceValueType.STRING}>
|
||||
{DeviceValueTypeNames[DeviceValueType.STRING]}
|
||||
</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
|
||||
{editItem.value_type !== DeviceValueType.BOOL &&
|
||||
editItem.value_type !== DeviceValueType.STRING && (
|
||||
<>
|
||||
<Grid>
|
||||
<TextField
|
||||
name="factor"
|
||||
label={LL.FACTOR()}
|
||||
value={numberValue(editItem.factor)}
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
sx={{ width: '11ch' }}
|
||||
margin="normal"
|
||||
type="number"
|
||||
slotProps={{
|
||||
htmlInput: { step: '0.001' }
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<TextField
|
||||
name="uom"
|
||||
label={LL.UNIT()}
|
||||
value={editItem.uom}
|
||||
margin="normal"
|
||||
sx={{ width: '11ch' }}
|
||||
onChange={updateFormValue}
|
||||
select
|
||||
>
|
||||
{DeviceValueUOM_s.map((val, i) => (
|
||||
<MenuItem key={val} value={i}>
|
||||
{val}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
{editItem.value_type === DeviceValueType.STRING &&
|
||||
editItem.device_id !== '0' && (
|
||||
<Grid>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="factor"
|
||||
label="Bytes"
|
||||
value={numberValue(editItem.factor)}
|
||||
sx={{ width: '11ch' }}
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
type="number"
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
{!creating && (
|
||||
<Box flexGrow={1}>
|
||||
<Button
|
||||
startIcon={<RemoveIcon />}
|
||||
variant="outlined"
|
||||
color="warning"
|
||||
onClick={remove}
|
||||
>
|
||||
{LL.REMOVE()}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
onClick={onClose}
|
||||
color="secondary"
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={creating ? <AddIcon /> : <DoneIcon />}
|
||||
variant="outlined"
|
||||
onClick={save}
|
||||
color="primary"
|
||||
>
|
||||
{creating ? LL.ADD(0) : LL.UPDATE()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomEntitiesDialog;
|
||||
@@ -1,50 +1,70 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useBlocker, useLocation } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
|
||||
import SaveIcon from '@mui/icons-material/Save';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
import {
|
||||
Button,
|
||||
Typography,
|
||||
Box,
|
||||
MenuItem,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
InputAdornment,
|
||||
Link,
|
||||
MenuItem,
|
||||
TextField,
|
||||
ToggleButton,
|
||||
ToggleButtonGroup,
|
||||
Grid,
|
||||
TextField,
|
||||
Link,
|
||||
InputAdornment
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table';
|
||||
import { useTheme } from '@table-library/react-table-library/theme';
|
||||
import { useRequest } from 'alova';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useBlocker, useLocation } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
import Grid from '@mui/material/Grid2';
|
||||
|
||||
import {
|
||||
Body,
|
||||
Cell,
|
||||
Header,
|
||||
HeaderCell,
|
||||
HeaderRow,
|
||||
Row,
|
||||
Table
|
||||
} from '@table-library/react-table-library/table';
|
||||
import { useTheme } from '@table-library/react-table-library/theme';
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import { useRequest } from 'alova/client';
|
||||
import RestartMonitor from 'app/status/RestartMonitor';
|
||||
import {
|
||||
BlockNavigation,
|
||||
ButtonRow,
|
||||
MessageBox,
|
||||
SectionContent,
|
||||
useLayoutTitle
|
||||
} from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
import {
|
||||
API,
|
||||
readCoreData,
|
||||
readDeviceEntities,
|
||||
resetCustomizations,
|
||||
writeCustomizationEntities,
|
||||
writeDeviceName
|
||||
} from '../../api/app';
|
||||
import SettingsCustomizationsDialog from './CustomizationsDialog';
|
||||
import EntityMaskToggle from './EntityMaskToggle';
|
||||
import OptionIcon from './OptionIcon';
|
||||
import SettingsCustomizationDialog from './SettingsCustomizationDialog';
|
||||
|
||||
import * as EMSESP from './api';
|
||||
|
||||
import { DeviceEntityMask } from './types';
|
||||
import type { DeviceShort, DeviceEntity } from './types';
|
||||
import type { FC } from 'react';
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import * as SystemApi from 'api/system';
|
||||
import { ButtonRow, SectionContent, MessageBox, BlockNavigation } from 'components';
|
||||
|
||||
import RestartMonitor from 'framework/system/RestartMonitor';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import type { APIcall, Device, DeviceEntity } from './types';
|
||||
|
||||
export const APIURL = window.location.origin + '/api/';
|
||||
|
||||
const SettingsCustomization: FC = () => {
|
||||
const Customizations = () => {
|
||||
const { LL } = useI18nContext();
|
||||
const [numChanges, setNumChanges] = useState<number>(0);
|
||||
const blocker = useBlocker(numChanges !== 0);
|
||||
@@ -57,38 +77,72 @@ const SettingsCustomization: FC = () => {
|
||||
const [search, setSearch] = useState('');
|
||||
const [selectedDeviceEntity, setSelectedDeviceEntity] = useState<DeviceEntity>();
|
||||
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||
const [rename, setRename] = useState<boolean>(false);
|
||||
|
||||
// fetch devices first
|
||||
const { data: devices } = useRequest(EMSESP.readDevices);
|
||||
useLayoutTitle(LL.CUSTOMIZATIONS());
|
||||
|
||||
// const { state } = useLocation();
|
||||
const [selectedDevice, setSelectedDevice] = useState<number>(useLocation().state || -1);
|
||||
// fetch devices first from coreData
|
||||
const { data: devices, send: fetchCoreData } = useRequest(readCoreData);
|
||||
|
||||
const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
const [selectedDevice, setSelectedDevice] = useState<number>(
|
||||
Number(useLocation().state) || -1
|
||||
);
|
||||
const [selectedDeviceTypeNameURL, setSelectedDeviceTypeNameURL] =
|
||||
useState<string>(''); // needed for API URL
|
||||
const [selectedDeviceName, setSelectedDeviceName] = useState<string>('');
|
||||
|
||||
const { send: resetCustomizations } = useRequest(EMSESP.resetCustomizations(), {
|
||||
const { send: sendResetCustomizations } = useRequest(resetCustomizations(), {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
const { send: writeCustomizationEntities } = useRequest((data) => EMSESP.writeCustomizationEntities(data), {
|
||||
immediate: false
|
||||
});
|
||||
const { send: sendDeviceName } = useRequest(
|
||||
(data: { id: number; name: string }) => writeDeviceName(data),
|
||||
{
|
||||
immediate: false
|
||||
}
|
||||
);
|
||||
|
||||
const { send: readDeviceEntities, onSuccess: onSuccess } = useRequest((data) => EMSESP.readDeviceEntities(data), {
|
||||
initialData: [],
|
||||
immediate: false
|
||||
});
|
||||
const { send: sendCustomizationEntities } = useRequest(
|
||||
(data: { id: number; entity_ids: string[] }) => writeCustomizationEntities(data),
|
||||
{
|
||||
immediate: false
|
||||
}
|
||||
);
|
||||
|
||||
const setOriginalSettings = (data: DeviceEntity[]) => {
|
||||
setDeviceEntities(data.map((de) => ({ ...de, o_m: de.m, o_cn: de.cn, o_mi: de.mi, o_ma: de.ma })));
|
||||
};
|
||||
|
||||
onSuccess((event) => {
|
||||
const { send: sendDeviceEntities } = useRequest(
|
||||
(data: number) => readDeviceEntities(data),
|
||||
{
|
||||
initialData: [],
|
||||
immediate: false
|
||||
}
|
||||
).onSuccess((event) => {
|
||||
setOriginalSettings(event.data);
|
||||
});
|
||||
|
||||
const { send: restartCommand } = useRequest(SystemApi.restart(), {
|
||||
immediate: false
|
||||
});
|
||||
const setOriginalSettings = (data: DeviceEntity[]) => {
|
||||
setDeviceEntities(
|
||||
data.map((de) => ({
|
||||
...de,
|
||||
o_m: de.m,
|
||||
o_cn: de.cn,
|
||||
o_mi: de.mi,
|
||||
o_ma: de.ma
|
||||
}))
|
||||
);
|
||||
};
|
||||
|
||||
const doRestart = async () => {
|
||||
setRestarting(true);
|
||||
await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch(
|
||||
(error: Error) => {
|
||||
toast.error(error.message);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const entities_theme = useTheme({
|
||||
Table: `
|
||||
@@ -136,10 +190,7 @@ const SettingsCustomization: FC = () => {
|
||||
}
|
||||
&:hover .td {
|
||||
border-top: 1px solid #177ac9;
|
||||
border-bottom: 1px solid #177ac9;
|
||||
}
|
||||
&:nth-of-type(odd) .td {
|
||||
background-color: #303030;
|
||||
background-color: #177ac9;
|
||||
}
|
||||
`,
|
||||
Cell: `
|
||||
@@ -159,7 +210,12 @@ const SettingsCustomization: FC = () => {
|
||||
});
|
||||
|
||||
function hasEntityChanged(de: DeviceEntity) {
|
||||
return (de?.cn || '') !== (de?.o_cn || '') || de.m !== de.o_m || de.ma !== de.o_ma || de.mi !== de.o_mi;
|
||||
return (
|
||||
(de?.cn || '') !== (de?.o_cn || '') ||
|
||||
de.m !== de.o_m ||
|
||||
de.ma !== de.o_ma ||
|
||||
de.mi !== de.o_mi
|
||||
);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@@ -182,28 +238,21 @@ const SettingsCustomization: FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (devices && selectedDevice !== -1) {
|
||||
void readDeviceEntities(selectedDevice);
|
||||
const id = devices.devices.findIndex((d) => d.i === selectedDevice);
|
||||
if (id === -1) {
|
||||
void sendDeviceEntities(selectedDevice);
|
||||
const index = devices.devices.findIndex((d) => d.id === selectedDevice);
|
||||
if (index === -1) {
|
||||
setSelectedDevice(-1);
|
||||
setSelectedDeviceName('');
|
||||
setSelectedDeviceTypeNameURL('');
|
||||
} else {
|
||||
setSelectedDeviceName(devices.devices[id].tn || '');
|
||||
setSelectedDeviceTypeNameURL(devices.devices[index].url || '');
|
||||
setSelectedDeviceName(devices.devices[index].n);
|
||||
setNumChanges(0);
|
||||
setRestartNeeded(false);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [devices, selectedDevice]);
|
||||
|
||||
const restart = async () => {
|
||||
await restartCommand().catch((error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
setRestarting(true);
|
||||
};
|
||||
|
||||
function formatValue(value: any) {
|
||||
function formatValue(value: unknown) {
|
||||
if (typeof value === 'number') {
|
||||
return new Intl.NumberFormat().format(value);
|
||||
} else if (value === undefined) {
|
||||
@@ -211,12 +260,21 @@ const SettingsCustomization: FC = () => {
|
||||
} else if (typeof value === 'boolean') {
|
||||
return value ? 'true' : 'false';
|
||||
}
|
||||
return value;
|
||||
return value as string;
|
||||
}
|
||||
|
||||
const formatName = (de: DeviceEntity, withShortname: boolean) =>
|
||||
(de.n && de.n[0] === '!' ? LL.COMMAND(1) + ': ' + de.n.slice(1) : de.cn && de.cn !== '' ? de.cn : de.n) +
|
||||
(withShortname ? ' ' + de.id : '');
|
||||
(de.n && de.n[0] === '!'
|
||||
? de.t
|
||||
? LL.COMMAND(1) + ': ' + de.t + ' ' + de.n.slice(1)
|
||||
: LL.COMMAND(1) + ': ' + de.n.slice(1)
|
||||
: de.cn && de.cn !== ''
|
||||
? de.t
|
||||
? de.t + ' ' + de.cn
|
||||
: de.cn
|
||||
: de.t
|
||||
? de.t + ' ' + de.n
|
||||
: de.n) + (withShortname ? ' ' + de.id : '');
|
||||
|
||||
const getMaskNumber = (newMask: string[]) => {
|
||||
let new_mask = 0;
|
||||
@@ -247,7 +305,8 @@ const SettingsCustomization: FC = () => {
|
||||
};
|
||||
|
||||
const filter_entity = (de: DeviceEntity) =>
|
||||
(de.m & selectedFilters || !selectedFilters) && formatName(de, true).includes(search.toLocaleLowerCase());
|
||||
(de.m & selectedFilters || !selectedFilters) &&
|
||||
formatName(de, true).includes(search);
|
||||
|
||||
const maskDisabled = (set: boolean) => {
|
||||
setDeviceEntities(
|
||||
@@ -256,8 +315,14 @@ const SettingsCustomization: FC = () => {
|
||||
return {
|
||||
...de,
|
||||
m: set
|
||||
? de.m | (DeviceEntityMask.DV_API_MQTT_EXCLUDE | DeviceEntityMask.DV_WEB_EXCLUDE)
|
||||
: de.m & ~(DeviceEntityMask.DV_API_MQTT_EXCLUDE | DeviceEntityMask.DV_WEB_EXCLUDE)
|
||||
? de.m |
|
||||
(DeviceEntityMask.DV_API_MQTT_EXCLUDE |
|
||||
DeviceEntityMask.DV_WEB_EXCLUDE)
|
||||
: de.m &
|
||||
~(
|
||||
DeviceEntityMask.DV_API_MQTT_EXCLUDE |
|
||||
DeviceEntityMask.DV_WEB_EXCLUDE
|
||||
)
|
||||
};
|
||||
} else {
|
||||
return de;
|
||||
@@ -268,10 +333,10 @@ const SettingsCustomization: FC = () => {
|
||||
|
||||
const resetCustomization = async () => {
|
||||
try {
|
||||
await resetCustomizations();
|
||||
await sendResetCustomizations();
|
||||
toast.info(LL.CUSTOMIZATIONS_RESTART());
|
||||
} catch (error) {
|
||||
toast.error(error.message);
|
||||
toast.error((error as Error).message);
|
||||
} finally {
|
||||
setConfirmReset(false);
|
||||
}
|
||||
@@ -282,7 +347,11 @@ const SettingsCustomization: FC = () => {
|
||||
};
|
||||
|
||||
const updateDeviceEntity = (updatedItem: DeviceEntity) => {
|
||||
setDeviceEntities(deviceEntities?.map((de) => (de.id === updatedItem.id ? { ...de, ...updatedItem } : de)));
|
||||
setDeviceEntities(
|
||||
deviceEntities?.map((de) =>
|
||||
de.id === updatedItem.id ? { ...de, ...updatedItem } : de
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const onDialogSave = (updatedItem: DeviceEntity) => {
|
||||
@@ -324,7 +393,10 @@ const SettingsCustomization: FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
await writeCustomizationEntities({ id: selectedDevice, entity_ids: masked_entities }).catch((error) => {
|
||||
await sendCustomizationEntities({
|
||||
id: selectedDevice,
|
||||
entity_ids: masked_entities
|
||||
}).catch((error: Error) => {
|
||||
if (error.message === 'Reboot required') {
|
||||
setRestartNeeded(true);
|
||||
} else {
|
||||
@@ -335,31 +407,90 @@ const SettingsCustomization: FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const renameDevice = async () => {
|
||||
await sendDeviceName({ id: selectedDevice, name: selectedDeviceName })
|
||||
.then(() => {
|
||||
toast.success(LL.UPDATED_OF(LL.NAME(1)));
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(LL.UPDATE_OF(LL.NAME(1)) + ' ' + LL.FAILED(1));
|
||||
})
|
||||
.finally(async () => {
|
||||
setRename(false);
|
||||
await fetchCoreData();
|
||||
});
|
||||
};
|
||||
|
||||
const renderDeviceList = () => (
|
||||
<>
|
||||
<Box mb={1} color="warning.main">
|
||||
<Typography variant="body2">{LL.CUSTOMIZATIONS_HELP_1()}.</Typography>
|
||||
<Typography variant="body1">{LL.CUSTOMIZATIONS_HELP_1()}.</Typography>
|
||||
</Box>
|
||||
<Box display="flex" flexWrap="wrap" alignItems="center" gap={2}>
|
||||
{rename ? (
|
||||
<TextField
|
||||
name="device"
|
||||
label={LL.EMS_DEVICE()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={selectedDeviceName}
|
||||
onChange={(e) => setSelectedDeviceName(e.target.value)}
|
||||
margin="normal"
|
||||
/>
|
||||
) : (
|
||||
<TextField
|
||||
name="device"
|
||||
label={LL.EMS_DEVICE()}
|
||||
variant="outlined"
|
||||
value={selectedDevice}
|
||||
disabled={numChanges !== 0}
|
||||
onChange={(e) => setSelectedDevice(parseInt(e.target.value))}
|
||||
margin="normal"
|
||||
style={{ minWidth: '50%' }}
|
||||
select
|
||||
>
|
||||
<MenuItem disabled key={-1} value={-1}>
|
||||
{LL.SELECT_DEVICE()}...
|
||||
</MenuItem>
|
||||
{devices.devices.map(
|
||||
(device: Device) =>
|
||||
device.id < 90 && (
|
||||
<MenuItem key={device.id} value={device.id}>
|
||||
{device.n} ({device.tn})
|
||||
</MenuItem>
|
||||
)
|
||||
)}
|
||||
</TextField>
|
||||
)}
|
||||
{selectedDevice !== -1 &&
|
||||
(rename ? (
|
||||
<>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={() => setRename(false)}
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<SaveIcon />}
|
||||
variant="outlined"
|
||||
onClick={() => renameDevice()}
|
||||
>
|
||||
{LL.RENAME()}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
startIcon={<EditIcon />}
|
||||
variant="outlined"
|
||||
onClick={() => setRename(true)}
|
||||
>
|
||||
{LL.RENAME()}
|
||||
</Button>
|
||||
))}
|
||||
</Box>
|
||||
<TextField
|
||||
name="device"
|
||||
label={LL.EMS_DEVICE()}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={selectedDevice}
|
||||
disabled={numChanges !== 0}
|
||||
onChange={(e) => setSelectedDevice(parseInt(e.target.value))}
|
||||
margin="normal"
|
||||
select
|
||||
>
|
||||
<MenuItem disabled key={-1} value={-1}>
|
||||
{LL.SELECT_DEVICE()}...
|
||||
</MenuItem>
|
||||
{devices.devices.map((device: DeviceShort) => (
|
||||
<MenuItem key={device.i} value={device.i}>
|
||||
{device.s}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -370,15 +501,27 @@ const SettingsCustomization: FC = () => {
|
||||
<>
|
||||
<Box color="warning.main">
|
||||
<Typography variant="body2" mt={1}>
|
||||
<OptionIcon type="favorite" isSet={true} />={LL.CUSTOMIZATIONS_HELP_2()}
|
||||
<OptionIcon type="readonly" isSet={true} />={LL.CUSTOMIZATIONS_HELP_3()}
|
||||
<OptionIcon type="api_mqtt_exclude" isSet={true} />={LL.CUSTOMIZATIONS_HELP_4()}
|
||||
<OptionIcon type="web_exclude" isSet={true} />={LL.CUSTOMIZATIONS_HELP_5()}
|
||||
<OptionIcon type="favorite" isSet={true} />={LL.CUSTOMIZATIONS_HELP_2()}
|
||||
|
||||
<OptionIcon type="readonly" isSet={true} />={LL.CUSTOMIZATIONS_HELP_3()}
|
||||
|
||||
<OptionIcon type="api_mqtt_exclude" isSet={true} />=
|
||||
{LL.CUSTOMIZATIONS_HELP_4()}
|
||||
<OptionIcon type="web_exclude" isSet={true} />=
|
||||
{LL.CUSTOMIZATIONS_HELP_5()}
|
||||
<OptionIcon type="deleted" isSet={true} />={LL.CUSTOMIZATIONS_HELP_6()}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Grid container mb={1} mt={0} spacing={1} direction="row" justifyContent="flex-start" alignItems="center">
|
||||
<Grid item xs={2}>
|
||||
<Grid
|
||||
container
|
||||
mb={1}
|
||||
mt={0}
|
||||
spacing={2}
|
||||
direction="row"
|
||||
justifyContent="flex-start"
|
||||
alignItems="center"
|
||||
>
|
||||
<Grid>
|
||||
<TextField
|
||||
size="small"
|
||||
variant="outlined"
|
||||
@@ -386,21 +529,23 @@ const SettingsCustomization: FC = () => {
|
||||
onChange={(event) => {
|
||||
setSearch(event.target.value);
|
||||
}}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon color="primary" sx={{ fontSize: 16 }} />
|
||||
</InputAdornment>
|
||||
)
|
||||
slotProps={{
|
||||
input: {
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon color="primary" sx={{ fontSize: 16 }} />
|
||||
</InputAdornment>
|
||||
)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Grid>
|
||||
<ToggleButtonGroup
|
||||
size="small"
|
||||
color="secondary"
|
||||
value={getMaskString(selectedFilters)}
|
||||
onChange={(event, mask) => {
|
||||
onChange={(event, mask: string[]) => {
|
||||
setSelectedFilters(getMaskNumber(mask));
|
||||
}}
|
||||
>
|
||||
@@ -421,7 +566,7 @@ const SettingsCustomization: FC = () => {
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Grid>
|
||||
<Button
|
||||
size="small"
|
||||
sx={{ fontSize: 10 }}
|
||||
@@ -434,7 +579,7 @@ const SettingsCustomization: FC = () => {
|
||||
<OptionIcon type="web_exclude" isSet={false} />
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Grid>
|
||||
<Button
|
||||
size="small"
|
||||
sx={{ fontSize: 10 }}
|
||||
@@ -447,14 +592,19 @@ const SettingsCustomization: FC = () => {
|
||||
<OptionIcon type="web_exclude" isSet={true} />
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Grid>
|
||||
<Typography variant="subtitle2" color="primary">
|
||||
{LL.SHOWING()} {shown_data.length}/{deviceEntities.length} {LL.ENTITIES(deviceEntities.length)}
|
||||
{LL.SHOWING()} {shown_data.length}/{deviceEntities.length}
|
||||
{LL.ENTITIES(deviceEntities.length)}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Table data={{ nodes: shown_data }} theme={entities_theme} layout={{ custom: true }}>
|
||||
{(tableList: any) => (
|
||||
<Table
|
||||
data={{ nodes: shown_data }}
|
||||
theme={entities_theme}
|
||||
layout={{ custom: true }}
|
||||
>
|
||||
{(tableList: DeviceEntity[]) => (
|
||||
<>
|
||||
<Header>
|
||||
<HeaderRow>
|
||||
@@ -473,13 +623,20 @@ const SettingsCustomization: FC = () => {
|
||||
</Cell>
|
||||
<Cell>
|
||||
{formatName(de, false)} (
|
||||
<Link target="_blank" href={APIURL + selectedDeviceName + '/' + de.id}>
|
||||
<Link
|
||||
target="_blank"
|
||||
href={APIURL + selectedDeviceTypeNameURL + '/' + de.id}
|
||||
>
|
||||
{de.id}
|
||||
</Link>
|
||||
)
|
||||
</Cell>
|
||||
<Cell>{!(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.mi)}</Cell>
|
||||
<Cell>{!(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.ma)}</Cell>
|
||||
<Cell>
|
||||
{!(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.mi)}
|
||||
</Cell>
|
||||
<Cell>
|
||||
{!(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.ma)}
|
||||
</Cell>
|
||||
<Cell>{formatValue(de.v)}</Cell>
|
||||
</Row>
|
||||
))}
|
||||
@@ -492,14 +649,28 @@ const SettingsCustomization: FC = () => {
|
||||
};
|
||||
|
||||
const renderResetDialog = () => (
|
||||
<Dialog sx={dialogStyle} open={confirmReset} onClose={() => setConfirmReset(false)}>
|
||||
<Dialog
|
||||
sx={dialogStyle}
|
||||
open={confirmReset}
|
||||
onClose={() => setConfirmReset(false)}
|
||||
>
|
||||
<DialogTitle>{LL.RESET(1)}</DialogTitle>
|
||||
<DialogContent dividers>{LL.CUSTOMIZATIONS_RESET()}</DialogContent>
|
||||
<DialogActions>
|
||||
<Button startIcon={<CancelIcon />} variant="outlined" onClick={() => setConfirmReset(false)} color="secondary">
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
onClick={() => setConfirmReset(false)}
|
||||
color="secondary"
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button startIcon={<SettingsBackupRestoreIcon />} variant="outlined" onClick={resetCustomization} color="error">
|
||||
<Button
|
||||
startIcon={<SettingsBackupRestoreIcon />}
|
||||
variant="outlined"
|
||||
onClick={resetCustomization}
|
||||
color="error"
|
||||
>
|
||||
{LL.RESET(0)}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
@@ -508,19 +679,20 @@ const SettingsCustomization: FC = () => {
|
||||
|
||||
const renderContent = () => (
|
||||
<>
|
||||
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
|
||||
{LL.DEVICE_ENTITIES()}
|
||||
</Typography>
|
||||
{devices && renderDeviceList()}
|
||||
{deviceEntities && renderDeviceData()}
|
||||
{restartNeeded && (
|
||||
<MessageBox my={2} level="warning" message={LL.RESTART_TEXT()}>
|
||||
<Button startIcon={<PowerSettingsNewIcon />} variant="contained" color="error" onClick={restart}>
|
||||
{selectedDevice !== -1 && !rename && renderDeviceData()}
|
||||
{restartNeeded ? (
|
||||
<MessageBox my={2} level="warning" message={LL.RESTART_TEXT(0)}>
|
||||
<Button
|
||||
startIcon={<PowerSettingsNewIcon />}
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={doRestart}
|
||||
>
|
||||
{LL.RESTART()}
|
||||
</Button>
|
||||
</MessageBox>
|
||||
)}
|
||||
{!restartNeeded && (
|
||||
) : (
|
||||
<Box display="flex" flexWrap="wrap">
|
||||
<Box flexGrow={1}>
|
||||
{numChanges !== 0 && (
|
||||
@@ -529,7 +701,7 @@ const SettingsCustomization: FC = () => {
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={() => devices && readDeviceEntities(selectedDevice)}
|
||||
onClick={() => devices && sendDeviceEntities(selectedDevice)}
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
@@ -544,16 +716,18 @@ const SettingsCustomization: FC = () => {
|
||||
</ButtonRow>
|
||||
)}
|
||||
</Box>
|
||||
<ButtonRow>
|
||||
<Button
|
||||
startIcon={<SettingsBackupRestoreIcon />}
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={() => setConfirmReset(true)}
|
||||
>
|
||||
{LL.RESET(0)}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
{!rename && (
|
||||
<ButtonRow mt={1}>
|
||||
<Button
|
||||
startIcon={<SettingsBackupRestoreIcon />}
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={() => setConfirmReset(true)}
|
||||
>
|
||||
{LL.RESET(0)}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
{renderResetDialog()}
|
||||
@@ -561,11 +735,11 @@ const SettingsCustomization: FC = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.CUSTOMIZATIONS()} titleGutter>
|
||||
<SectionContent>
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
{restarting ? <RestartMonitor /> : renderContent()}
|
||||
{selectedDeviceEntity && (
|
||||
<SettingsCustomizationDialog
|
||||
<SettingsCustomizationsDialog
|
||||
open={dialogOpen}
|
||||
onClose={onDialogClose}
|
||||
onSave={onDialogSave}
|
||||
@@ -576,4 +750,4 @@ const SettingsCustomization: FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsCustomization;
|
||||
export default Customizations;
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import DoneIcon from '@mui/icons-material/Done';
|
||||
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
@@ -9,29 +10,32 @@ import {
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Grid,
|
||||
TextField,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Grid from '@mui/material/Grid2';
|
||||
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { numberValue, updateValue } from 'utils';
|
||||
|
||||
import EntityMaskToggle from './EntityMaskToggle';
|
||||
import { DeviceEntityMask } from './types';
|
||||
import type { DeviceEntity } from './types';
|
||||
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
import { updateValue } from 'utils';
|
||||
|
||||
type SettingsCustomizationDialogProps = {
|
||||
interface SettingsCustomizationsDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (di: DeviceEntity) => void;
|
||||
selectedItem: DeviceEntity;
|
||||
};
|
||||
}
|
||||
|
||||
const SettingsCustomizationDialog = ({ open, onClose, onSave, selectedItem }: SettingsCustomizationDialogProps) => {
|
||||
const CustomizationsDialog = ({
|
||||
open,
|
||||
onClose,
|
||||
onSave,
|
||||
selectedItem
|
||||
}: SettingsCustomizationsDialogProps) => {
|
||||
const { LL } = useI18nContext();
|
||||
const [editItem, setEditItem] = useState<DeviceEntity>(selectedItem);
|
||||
const [error, setError] = useState<boolean>(false);
|
||||
@@ -39,7 +43,9 @@ const SettingsCustomizationDialog = ({ open, onClose, onSave, selectedItem }: Se
|
||||
const updateFormValue = updateValue(setEditItem);
|
||||
|
||||
const isWriteableNumber =
|
||||
typeof editItem.v === 'number' && editItem.w && !(editItem.m & DeviceEntityMask.DV_READONLY);
|
||||
typeof editItem.v === 'number' &&
|
||||
editItem.w &&
|
||||
!(editItem.m & DeviceEntityMask.DV_READONLY);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
@@ -48,12 +54,19 @@ const SettingsCustomizationDialog = ({ open, onClose, onSave, selectedItem }: Se
|
||||
}
|
||||
}, [open, selectedItem]);
|
||||
|
||||
const close = () => {
|
||||
onClose();
|
||||
const handleClose = (_event, reason: 'backdropClick' | 'escapeKeyDown') => {
|
||||
if (reason !== 'backdropClick') {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const save = () => {
|
||||
if (isWriteableNumber && editItem.mi && editItem.ma && editItem.mi > editItem?.ma) {
|
||||
if (
|
||||
isWriteableNumber &&
|
||||
editItem.mi &&
|
||||
editItem.ma &&
|
||||
editItem.mi > editItem?.ma
|
||||
) {
|
||||
setError(true);
|
||||
} else {
|
||||
onSave(editItem);
|
||||
@@ -65,12 +78,12 @@ const SettingsCustomizationDialog = ({ open, onClose, onSave, selectedItem }: Se
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog sx={dialogStyle} open={open} onClose={close}>
|
||||
<Dialog sx={dialogStyle} open={open} onClose={handleClose}>
|
||||
<DialogTitle>{LL.EDIT() + ' ' + LL.ENTITY()}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Grid container direction="row">
|
||||
<Grid container>
|
||||
<Typography variant="body2" color="warning.main">
|
||||
{LL.ENTITY() + ' ID'}:
|
||||
{LL.ID_OF(LL.ENTITY())}:
|
||||
</Typography>
|
||||
<Typography variant="body2">{editItem.id}</Typography>
|
||||
</Grid>
|
||||
@@ -98,35 +111,34 @@ const SettingsCustomizationDialog = ({ open, onClose, onSave, selectedItem }: Se
|
||||
<Box mt={1} mb={2}>
|
||||
<EntityMaskToggle onUpdate={updateDeviceEntity} de={editItem} />
|
||||
</Box>
|
||||
<Grid container spacing={1}>
|
||||
<Grid item>
|
||||
<Grid container spacing={2}>
|
||||
<Grid>
|
||||
<TextField
|
||||
name="cn"
|
||||
label={LL.NEW_NAME_OF(LL.ENTITY())}
|
||||
value={editItem.cn}
|
||||
autoFocus
|
||||
sx={{ width: '30ch' }}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
</Grid>
|
||||
{isWriteableNumber && (
|
||||
<>
|
||||
<Grid item>
|
||||
<Grid>
|
||||
<TextField
|
||||
name="mi"
|
||||
label={LL.MIN()}
|
||||
value={editItem.mi}
|
||||
sx={{ width: '8ch' }}
|
||||
value={numberValue(editItem.mi)}
|
||||
sx={{ width: '11ch' }}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Grid>
|
||||
<TextField
|
||||
name="ma"
|
||||
label={LL.MAX()}
|
||||
value={editItem.ma}
|
||||
sx={{ width: '8ch' }}
|
||||
value={numberValue(editItem.ma)}
|
||||
sx={{ width: '11ch' }}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
@@ -141,10 +153,20 @@ const SettingsCustomizationDialog = ({ open, onClose, onSave, selectedItem }: Se
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button startIcon={<CancelIcon />} variant="outlined" onClick={close} color="secondary">
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
onClick={onClose}
|
||||
color="secondary"
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button startIcon={<DoneIcon />} variant="outlined" onClick={save} color="primary">
|
||||
<Button
|
||||
startIcon={<DoneIcon />}
|
||||
variant="outlined"
|
||||
onClick={save}
|
||||
color="primary"
|
||||
>
|
||||
{LL.UPDATE()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
@@ -152,4 +174,4 @@ const SettingsCustomizationDialog = ({ open, onClose, onSave, selectedItem }: Se
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsCustomizationDialog;
|
||||
export default CustomizationsDialog;
|
||||
366
interface/src/app/main/Dashboard.tsx
Normal file
366
interface/src/app/main/Dashboard.tsx
Normal file
@@ -0,0 +1,366 @@
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { IconContext } from 'react-icons/lib';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import UnfoldLessIcon from '@mui/icons-material/UnfoldLess';
|
||||
import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore';
|
||||
import {
|
||||
Box,
|
||||
IconButton,
|
||||
ToggleButton,
|
||||
ToggleButtonGroup,
|
||||
Tooltip,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
import Grid from '@mui/material/Grid2';
|
||||
|
||||
import { Body, Cell, Row, Table } from '@table-library/react-table-library/table';
|
||||
import { useTheme } from '@table-library/react-table-library/theme';
|
||||
import { CellTree, useTree } from '@table-library/react-table-library/tree';
|
||||
import { useRequest } from 'alova/client';
|
||||
import { FormLoader, SectionContent, useLayoutTitle } from 'components';
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { useInterval, usePersistState } from 'utils';
|
||||
|
||||
import { readDashboard, writeDeviceValue } from '../../api/app';
|
||||
import DeviceIcon from './DeviceIcon';
|
||||
import DevicesDialog from './DevicesDialog';
|
||||
import { formatValue } from './deviceValue';
|
||||
import {
|
||||
type DashboardItem,
|
||||
DeviceEntityMask,
|
||||
DeviceType,
|
||||
type DeviceValue
|
||||
} from './types';
|
||||
import { deviceValueItemValidation } from './validators';
|
||||
|
||||
const Dashboard = () => {
|
||||
const { LL } = useI18nContext();
|
||||
const { me } = useContext(AuthenticatedContext);
|
||||
|
||||
useLayoutTitle(LL.DASHBOARD());
|
||||
|
||||
const [showAll, setShowAll] = usePersistState(true, 'showAll');
|
||||
|
||||
const [deviceValueDialogOpen, setDeviceValueDialogOpen] = useState<boolean>(false);
|
||||
const [parentNodes, setParentNodes] = useState<number>(0);
|
||||
const [selectedDashboardItem, setSelectedDashboardItem] =
|
||||
useState<DashboardItem>();
|
||||
|
||||
const {
|
||||
data,
|
||||
send: fetchDashboard,
|
||||
error,
|
||||
loading
|
||||
} = useRequest(readDashboard, {
|
||||
initialData: []
|
||||
}).onSuccess((event) => {
|
||||
if (event.data.length !== parentNodes) {
|
||||
setParentNodes(event.data.length); // count number of parents/devices
|
||||
}
|
||||
});
|
||||
|
||||
const { loading: submitting, send: sendDeviceValue } = useRequest(
|
||||
(data: { id: number; c: string; v: unknown }) => writeDeviceValue(data),
|
||||
{
|
||||
immediate: false
|
||||
}
|
||||
);
|
||||
|
||||
const deviceValueDialogSave = async (devicevalue: DeviceValue) => {
|
||||
if (!selectedDashboardItem) {
|
||||
return;
|
||||
}
|
||||
const id = selectedDashboardItem.parentNode.id; // this is the parent ID
|
||||
await sendDeviceValue({ id, c: devicevalue.c ?? '', v: devicevalue.v })
|
||||
.then(() => {
|
||||
toast.success(LL.WRITE_CMD_SENT());
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
toast.error(error.message);
|
||||
})
|
||||
.finally(() => {
|
||||
setDeviceValueDialogOpen(false);
|
||||
setSelectedDashboardItem(undefined);
|
||||
});
|
||||
};
|
||||
|
||||
const dashboard_theme = useTheme({
|
||||
Table: `
|
||||
--data-table-library_grid-template-columns: minmax(80px, auto) 120px 32px;
|
||||
`,
|
||||
BaseRow: `
|
||||
font-size: 14px;
|
||||
.td {
|
||||
height: 28px;
|
||||
}
|
||||
`,
|
||||
Row: `
|
||||
cursor: pointer;
|
||||
background-color: #1e1e1e;
|
||||
&:nth-of-type(odd) .td {
|
||||
background-color: #303030;
|
||||
},
|
||||
&:hover .td {
|
||||
background-color: #177ac9;
|
||||
}
|
||||
`,
|
||||
BaseCell: `
|
||||
&:nth-of-type(2) {
|
||||
text-align: right;
|
||||
}
|
||||
&:nth-of-type(3) {
|
||||
text-align: right;
|
||||
}
|
||||
`
|
||||
});
|
||||
|
||||
const tree = useTree(
|
||||
{ nodes: data },
|
||||
{
|
||||
onChange: undefined // not used but needed
|
||||
},
|
||||
{
|
||||
treeIcon: {
|
||||
margin: '4px',
|
||||
iconDefault: null,
|
||||
iconRight: (
|
||||
<ChevronRightIcon
|
||||
sx={{ fontSize: 16, verticalAlign: 'middle' }}
|
||||
color="info"
|
||||
/>
|
||||
),
|
||||
iconDown: (
|
||||
<ExpandMoreIcon
|
||||
sx={{ fontSize: 16, verticalAlign: 'middle' }}
|
||||
color="info"
|
||||
/>
|
||||
)
|
||||
},
|
||||
indentation: 45
|
||||
}
|
||||
);
|
||||
|
||||
useInterval(() => {
|
||||
if (!deviceValueDialogOpen) {
|
||||
void fetchDashboard();
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
useEffect(() => {
|
||||
showAll
|
||||
? tree.fns.onAddAll(data.map((item: DashboardItem) => item.id)) // expand tree
|
||||
: tree.fns.onRemoveAll(); // collapse tree
|
||||
}, [parentNodes]);
|
||||
|
||||
const showType = (n?: string, t?: number) => {
|
||||
// if we have a name show it
|
||||
if (n) {
|
||||
return n;
|
||||
}
|
||||
if (t) {
|
||||
// otherwise pick translation based on type
|
||||
switch (t) {
|
||||
case DeviceType.CUSTOM:
|
||||
return LL.CUSTOM_ENTITIES(0);
|
||||
case DeviceType.ANALOGSENSOR:
|
||||
return LL.ANALOG_SENSORS();
|
||||
case DeviceType.TEMPERATURESENSOR:
|
||||
return LL.TEMP_SENSORS();
|
||||
case DeviceType.SCHEDULER:
|
||||
return LL.SCHEDULER();
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const showName = (di: DashboardItem) => {
|
||||
if (di.id < 100) {
|
||||
// if its a device (parent node) and has entities
|
||||
if (di.nodes?.length) {
|
||||
return (
|
||||
<>
|
||||
<span style="font-size: 14px">
|
||||
<DeviceIcon type_id={di.t ?? 0} />
|
||||
{showType(di.n, di.t)}
|
||||
</span>
|
||||
<span style={{ color: 'lightblue' }}> ({di.nodes?.length})</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
if (di.dv) {
|
||||
return <span style="color:lightgrey">{di.dv.id.slice(2)}</span>;
|
||||
}
|
||||
};
|
||||
|
||||
const hasMask = (id: string, mask: number) =>
|
||||
(parseInt(id.slice(0, 2), 16) & mask) === mask;
|
||||
|
||||
const editDashboardValue = (di: DashboardItem) => {
|
||||
if (me.admin && di.dv?.c) {
|
||||
setSelectedDashboardItem(di);
|
||||
setDeviceValueDialogOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleShowAll = (
|
||||
event: React.MouseEvent<HTMLElement>,
|
||||
toggle: boolean | null
|
||||
) => {
|
||||
if (toggle !== null) {
|
||||
tree.fns.onToggleAll({});
|
||||
setShowAll(toggle);
|
||||
}
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={fetchDashboard} errorMessage={error?.message} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: 'black',
|
||||
pt: 1,
|
||||
pl: 2
|
||||
}}
|
||||
>
|
||||
<Grid container spacing={0} justifyContent="flex-start">
|
||||
<Grid size={11}>
|
||||
<Typography mb={2} variant="body1" color="warning">
|
||||
{LL.DASHBOARD_1()}.
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid size={1} alignItems="end">
|
||||
<ToggleButtonGroup
|
||||
color="primary"
|
||||
size="small"
|
||||
value={showAll}
|
||||
exclusive
|
||||
onChange={handleShowAll}
|
||||
>
|
||||
<ToggleButton value={true}>
|
||||
<UnfoldMoreIcon sx={{ fontSize: 14 }} />
|
||||
</ToggleButton>
|
||||
<ToggleButton value={false}>
|
||||
<UnfoldLessIcon sx={{ fontSize: 14 }} />
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
padding={1}
|
||||
justifyContent="center"
|
||||
flexDirection="column"
|
||||
sx={{
|
||||
borderRadius: 1,
|
||||
border: '1px solid grey'
|
||||
}}
|
||||
>
|
||||
<IconContext.Provider
|
||||
value={{
|
||||
color: 'lightblue',
|
||||
size: '16',
|
||||
style: { verticalAlign: 'middle' }
|
||||
}}
|
||||
>
|
||||
{!loading && data.length === 0 ? (
|
||||
<Typography variant="subtitle2" color="secondary">
|
||||
{LL.NO_DATA()}
|
||||
</Typography>
|
||||
) : (
|
||||
<Table
|
||||
data={{ nodes: data }}
|
||||
theme={dashboard_theme}
|
||||
layout={{ custom: true }}
|
||||
tree={tree}
|
||||
>
|
||||
{(tableList: DashboardItem[]) => (
|
||||
<Body>
|
||||
{tableList.map((di: DashboardItem) => (
|
||||
<Row
|
||||
key={di.id}
|
||||
item={di}
|
||||
onClick={() => editDashboardValue(di)}
|
||||
>
|
||||
{di.id > 99 ? (
|
||||
<>
|
||||
<Cell>{showName(di)}</Cell>
|
||||
<Cell>
|
||||
<Tooltip
|
||||
placement="left"
|
||||
title={formatValue(LL, di.dv?.v, di.dv?.u)}
|
||||
arrow
|
||||
>
|
||||
<span style={{ color: 'lightgrey' }}>
|
||||
{formatValue(LL, di.dv?.v, di.dv?.u)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Cell>
|
||||
|
||||
<Cell>
|
||||
{me.admin &&
|
||||
di.dv?.c &&
|
||||
!hasMask(di.dv.id, DeviceEntityMask.DV_READONLY) && (
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => editDashboardValue(di)}
|
||||
>
|
||||
<EditIcon
|
||||
color="primary"
|
||||
sx={{ fontSize: 16 }}
|
||||
/>
|
||||
</IconButton>
|
||||
)}
|
||||
</Cell>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CellTree item={di}>{showName(di)}</CellTree>
|
||||
<Cell />
|
||||
<Cell />
|
||||
</>
|
||||
)}
|
||||
</Row>
|
||||
))}
|
||||
</Body>
|
||||
)}
|
||||
</Table>
|
||||
)}
|
||||
</IconContext.Provider>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent>
|
||||
{renderContent()}
|
||||
{selectedDashboardItem && selectedDashboardItem.dv && (
|
||||
<DevicesDialog
|
||||
open={deviceValueDialogOpen}
|
||||
onClose={() => setDeviceValueDialogOpen(false)}
|
||||
onSave={deviceValueDialogSave}
|
||||
selectedItem={selectedDashboardItem.dv}
|
||||
writeable={true}
|
||||
validator={deviceValueItemValidation(selectedDashboardItem.dv)}
|
||||
progress={submitting}
|
||||
/>
|
||||
)}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
53
interface/src/app/main/DeviceIcon.tsx
Normal file
53
interface/src/app/main/DeviceIcon.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { AiOutlineAlert, AiOutlineControl, AiOutlineGateway } from 'react-icons/ai';
|
||||
import { CgSmartHomeBoiler } from 'react-icons/cg';
|
||||
import { FaSolarPanel } from 'react-icons/fa';
|
||||
import { GiHeatHaze, GiTap } from 'react-icons/gi';
|
||||
import { MdPlaylistAdd } from 'react-icons/md';
|
||||
import { MdMoreTime } from 'react-icons/md';
|
||||
import {
|
||||
MdOutlineDevices,
|
||||
MdOutlinePool,
|
||||
MdOutlineSensors,
|
||||
MdThermostatAuto
|
||||
} from 'react-icons/md';
|
||||
import { PiFan, PiGauge } from 'react-icons/pi';
|
||||
import { TiFlowSwitch, TiThermometer } from 'react-icons/ti';
|
||||
import { VscVmConnect } from 'react-icons/vsc';
|
||||
|
||||
import type { SvgIconProps } from '@mui/material';
|
||||
|
||||
import { DeviceType } from './types';
|
||||
|
||||
const deviceIconLookup: {
|
||||
[key in DeviceType]: React.ComponentType<SvgIconProps> | undefined;
|
||||
} = {
|
||||
[DeviceType.TEMPERATURESENSOR]: TiThermometer,
|
||||
[DeviceType.ANALOGSENSOR]: PiGauge,
|
||||
[DeviceType.BOILER]: CgSmartHomeBoiler,
|
||||
[DeviceType.HEATSOURCE]: CgSmartHomeBoiler,
|
||||
[DeviceType.THERMOSTAT]: MdThermostatAuto,
|
||||
[DeviceType.MIXER]: AiOutlineControl,
|
||||
[DeviceType.SOLAR]: FaSolarPanel,
|
||||
[DeviceType.HEATPUMP]: GiHeatHaze,
|
||||
[DeviceType.GATEWAY]: AiOutlineGateway,
|
||||
[DeviceType.SWITCH]: TiFlowSwitch,
|
||||
[DeviceType.CONTROLLER]: VscVmConnect,
|
||||
[DeviceType.CONNECT]: VscVmConnect,
|
||||
[DeviceType.ALERT]: AiOutlineAlert,
|
||||
[DeviceType.EXTENSION]: MdOutlineDevices,
|
||||
[DeviceType.WATER]: GiTap,
|
||||
[DeviceType.POOL]: MdOutlinePool,
|
||||
[DeviceType.CUSTOM]: MdPlaylistAdd,
|
||||
[DeviceType.UNKNOWN]: MdOutlineSensors,
|
||||
[DeviceType.SYSTEM]: undefined,
|
||||
[DeviceType.SCHEDULER]: MdMoreTime,
|
||||
[DeviceType.GENERIC]: MdOutlineSensors,
|
||||
[DeviceType.VENTILATION]: PiFan
|
||||
};
|
||||
|
||||
const DeviceIcon = ({ type_id }: { type_id: DeviceType }) => {
|
||||
const Icon = deviceIconLookup[type_id];
|
||||
return Icon ? <Icon /> : null;
|
||||
};
|
||||
|
||||
export default DeviceIcon;
|
||||
769
interface/src/app/main/Devices.tsx
Normal file
769
interface/src/app/main/Devices.tsx
Normal file
@@ -0,0 +1,769 @@
|
||||
import {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useState
|
||||
} from 'react';
|
||||
import { IconContext } from 'react-icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import CommentsDisabledOutlinedIcon from '@mui/icons-material/CommentsDisabledOutlined';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import EditOffOutlinedIcon from '@mui/icons-material/EditOffOutlined';
|
||||
import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered';
|
||||
import DownloadIcon from '@mui/icons-material/GetApp';
|
||||
import HighlightOffIcon from '@mui/icons-material/HighlightOff';
|
||||
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
||||
import KeyboardArrowDownOutlinedIcon from '@mui/icons-material/KeyboardArrowDownOutlined';
|
||||
import KeyboardArrowUpOutlinedIcon from '@mui/icons-material/KeyboardArrowUpOutlined';
|
||||
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
||||
import StarIcon from '@mui/icons-material/Star';
|
||||
import StarBorderOutlinedIcon from '@mui/icons-material/StarBorderOutlined';
|
||||
import UnfoldMoreOutlinedIcon from '@mui/icons-material/UnfoldMoreOutlined';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
CircularProgress,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
IconButton,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Tooltip,
|
||||
type TooltipProps,
|
||||
Typography,
|
||||
styled,
|
||||
tooltipClasses
|
||||
} from '@mui/material';
|
||||
import Grid from '@mui/material/Grid2';
|
||||
|
||||
import { useRowSelect } from '@table-library/react-table-library/select';
|
||||
import { SortToggleType, useSort } from '@table-library/react-table-library/sort';
|
||||
import {
|
||||
Body,
|
||||
Cell,
|
||||
Header,
|
||||
HeaderCell,
|
||||
HeaderRow,
|
||||
Row,
|
||||
Table
|
||||
} from '@table-library/react-table-library/table';
|
||||
import { useTheme } from '@table-library/react-table-library/theme';
|
||||
import type { Action, State } from '@table-library/react-table-library/types/common';
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import { useRequest } from 'alova/client';
|
||||
import { MessageBox, SectionContent, useLayoutTitle } from 'components';
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { useInterval } from 'utils';
|
||||
|
||||
import { readCoreData, readDeviceData, writeDeviceValue } from '../../api/app';
|
||||
import DeviceIcon from './DeviceIcon';
|
||||
import DevicesDialog from './DevicesDialog';
|
||||
import { formatValue } from './deviceValue';
|
||||
import { DeviceEntityMask, DeviceType, DeviceValueUOM_s } from './types';
|
||||
import type { Device, DeviceValue } from './types';
|
||||
import { deviceValueItemValidation } from './validators';
|
||||
|
||||
const Devices = () => {
|
||||
const { LL } = useI18nContext();
|
||||
const { me } = useContext(AuthenticatedContext);
|
||||
|
||||
const [size, setSize] = useState([0, 0]);
|
||||
const [selectedDeviceValue, setSelectedDeviceValue] = useState<DeviceValue>();
|
||||
const [onlyFav, setOnlyFav] = useState(false);
|
||||
const [deviceValueDialogOpen, setDeviceValueDialogOpen] = useState(false);
|
||||
const [showDeviceInfo, setShowDeviceInfo] = useState(false);
|
||||
const [selectedDevice, setSelectedDevice] = useState<number>();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
useLayoutTitle(LL.DEVICES());
|
||||
|
||||
const { data: coreData, send: sendCoreData } = useRequest(() => readCoreData(), {
|
||||
initialData: {
|
||||
connected: true,
|
||||
devices: []
|
||||
}
|
||||
});
|
||||
|
||||
const { data: deviceData, send: sendDeviceData } = useRequest(
|
||||
(id: number) => readDeviceData(id),
|
||||
{
|
||||
initialData: {
|
||||
nodes: []
|
||||
},
|
||||
immediate: false
|
||||
}
|
||||
);
|
||||
|
||||
const { loading: submitting, send: sendDeviceValue } = useRequest(
|
||||
(data: { id: number; c: string; v: unknown }) => writeDeviceValue(data),
|
||||
{
|
||||
immediate: false
|
||||
}
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
function updateSize() {
|
||||
setSize([window.innerWidth, window.innerHeight]);
|
||||
}
|
||||
window.addEventListener('resize', updateSize);
|
||||
updateSize();
|
||||
return () => window.removeEventListener('resize', updateSize);
|
||||
}, []);
|
||||
|
||||
const leftOffset = () => {
|
||||
const devicesWindow = document.getElementById('devices-window');
|
||||
if (!devicesWindow) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const clientRect = devicesWindow.getBoundingClientRect();
|
||||
const left = clientRect.left;
|
||||
const right = clientRect.right;
|
||||
|
||||
if (!left || !right) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return left + (right - left < 400 ? 0 : 200);
|
||||
};
|
||||
|
||||
const common_theme = useTheme({
|
||||
BaseRow: `
|
||||
font-size: 14px;
|
||||
`,
|
||||
HeaderRow: `
|
||||
text-transform: uppercase;
|
||||
background-color: black;
|
||||
color: #90CAF9;
|
||||
.th {
|
||||
border-bottom: 1px solid #565656;
|
||||
}
|
||||
`,
|
||||
Row: `
|
||||
cursor: pointer;
|
||||
background-color: #1E1E1E;
|
||||
.td {
|
||||
padding: 8px;
|
||||
}
|
||||
&.tr.tr-body.row-select.row-select-single-selected {
|
||||
background-color: #177ac9;
|
||||
font-weight: normal;
|
||||
}
|
||||
`
|
||||
});
|
||||
|
||||
const device_theme = useTheme([
|
||||
common_theme,
|
||||
{
|
||||
Table: `
|
||||
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) 130px;
|
||||
`,
|
||||
HeaderRow: `
|
||||
.th {
|
||||
padding: 8px;
|
||||
height: 36px;
|
||||
`,
|
||||
Row: `
|
||||
&:hover .td {
|
||||
background-color: #177ac9;
|
||||
`
|
||||
}
|
||||
]);
|
||||
|
||||
const data_theme = useTheme([
|
||||
common_theme,
|
||||
{
|
||||
Table: `
|
||||
--data-table-library_grid-template-columns: minmax(200px, auto) minmax(150px, auto) 40px;
|
||||
height: auto;
|
||||
max-height: 100%;
|
||||
overflow-y: scroll;
|
||||
::-webkit-scrollbar {
|
||||
display:none;
|
||||
}
|
||||
`,
|
||||
BaseRow: `
|
||||
.td {
|
||||
height: 32px;
|
||||
}
|
||||
`,
|
||||
BaseCell: `
|
||||
&:nth-of-type(1) {
|
||||
border-left: 1px solid #177ac9;
|
||||
},
|
||||
&:nth-of-type(2) {
|
||||
text-align: right;
|
||||
},
|
||||
&:nth-of-type(3) {
|
||||
border-right: 1px solid #177ac9;
|
||||
}
|
||||
`,
|
||||
HeaderRow: `
|
||||
.th {
|
||||
border-top: 1px solid #565656;
|
||||
}
|
||||
`,
|
||||
Row: `
|
||||
&:nth-of-type(odd) .td {
|
||||
background-color: #303030;
|
||||
},
|
||||
&:hover .td {
|
||||
background-color: #177ac9;
|
||||
}
|
||||
`
|
||||
}
|
||||
]);
|
||||
|
||||
const ButtonTooltip = styled(({ className, ...props }: TooltipProps) => (
|
||||
<Tooltip {...props} arrow classes={{ popper: className }} />
|
||||
))(({ theme }) => ({
|
||||
[`& .${tooltipClasses.arrow}`]: {
|
||||
color: theme.palette.success.main
|
||||
},
|
||||
[`& .${tooltipClasses.tooltip}`]: {
|
||||
backgroundColor: theme.palette.success.main,
|
||||
color: 'rgba(0, 0, 0, 0.87)',
|
||||
boxShadow: theme.shadows[1],
|
||||
fontSize: 10
|
||||
}
|
||||
}));
|
||||
|
||||
const getSortIcon = (state: State, sortKey: unknown) => {
|
||||
if (state.sortKey === sortKey && state.reverse) {
|
||||
return <KeyboardArrowDownOutlinedIcon />;
|
||||
}
|
||||
if (state.sortKey === sortKey && !state.reverse) {
|
||||
return <KeyboardArrowUpOutlinedIcon />;
|
||||
}
|
||||
return <UnfoldMoreOutlinedIcon />;
|
||||
};
|
||||
|
||||
const dv_sort = useSort(
|
||||
{ nodes: deviceData.nodes },
|
||||
{},
|
||||
{
|
||||
sortIcon: {
|
||||
iconDefault: <UnfoldMoreOutlinedIcon />,
|
||||
iconUp: <KeyboardArrowUpOutlinedIcon />,
|
||||
iconDown: <KeyboardArrowDownOutlinedIcon />
|
||||
},
|
||||
sortToggleType: SortToggleType.AlternateWithReset,
|
||||
sortFns: {
|
||||
NAME: (array) =>
|
||||
array.sort((a, b) =>
|
||||
a.id.toString().slice(2).localeCompare(b.id.toString().slice(2))
|
||||
),
|
||||
|
||||
VALUE: (array) =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
array.sort((a, b) => a.v.toString().localeCompare(b.v.toString()))
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
async function onSelectChange(action: Action, state: State) {
|
||||
setSelectedDevice(state.id as number);
|
||||
if (action.type === 'ADD_BY_ID_EXCLUSIVELY') {
|
||||
await sendDeviceData(state.id as number);
|
||||
}
|
||||
}
|
||||
|
||||
const device_select = useRowSelect(
|
||||
{ nodes: coreData.devices },
|
||||
{
|
||||
onChange: onSelectChange
|
||||
}
|
||||
);
|
||||
|
||||
const resetDeviceSelect = () => {
|
||||
device_select.fns.onRemoveAll();
|
||||
};
|
||||
|
||||
const escFunction = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
if (device_select) {
|
||||
device_select.fns.onRemoveAll();
|
||||
}
|
||||
}
|
||||
},
|
||||
[device_select]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', escFunction);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', escFunction);
|
||||
};
|
||||
}, [escFunction]);
|
||||
|
||||
const customize = () => {
|
||||
if (selectedDevice === 99) {
|
||||
navigate('/customentities');
|
||||
} else {
|
||||
navigate('/customizations', { state: selectedDevice });
|
||||
}
|
||||
};
|
||||
|
||||
const escapeCsvCell = (cell: string) => {
|
||||
if (cell == null) {
|
||||
return '';
|
||||
}
|
||||
const sc = cell.toString().trim();
|
||||
if (sc === '' || sc === '""') {
|
||||
return sc;
|
||||
}
|
||||
if (
|
||||
sc.includes('"') ||
|
||||
sc.includes(';') ||
|
||||
sc.includes('\n') ||
|
||||
sc.includes('\r')
|
||||
) {
|
||||
return '"' + sc.replace(/"/g, '""') + '"';
|
||||
}
|
||||
return sc;
|
||||
};
|
||||
|
||||
const hasMask = (id: string, mask: number) =>
|
||||
(parseInt(id.slice(0, 2), 16) & mask) === mask;
|
||||
|
||||
const handleDownloadCsv = () => {
|
||||
const deviceIndex = coreData.devices.findIndex(
|
||||
(d) => d.id === device_select.state.id
|
||||
);
|
||||
if (deviceIndex === -1) {
|
||||
return;
|
||||
}
|
||||
const filename =
|
||||
coreData.devices[deviceIndex].tn + '_' + coreData.devices[deviceIndex].n;
|
||||
|
||||
const columns = [
|
||||
{
|
||||
accessor: (dv: DeviceValue) => dv.id.slice(2),
|
||||
name: LL.ENTITY_NAME(0)
|
||||
},
|
||||
{
|
||||
accessor: (dv: DeviceValue) =>
|
||||
typeof dv.v === 'number' ? new Intl.NumberFormat().format(dv.v) : dv.v,
|
||||
name: LL.VALUE(0)
|
||||
},
|
||||
{
|
||||
accessor: (dv: DeviceValue) =>
|
||||
dv.u !== undefined && DeviceValueUOM_s[dv.u]
|
||||
? DeviceValueUOM_s[dv.u].replace(/[^a-zA-Z0-9]/g, '')
|
||||
: '',
|
||||
name: 'UoM'
|
||||
},
|
||||
{
|
||||
accessor: (dv: DeviceValue) =>
|
||||
dv.c && !hasMask(dv.id, DeviceEntityMask.DV_READONLY) ? 'yes' : 'no',
|
||||
name: LL.WRITEABLE()
|
||||
},
|
||||
{
|
||||
accessor: (dv: DeviceValue) =>
|
||||
dv.h
|
||||
? dv.h
|
||||
: dv.l
|
||||
? dv.l.join(' | ')
|
||||
: dv.m !== undefined && dv.x !== undefined
|
||||
? dv.m + ', ' + dv.x
|
||||
: '',
|
||||
name: 'Range'
|
||||
}
|
||||
];
|
||||
|
||||
const data = onlyFav
|
||||
? deviceData.nodes.filter((dv) => hasMask(dv.id, DeviceEntityMask.DV_FAVORITE))
|
||||
: deviceData.nodes;
|
||||
|
||||
const csvData = data.reduce(
|
||||
(csvString: string, rowItem: DeviceValue) =>
|
||||
csvString +
|
||||
columns
|
||||
.map(({ accessor }: { accessor: (dv: DeviceValue) => unknown }) =>
|
||||
escapeCsvCell(accessor(rowItem) as string)
|
||||
)
|
||||
.join(';') +
|
||||
'\r\n',
|
||||
columns.map(({ name }: { name: string }) => escapeCsvCell(name)).join(';') +
|
||||
'\r\n'
|
||||
);
|
||||
|
||||
const downloadBlob = (blob: Blob) => {
|
||||
const downloadLink = document.createElement('a');
|
||||
downloadLink.download = filename;
|
||||
downloadLink.href = window.URL.createObjectURL(blob);
|
||||
document.body.appendChild(downloadLink);
|
||||
downloadLink.click();
|
||||
document.body.removeChild(downloadLink);
|
||||
};
|
||||
|
||||
const device = { ...{ device: coreData.devices[deviceIndex] }, ...deviceData };
|
||||
downloadBlob(
|
||||
new Blob([JSON.stringify(device, null, 2)], {
|
||||
type: 'text;charset:utf-8'
|
||||
})
|
||||
);
|
||||
|
||||
downloadBlob(new Blob([csvData], { type: 'text/csv;charset:utf-8' }));
|
||||
};
|
||||
|
||||
useInterval(() => {
|
||||
if (!deviceValueDialogOpen) {
|
||||
selectedDevice ? void sendDeviceData(selectedDevice) : void sendCoreData();
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
const deviceValueDialogSave = async (devicevalue: DeviceValue) => {
|
||||
const id = Number(device_select.state.id);
|
||||
await sendDeviceValue({ id, c: devicevalue.c ?? '', v: devicevalue.v })
|
||||
.then(() => {
|
||||
toast.success(LL.WRITE_CMD_SENT());
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
toast.error(error.message);
|
||||
})
|
||||
.finally(async () => {
|
||||
setDeviceValueDialogOpen(false);
|
||||
await sendDeviceData(id);
|
||||
setSelectedDeviceValue(undefined);
|
||||
});
|
||||
};
|
||||
|
||||
const renderDeviceDetails = () => {
|
||||
if (showDeviceInfo) {
|
||||
const deviceIndex = coreData.devices.findIndex(
|
||||
(d) => d.id === device_select.state.id
|
||||
);
|
||||
if (deviceIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
sx={dialogStyle}
|
||||
open={showDeviceInfo}
|
||||
onClose={() => setShowDeviceInfo(false)}
|
||||
>
|
||||
<DialogTitle>{LL.DEVICE_DETAILS()}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<List dense={true}>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={LL.TYPE(0)}
|
||||
secondary={coreData.devices[deviceIndex].tn}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={LL.NAME(0)}
|
||||
secondary={coreData.devices[deviceIndex].n}
|
||||
/>
|
||||
</ListItem>
|
||||
{coreData.devices[deviceIndex].t !== DeviceType.CUSTOM && (
|
||||
<>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={LL.BRAND()}
|
||||
secondary={coreData.devices[deviceIndex].b}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={LL.ID_OF(LL.DEVICE())}
|
||||
secondary={
|
||||
'0x' +
|
||||
(
|
||||
'00' +
|
||||
coreData.devices[deviceIndex].d.toString(16).toUpperCase()
|
||||
).slice(-2)
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={LL.ID_OF(LL.PRODUCT())}
|
||||
secondary={coreData.devices[deviceIndex].p}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={LL.VERSION()}
|
||||
secondary={coreData.devices[deviceIndex].v}
|
||||
/>
|
||||
</ListItem>
|
||||
</>
|
||||
)}
|
||||
</List>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => setShowDeviceInfo(false)}
|
||||
color="secondary"
|
||||
>
|
||||
{LL.CLOSE()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const renderCoreData = () => (
|
||||
<>
|
||||
<IconContext.Provider
|
||||
value={{
|
||||
color: 'lightblue',
|
||||
size: '18',
|
||||
style: { verticalAlign: 'middle' }
|
||||
}}
|
||||
>
|
||||
{!coreData.connected && (
|
||||
<MessageBox my={2} level="error" message={LL.EMS_BUS_WARNING()} />
|
||||
)}
|
||||
|
||||
{coreData.connected && (
|
||||
<Table
|
||||
data={{ nodes: coreData.devices }}
|
||||
select={device_select}
|
||||
theme={device_theme}
|
||||
layout={{ custom: true }}
|
||||
>
|
||||
{(tableList: Device[]) => (
|
||||
<>
|
||||
<Header>
|
||||
<HeaderRow>
|
||||
<HeaderCell resize>{LL.DESCRIPTION()}</HeaderCell>
|
||||
<HeaderCell stiff>{LL.TYPE(0)}</HeaderCell>
|
||||
</HeaderRow>
|
||||
</Header>
|
||||
<Body>
|
||||
{tableList.length === 0 && (
|
||||
<CircularProgress sx={{ margin: 1 }} size={18} />
|
||||
)}
|
||||
{tableList.map((device: Device) => (
|
||||
<Row key={device.id} item={device}>
|
||||
<Cell>
|
||||
<DeviceIcon type_id={device.t} />
|
||||
|
||||
{device.n}
|
||||
<span style={{ color: 'lightblue' }}>
|
||||
({device.e})
|
||||
</span>
|
||||
</Cell>
|
||||
<Cell stiff>{device.tn}</Cell>
|
||||
</Row>
|
||||
))}
|
||||
</Body>
|
||||
</>
|
||||
)}
|
||||
</Table>
|
||||
)}
|
||||
</IconContext.Provider>
|
||||
</>
|
||||
);
|
||||
|
||||
const deviceValueDialogClose = () => {
|
||||
setDeviceValueDialogOpen(false);
|
||||
void sendDeviceData(selectedDevice);
|
||||
};
|
||||
|
||||
const renderDeviceData = () => {
|
||||
if (!selectedDevice) {
|
||||
return;
|
||||
}
|
||||
|
||||
const showDeviceValue = (dv: DeviceValue) => {
|
||||
setSelectedDeviceValue(dv);
|
||||
setDeviceValueDialogOpen(true);
|
||||
};
|
||||
|
||||
const renderNameCell = (dv: DeviceValue) => (
|
||||
<>
|
||||
{dv.id.slice(2)}
|
||||
{hasMask(dv.id, DeviceEntityMask.DV_FAVORITE) && (
|
||||
<StarIcon color="primary" sx={{ fontSize: 12 }} />
|
||||
)}
|
||||
{hasMask(dv.id, DeviceEntityMask.DV_READONLY) && (
|
||||
<EditOffOutlinedIcon color="primary" sx={{ fontSize: 12 }} />
|
||||
)}
|
||||
{hasMask(dv.id, DeviceEntityMask.DV_API_MQTT_EXCLUDE) && (
|
||||
<CommentsDisabledOutlinedIcon color="primary" sx={{ fontSize: 12 }} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
const shown_data = onlyFav
|
||||
? deviceData.nodes.filter((dv) => hasMask(dv.id, DeviceEntityMask.DV_FAVORITE))
|
||||
: deviceData.nodes;
|
||||
|
||||
const deviceIndex = coreData.devices.findIndex(
|
||||
(d) => d.id === device_select.state.id
|
||||
);
|
||||
if (deviceIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: 'black',
|
||||
position: 'absolute',
|
||||
left: () => leftOffset(),
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
top: 64,
|
||||
zIndex: 'modal',
|
||||
maxHeight: () => size[1] - 126,
|
||||
border: '1px solid #177ac9'
|
||||
}}
|
||||
>
|
||||
<Box sx={{ border: '1px solid #177ac9' }}>
|
||||
<Typography noWrap variant="subtitle1" color="warning.main" sx={{ ml: 1 }}>
|
||||
{coreData.devices[deviceIndex].n} (
|
||||
{coreData.devices[deviceIndex].tn})
|
||||
</Typography>
|
||||
|
||||
<Grid container justifyContent="space-between">
|
||||
<Typography sx={{ ml: 1 }} variant="subtitle2" color="grey">
|
||||
{LL.SHOWING() +
|
||||
' ' +
|
||||
shown_data.length +
|
||||
'/' +
|
||||
coreData.devices[deviceIndex].e +
|
||||
' ' +
|
||||
LL.ENTITIES(shown_data.length)}
|
||||
<ButtonTooltip title="Info">
|
||||
<IconButton onClick={() => setShowDeviceInfo(true)}>
|
||||
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18 }} />
|
||||
</IconButton>
|
||||
</ButtonTooltip>
|
||||
{me.admin && (
|
||||
<ButtonTooltip title={LL.CUSTOMIZATIONS()}>
|
||||
<IconButton onClick={customize}>
|
||||
<FormatListNumberedIcon color="primary" sx={{ fontSize: 18 }} />
|
||||
</IconButton>
|
||||
</ButtonTooltip>
|
||||
)}
|
||||
<ButtonTooltip title={LL.EXPORT()}>
|
||||
<IconButton onClick={handleDownloadCsv}>
|
||||
<DownloadIcon color="primary" sx={{ fontSize: 18 }} />
|
||||
</IconButton>
|
||||
</ButtonTooltip>
|
||||
<ButtonTooltip title={LL.FAVORITES()}>
|
||||
<IconButton onClick={() => setOnlyFav(!onlyFav)}>
|
||||
{onlyFav ? (
|
||||
<StarIcon color="primary" sx={{ fontSize: 18 }} />
|
||||
) : (
|
||||
<StarBorderOutlinedIcon color="primary" sx={{ fontSize: 18 }} />
|
||||
)}
|
||||
</IconButton>
|
||||
</ButtonTooltip>
|
||||
</Typography>
|
||||
<Grid justifyContent="flex-end">
|
||||
<ButtonTooltip title={LL.CANCEL()}>
|
||||
<IconButton onClick={resetDeviceSelect}>
|
||||
<HighlightOffIcon color="primary" sx={{ fontSize: 18 }} />
|
||||
</IconButton>
|
||||
</ButtonTooltip>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
<Table
|
||||
data={{ nodes: shown_data }}
|
||||
theme={data_theme}
|
||||
sort={dv_sort}
|
||||
layout={{ custom: true, fixedHeader: true }}
|
||||
>
|
||||
{(tableList: DeviceValue[]) => (
|
||||
<>
|
||||
<Header>
|
||||
<HeaderRow>
|
||||
<HeaderCell resize>
|
||||
<Button
|
||||
fullWidth
|
||||
style={{ fontSize: '14px', justifyContent: 'flex-start' }}
|
||||
endIcon={getSortIcon(dv_sort.state, 'NAME')}
|
||||
onClick={() => dv_sort.fns.onToggleSort({ sortKey: 'NAME' })}
|
||||
>
|
||||
{LL.ENTITY_NAME(0)}
|
||||
</Button>
|
||||
</HeaderCell>
|
||||
<HeaderCell resize>
|
||||
<Button
|
||||
fullWidth
|
||||
style={{ fontSize: '14px', justifyContent: 'flex-end' }}
|
||||
endIcon={getSortIcon(dv_sort.state, 'VALUE')}
|
||||
onClick={() => dv_sort.fns.onToggleSort({ sortKey: 'VALUE' })}
|
||||
>
|
||||
{LL.VALUE(0)}
|
||||
</Button>
|
||||
</HeaderCell>
|
||||
<HeaderCell stiff />
|
||||
</HeaderRow>
|
||||
</Header>
|
||||
<Body>
|
||||
{tableList.map((dv: DeviceValue) => (
|
||||
<Row key={dv.id} item={dv} onClick={() => showDeviceValue(dv)}>
|
||||
<Cell>{renderNameCell(dv)}</Cell>
|
||||
<Cell>{formatValue(LL, dv.v, dv.u)}</Cell>
|
||||
<Cell stiff>
|
||||
{me.admin &&
|
||||
dv.c &&
|
||||
!hasMask(dv.id, DeviceEntityMask.DV_READONLY) && (
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => showDeviceValue(dv)}
|
||||
>
|
||||
{dv.v === '' ? (
|
||||
<PlayArrowIcon color="primary" sx={{ fontSize: 16 }} />
|
||||
) : (
|
||||
<EditIcon color="primary" sx={{ fontSize: 16 }} />
|
||||
)}
|
||||
</IconButton>
|
||||
)}
|
||||
</Cell>
|
||||
</Row>
|
||||
))}
|
||||
</Body>
|
||||
</>
|
||||
)}
|
||||
</Table>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent id="devices-window">
|
||||
{renderCoreData()}
|
||||
{renderDeviceData()}
|
||||
{renderDeviceDetails()}
|
||||
{selectedDeviceValue && (
|
||||
<DevicesDialog
|
||||
open={deviceValueDialogOpen}
|
||||
onClose={deviceValueDialogClose}
|
||||
onSave={deviceValueDialogSave}
|
||||
selectedItem={selectedDeviceValue}
|
||||
writeable={
|
||||
selectedDeviceValue.c !== undefined &&
|
||||
!hasMask(selectedDeviceValue.id, DeviceEntityMask.DV_READONLY)
|
||||
}
|
||||
validator={deviceValueItemValidation(selectedDeviceValue)}
|
||||
progress={submitting}
|
||||
/>
|
||||
)}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default Devices;
|
||||
@@ -1,36 +1,35 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
CircularProgress,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
FormHelperText,
|
||||
InputAdornment,
|
||||
MenuItem,
|
||||
TextField,
|
||||
FormHelperText,
|
||||
Grid,
|
||||
Box,
|
||||
Typography,
|
||||
CircularProgress
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
import { useState, useEffect } from 'react';
|
||||
import Grid from '@mui/material/Grid2';
|
||||
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import type Schema from 'async-validator';
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import { ValidatedTextField } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { numberValue, updateValue } from 'utils';
|
||||
import { validate } from 'validators';
|
||||
|
||||
import { DeviceValueUOM, DeviceValueUOM_s } from './types';
|
||||
import type { DeviceValue } from './types';
|
||||
import type Schema from 'async-validator';
|
||||
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import { ValidatedTextField } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { updateValue } from 'utils';
|
||||
|
||||
import { validate } from 'validators';
|
||||
|
||||
type DashboardDevicesDialogProps = {
|
||||
interface DevicesDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (as: DeviceValue) => void;
|
||||
@@ -38,9 +37,9 @@ type DashboardDevicesDialogProps = {
|
||||
writeable: boolean;
|
||||
validator: Schema;
|
||||
progress: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
const DashboardDevicesDialog = ({
|
||||
const DevicesDialog = ({
|
||||
open,
|
||||
onClose,
|
||||
onSave,
|
||||
@@ -48,7 +47,7 @@ const DashboardDevicesDialog = ({
|
||||
writeable,
|
||||
validator,
|
||||
progress
|
||||
}: DashboardDevicesDialogProps) => {
|
||||
}: DevicesDialogProps) => {
|
||||
const { LL } = useI18nContext();
|
||||
const [editItem, setEditItem] = useState<DeviceValue>(selectedItem);
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
@@ -71,12 +70,15 @@ const DashboardDevicesDialog = ({
|
||||
setFieldErrors(undefined);
|
||||
await validate(validator, editItem);
|
||||
onSave(editItem);
|
||||
} catch (errors: any) {
|
||||
setFieldErrors(errors);
|
||||
} catch (error) {
|
||||
setFieldErrors(error as ValidateFieldsError);
|
||||
}
|
||||
};
|
||||
|
||||
const setUom = (uom: number) => {
|
||||
const setUom = (uom?: DeviceValueUOM) => {
|
||||
if (uom === undefined) {
|
||||
return;
|
||||
}
|
||||
switch (uom) {
|
||||
case DeviceValueUOM.HOURS:
|
||||
return LL.HOURS();
|
||||
@@ -103,21 +105,24 @@ const DashboardDevicesDialog = ({
|
||||
return (
|
||||
<Dialog sx={dialogStyle} open={open} onClose={close}>
|
||||
<DialogTitle>
|
||||
{selectedItem.v === '' && selectedItem.c ? LL.RUN_COMMAND() : writeable ? LL.CHANGE_VALUE() : LL.VALUE(1)}
|
||||
{selectedItem.v === '' && selectedItem.c
|
||||
? LL.RUN_COMMAND()
|
||||
: writeable
|
||||
? LL.CHANGE_VALUE()
|
||||
: LL.VALUE(0)}
|
||||
</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Box color="warning.main" p={0} pl={0} pr={0} mt={0} mb={2}>
|
||||
<Typography variant="body2">{editItem.id.slice(2)}</Typography>
|
||||
</Box>
|
||||
<Grid>
|
||||
<Grid item>
|
||||
<Grid container>
|
||||
<Grid size={12}>
|
||||
{editItem.l ? (
|
||||
<TextField
|
||||
name="v"
|
||||
label={LL.VALUE(1)}
|
||||
label={LL.VALUE(0)}
|
||||
value={editItem.v}
|
||||
disabled={!writeable}
|
||||
autoFocus
|
||||
sx={{ width: '30ch' }}
|
||||
select
|
||||
onChange={updateFormValue}
|
||||
@@ -132,34 +137,41 @@ const DashboardDevicesDialog = ({
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="v"
|
||||
label={LL.VALUE(1)}
|
||||
value={Math.round(editItem.v * 10) / 10}
|
||||
label={LL.VALUE(0)}
|
||||
value={numberValue(Math.round((editItem.v as number) * 10) / 10)}
|
||||
autoFocus
|
||||
disabled={!writeable}
|
||||
type="number"
|
||||
sx={{ width: '30ch' }}
|
||||
onChange={updateFormValue}
|
||||
inputProps={editItem.s ? { min: editItem.m, max: editItem.x, step: editItem.s } : {}}
|
||||
InputProps={{
|
||||
startAdornment: <InputAdornment position="start">{setUom(editItem.u)}</InputAdornment>
|
||||
slotProps={{
|
||||
htmlInput: editItem.s
|
||||
? { min: editItem.m, max: editItem.x, step: editItem.s }
|
||||
: {},
|
||||
input: {
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
{setUom(editItem.u)}
|
||||
</InputAdornment>
|
||||
)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="v"
|
||||
label={LL.VALUE(1)}
|
||||
label={LL.VALUE(0)}
|
||||
value={editItem.v}
|
||||
disabled={!writeable}
|
||||
autoFocus
|
||||
sx={{ width: '30ch' }}
|
||||
multiline={editItem.u ? false : true}
|
||||
multiline={!editItem.u}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
)}
|
||||
</Grid>
|
||||
{writeable && (
|
||||
<Grid item>
|
||||
<Grid>
|
||||
<FormHelperText>{showHelperText(editItem)}</FormHelperText>
|
||||
</Grid>
|
||||
)}
|
||||
@@ -176,10 +188,20 @@ const DashboardDevicesDialog = ({
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
<Button startIcon={<CancelIcon />} variant="outlined" onClick={close} color="secondary">
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
onClick={close}
|
||||
color="secondary"
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button startIcon={<WarningIcon color="warning" />} variant="contained" onClick={save} color="info">
|
||||
<Button
|
||||
startIcon={<WarningIcon color="warning" />}
|
||||
variant="outlined"
|
||||
onClick={save}
|
||||
color="primary"
|
||||
>
|
||||
{selectedItem.v === '' && selectedItem.c ? LL.EXECUTE() : LL.UPDATE()}
|
||||
</Button>
|
||||
{progress && (
|
||||
@@ -204,4 +226,4 @@ const DashboardDevicesDialog = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardDevicesDialog;
|
||||
export default DevicesDialog;
|
||||
@@ -1,12 +1,13 @@
|
||||
import { ToggleButton, ToggleButtonGroup } from '@mui/material';
|
||||
|
||||
import OptionIcon from './OptionIcon';
|
||||
import { DeviceEntityMask } from './types';
|
||||
import type { DeviceEntity } from './types';
|
||||
|
||||
type EntityMaskToggleProps = {
|
||||
interface EntityMaskToggleProps {
|
||||
onUpdate: (de: DeviceEntity) => void;
|
||||
de: DeviceEntity;
|
||||
};
|
||||
}
|
||||
|
||||
const EntityMaskToggle = ({ onUpdate, de }: EntityMaskToggleProps) => {
|
||||
const getMaskNumber = (newMask: string[]) => {
|
||||
@@ -42,7 +43,7 @@ const EntityMaskToggle = ({ onUpdate, de }: EntityMaskToggleProps) => {
|
||||
size="small"
|
||||
color="secondary"
|
||||
value={getMaskString(de.m)}
|
||||
onChange={(event, mask) => {
|
||||
onChange={(event, mask: string[]) => {
|
||||
de.m = getMaskNumber(mask);
|
||||
if (de.n === '' && de.m & DeviceEntityMask.DV_READONLY) {
|
||||
de.m = de.m | DeviceEntityMask.DV_WEB_EXCLUDE;
|
||||
@@ -54,25 +55,46 @@ const EntityMaskToggle = ({ onUpdate, de }: EntityMaskToggleProps) => {
|
||||
}}
|
||||
>
|
||||
<ToggleButton value="8" disabled={(de.m & 0x81) !== 0 || de.n === undefined}>
|
||||
<OptionIcon type="favorite" isSet={(de.m & DeviceEntityMask.DV_FAVORITE) === DeviceEntityMask.DV_FAVORITE} />
|
||||
<OptionIcon
|
||||
type="favorite"
|
||||
isSet={
|
||||
(de.m & DeviceEntityMask.DV_FAVORITE) === DeviceEntityMask.DV_FAVORITE
|
||||
}
|
||||
/>
|
||||
</ToggleButton>
|
||||
<ToggleButton value="4" disabled={!de.w || (de.m & 0x83) >= 3}>
|
||||
<OptionIcon type="readonly" isSet={(de.m & DeviceEntityMask.DV_READONLY) === DeviceEntityMask.DV_READONLY} />
|
||||
<OptionIcon
|
||||
type="readonly"
|
||||
isSet={
|
||||
(de.m & DeviceEntityMask.DV_READONLY) === DeviceEntityMask.DV_READONLY
|
||||
}
|
||||
/>
|
||||
</ToggleButton>
|
||||
<ToggleButton value="2" disabled={de.n === '' || (de.m & 0x80) !== 0}>
|
||||
<OptionIcon
|
||||
type="api_mqtt_exclude"
|
||||
isSet={(de.m & DeviceEntityMask.DV_API_MQTT_EXCLUDE) === DeviceEntityMask.DV_API_MQTT_EXCLUDE}
|
||||
isSet={
|
||||
(de.m & DeviceEntityMask.DV_API_MQTT_EXCLUDE) ===
|
||||
DeviceEntityMask.DV_API_MQTT_EXCLUDE
|
||||
}
|
||||
/>
|
||||
</ToggleButton>
|
||||
<ToggleButton value="1" disabled={de.n === undefined || (de.m & 0x80) !== 0}>
|
||||
<OptionIcon
|
||||
type="web_exclude"
|
||||
isSet={(de.m & DeviceEntityMask.DV_WEB_EXCLUDE) === DeviceEntityMask.DV_WEB_EXCLUDE}
|
||||
isSet={
|
||||
(de.m & DeviceEntityMask.DV_WEB_EXCLUDE) ===
|
||||
DeviceEntityMask.DV_WEB_EXCLUDE
|
||||
}
|
||||
/>
|
||||
</ToggleButton>
|
||||
<ToggleButton value="128">
|
||||
<OptionIcon type="deleted" isSet={(de.m & DeviceEntityMask.DV_DELETED) === DeviceEntityMask.DV_DELETED} />
|
||||
<OptionIcon
|
||||
type="deleted"
|
||||
isSet={
|
||||
(de.m & DeviceEntityMask.DV_DELETED) === DeviceEntityMask.DV_DELETED
|
||||
}
|
||||
/>
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
);
|
||||
191
interface/src/app/main/Help.tsx
Normal file
191
interface/src/app/main/Help.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
import { useContext, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import CommentIcon from '@mui/icons-material/CommentTwoTone';
|
||||
import DownloadIcon from '@mui/icons-material/GetApp';
|
||||
import GitHubIcon from '@mui/icons-material/GitHub';
|
||||
import MenuBookIcon from '@mui/icons-material/MenuBookTwoTone';
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
Divider,
|
||||
Link,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemButton,
|
||||
ListItemText,
|
||||
Stack,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
|
||||
import { useRequest } from 'alova/client';
|
||||
import { SectionContent, useLayoutTitle } from 'components';
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { saveFile } from 'utils';
|
||||
|
||||
import { API, callAction } from '../../api/app';
|
||||
import type { APIcall } from './types';
|
||||
|
||||
const Help = () => {
|
||||
const { LL } = useI18nContext();
|
||||
useLayoutTitle(LL.HELP());
|
||||
|
||||
const { me } = useContext(AuthenticatedContext);
|
||||
|
||||
const [customSupportIMG, setCustomSupportIMG] = useState<string | null>(null);
|
||||
const [customSupportHTML, setCustomSupportHTML] = useState<string | null>(null);
|
||||
const [notFound, setNotFound] = useState<boolean>(false);
|
||||
|
||||
useRequest(() => callAction({ action: 'customSupport' })).onSuccess((event) => {
|
||||
if (event && event.data && Object.keys(event.data).length !== 0) {
|
||||
const data = event.data.Support;
|
||||
if (data.img_url) {
|
||||
setCustomSupportIMG(data.img_url);
|
||||
}
|
||||
if (data.html) {
|
||||
setCustomSupportHTML(data.html.join('<br/>'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// const { send: sendExportAllValues } = useRequest(
|
||||
// () => callAction({ action: 'export', param: 'allvalues' }),
|
||||
// {
|
||||
// immediate: false
|
||||
// }
|
||||
// )
|
||||
// .onSuccess((event) => {
|
||||
// saveFile(event.data, 'allvalues', '.txt');
|
||||
// toast.info(LL.DOWNLOAD_SUCCESSFUL());
|
||||
// })
|
||||
// .onError((error) => {
|
||||
// toast.error(error.message);
|
||||
// });
|
||||
|
||||
const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
|
||||
immediate: false
|
||||
})
|
||||
.onSuccess((event) => {
|
||||
saveFile(event.data, 'system_info', '.json');
|
||||
toast.info(LL.DOWNLOAD_SUCCESSFUL());
|
||||
})
|
||||
.onError((error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
|
||||
return (
|
||||
<SectionContent>
|
||||
<Stack
|
||||
padding={1}
|
||||
mb={2}
|
||||
direction="row"
|
||||
divider={<Divider orientation="vertical" flexItem />}
|
||||
sx={{
|
||||
borderRadius: 3,
|
||||
border: '2px solid grey',
|
||||
justifyContent: 'space-evenly',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle1">
|
||||
{customSupportHTML ? (
|
||||
<div dangerouslySetInnerHTML={{ __html: customSupportHTML }} />
|
||||
) : (
|
||||
LL.HELP_INFORMATION_5()
|
||||
)}
|
||||
</Typography>
|
||||
<Box
|
||||
component="img"
|
||||
referrerPolicy="no-referrer"
|
||||
sx={{
|
||||
maxHeight: { xs: 100, md: 250 }
|
||||
}}
|
||||
onError={() => setNotFound(true)}
|
||||
src={
|
||||
notFound
|
||||
? ''
|
||||
: customSupportIMG || 'https://emsesp.org/_media/images/installer.jpeg'
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
{me.admin && (
|
||||
<List sx={{ borderRadius: 3, border: '2px solid grey' }}>
|
||||
<ListItem>
|
||||
<ListItemButton component="a" href="https://emsesp.org">
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: '#72caf9' }}>
|
||||
<MenuBookIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={LL.HELP_INFORMATION_1()} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
|
||||
<ListItem>
|
||||
<ListItemButton component="a" href="https://discord.gg/3J3GgnzpyT">
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: '#72caf9' }}>
|
||||
<CommentIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={LL.HELP_INFORMATION_2()} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
|
||||
<ListItem>
|
||||
<ListItemButton
|
||||
component="a"
|
||||
href="https://github.com/emsesp/EMS-ESP32/issues/new/choose"
|
||||
>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: '#72caf9' }}>
|
||||
<GitHubIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={LL.HELP_INFORMATION_3()} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
</List>
|
||||
)}
|
||||
|
||||
<Box p={2} color="warning.main">
|
||||
<Typography mb={1} variant="body1">
|
||||
{LL.HELP_INFORMATION_4()}.
|
||||
</Typography>
|
||||
<Button
|
||||
startIcon={<DownloadIcon />}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => sendAPI({ device: 'system', cmd: 'info', id: 0 })}
|
||||
>
|
||||
{LL.SUPPORT_INFORMATION(0)}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{/* <Button
|
||||
sx={{ ml: 2 }}
|
||||
startIcon={<DownloadIcon />}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => sendExportAllValues()}
|
||||
>
|
||||
{LL.DOWNLOAD(1)} {LL.ALLVALUES()}
|
||||
</Button> */}
|
||||
|
||||
<Divider sx={{ mt: 4 }} />
|
||||
|
||||
<Typography color="white" variant="subtitle1" align="center" mt={1}>
|
||||
©
|
||||
<Link target="_blank" href="https://emsesp.org" color="primary">
|
||||
{'emsesp.org'}
|
||||
</Link>
|
||||
</Typography>
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default Help;
|
||||
269
interface/src/app/main/Modules.tsx
Normal file
269
interface/src/app/main/Modules.tsx
Normal file
@@ -0,0 +1,269 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useBlocker } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import CircleIcon from '@mui/icons-material/Circle';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
import { Box, Button, Typography } from '@mui/material';
|
||||
|
||||
import {
|
||||
Body,
|
||||
Cell,
|
||||
Header,
|
||||
HeaderCell,
|
||||
HeaderRow,
|
||||
Row,
|
||||
Table
|
||||
} from '@table-library/react-table-library/table';
|
||||
import { useTheme } from '@table-library/react-table-library/theme';
|
||||
import { updateState, useRequest } from 'alova/client';
|
||||
import {
|
||||
BlockNavigation,
|
||||
ButtonRow,
|
||||
FormLoader,
|
||||
SectionContent,
|
||||
useLayoutTitle
|
||||
} from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
import { readModules, writeModules } from '../../api/app';
|
||||
import ModulesDialog from './ModulesDialog';
|
||||
import type { ModuleItem } from './types';
|
||||
|
||||
const Modules = () => {
|
||||
const { LL } = useI18nContext();
|
||||
const [numChanges, setNumChanges] = useState<number>(0);
|
||||
const blocker = useBlocker(numChanges !== 0);
|
||||
|
||||
const [selectedModuleItem, setSelectedModuleItem] = useState<ModuleItem>();
|
||||
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||
|
||||
useLayoutTitle(LL.MODULES());
|
||||
|
||||
const {
|
||||
data: modules,
|
||||
send: fetchModules,
|
||||
error
|
||||
} = useRequest(readModules, {
|
||||
initialData: []
|
||||
});
|
||||
|
||||
const { send: updateModules } = useRequest(
|
||||
(data: { key: string; enabled: boolean; license: string }) => writeModules(data),
|
||||
{
|
||||
immediate: false
|
||||
}
|
||||
);
|
||||
|
||||
const modules_theme = useTheme({
|
||||
Table: `
|
||||
--data-table-library_grid-template-columns: 48px 180px 120px 100px repeat(1, minmax(160px, 1fr)) 180px;
|
||||
`,
|
||||
BaseRow: `
|
||||
font-size: 14px;
|
||||
.td {
|
||||
height: 32px;
|
||||
}
|
||||
`,
|
||||
BaseCell: `
|
||||
&:nth-of-type(1) {
|
||||
text-align: center;
|
||||
}
|
||||
`,
|
||||
HeaderRow: `
|
||||
text-transform: uppercase;
|
||||
background-color: black;
|
||||
color: #90CAF9;
|
||||
.th {
|
||||
border-bottom: 1px solid #565656;
|
||||
height: 36px;
|
||||
}
|
||||
`,
|
||||
Row: `
|
||||
background-color: #1e1e1e;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
.td {
|
||||
border-top: 1px solid #565656;
|
||||
border-bottom: 1px solid #565656;
|
||||
}
|
||||
&:hover .td {
|
||||
border-top: 1px solid #177ac9;
|
||||
border-bottom: 1px solid #177ac9;
|
||||
}
|
||||
&:nth-of-type(odd) .td {
|
||||
background-color: #303030;
|
||||
}
|
||||
`
|
||||
});
|
||||
|
||||
const onDialogClose = () => {
|
||||
setDialogOpen(false);
|
||||
};
|
||||
|
||||
const onDialogSave = (updatedItem: ModuleItem) => {
|
||||
setDialogOpen(false);
|
||||
updateModuleItem(updatedItem);
|
||||
};
|
||||
|
||||
const editModuleItem = useCallback((mi: ModuleItem) => {
|
||||
setSelectedModuleItem(mi);
|
||||
setDialogOpen(true);
|
||||
}, []);
|
||||
|
||||
const onCancel = async () => {
|
||||
await fetchModules().then(() => {
|
||||
setNumChanges(0);
|
||||
});
|
||||
};
|
||||
|
||||
function hasModulesChanged(mi: ModuleItem) {
|
||||
return mi.enabled !== mi.o_enabled || mi.license !== mi.o_license;
|
||||
}
|
||||
|
||||
const updateModuleItem = (updatedItem: ModuleItem) => {
|
||||
void updateState(readModules(), (data: ModuleItem[]) => {
|
||||
const new_data = data.map((mi) =>
|
||||
mi.id === updatedItem.id ? { ...mi, ...updatedItem } : mi
|
||||
);
|
||||
setNumChanges(new_data.filter((mi) => hasModulesChanged(mi)).length);
|
||||
return new_data;
|
||||
});
|
||||
};
|
||||
|
||||
const saveModules = async () => {
|
||||
await updateModules({
|
||||
modules: modules.map((condensed_mi) => ({
|
||||
key: condensed_mi.key,
|
||||
enabled: condensed_mi.enabled,
|
||||
license: condensed_mi.license
|
||||
}))
|
||||
})
|
||||
.then(() => {
|
||||
toast.success(LL.MODULES_UPDATED());
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
toast.error(error.message);
|
||||
})
|
||||
.finally(async () => {
|
||||
await fetchModules();
|
||||
setNumChanges(0);
|
||||
});
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
if (!modules) {
|
||||
return <FormLoader onRetry={fetchModules} errorMessage={error?.message} />;
|
||||
}
|
||||
|
||||
if (modules.length === 0) {
|
||||
return (
|
||||
<Typography variant="body2" color="error">
|
||||
{LL.MODULES_NONE()}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
const colorStatus = (status: number) => {
|
||||
if (status === 1) {
|
||||
return <div style={{ color: 'red' }}>Pending Activation</div>;
|
||||
}
|
||||
return <div style={{ color: '#00FF7F' }}>Activated</div>;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box mb={2} color="warning.main">
|
||||
<Typography variant="body1">{LL.MODULES_DESCRIPTION()}.</Typography>
|
||||
</Box>
|
||||
<Table
|
||||
data={{ nodes: modules }}
|
||||
theme={modules_theme}
|
||||
layout={{ custom: true }}
|
||||
>
|
||||
{(tableList: ModuleItem[]) => (
|
||||
<>
|
||||
<Header>
|
||||
<HeaderRow>
|
||||
<HeaderCell />
|
||||
<HeaderCell>{LL.NAME(0)}</HeaderCell>
|
||||
<HeaderCell>Author</HeaderCell>
|
||||
<HeaderCell>{LL.VERSION()}</HeaderCell>
|
||||
<HeaderCell>Message</HeaderCell>
|
||||
<HeaderCell>{LL.STATUS_OF('')}</HeaderCell>
|
||||
</HeaderRow>
|
||||
</Header>
|
||||
<Body>
|
||||
{tableList.map((mi: ModuleItem) => (
|
||||
<Row key={mi.id} item={mi} onClick={() => editModuleItem(mi)}>
|
||||
<Cell stiff>
|
||||
{mi.enabled ? (
|
||||
<CircleIcon
|
||||
color="success"
|
||||
sx={{ fontSize: 16, verticalAlign: 'middle' }}
|
||||
/>
|
||||
) : (
|
||||
<CircleIcon
|
||||
color="error"
|
||||
sx={{ fontSize: 16, verticalAlign: 'middle' }}
|
||||
/>
|
||||
)}
|
||||
</Cell>
|
||||
<Cell>{mi.name}</Cell>
|
||||
<Cell>{mi.author}</Cell>
|
||||
<Cell>{mi.version}</Cell>
|
||||
<Cell>{mi.message}</Cell>
|
||||
<Cell>{colorStatus(mi.status)}</Cell>
|
||||
</Row>
|
||||
))}
|
||||
</Body>
|
||||
</>
|
||||
)}
|
||||
</Table>
|
||||
|
||||
<Box mt={1} display="flex" flexWrap="wrap">
|
||||
<Box flexGrow={1}>
|
||||
{numChanges !== 0 && (
|
||||
<ButtonRow>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
onClick={onCancel}
|
||||
color="secondary"
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<WarningIcon color="warning" />}
|
||||
variant="contained"
|
||||
color="info"
|
||||
onClick={saveModules}
|
||||
>
|
||||
{LL.APPLY_CHANGES(numChanges)}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent>
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
{renderContent()}
|
||||
{selectedModuleItem && (
|
||||
<ModulesDialog
|
||||
open={dialogOpen}
|
||||
onClose={onDialogClose}
|
||||
onSave={onDialogSave}
|
||||
selectedItem={selectedModuleItem}
|
||||
/>
|
||||
)}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default Modules;
|
||||
106
interface/src/app/main/ModulesDialog.tsx
Normal file
106
interface/src/app/main/ModulesDialog.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import DoneIcon from '@mui/icons-material/Done';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Checkbox,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
TextField
|
||||
} from '@mui/material';
|
||||
import Grid from '@mui/material/Grid2';
|
||||
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import { BlockFormControlLabel } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { updateValue } from 'utils';
|
||||
|
||||
import type { ModuleItem } from './types';
|
||||
|
||||
interface ModulesDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (mi: ModuleItem) => void;
|
||||
selectedItem: ModuleItem;
|
||||
}
|
||||
|
||||
const ModulesDialog = ({
|
||||
open,
|
||||
onClose,
|
||||
onSave,
|
||||
selectedItem
|
||||
}: ModulesDialogProps) => {
|
||||
const { LL } = useI18nContext();
|
||||
const [editItem, setEditItem] = useState<ModuleItem>(selectedItem);
|
||||
|
||||
const updateFormValue = updateValue(setEditItem);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setEditItem(selectedItem);
|
||||
}
|
||||
}, [open, selectedItem]);
|
||||
|
||||
const close = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
const save = () => {
|
||||
onSave(editItem);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog sx={dialogStyle} fullWidth maxWidth="xs" open={open} onClose={onClose}>
|
||||
<DialogTitle>{LL.EDIT() + ' ' + editItem.key}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Grid container>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={editItem.enabled}
|
||||
onChange={updateFormValue}
|
||||
name="enabled"
|
||||
/>
|
||||
}
|
||||
label="Enabled"
|
||||
/>
|
||||
</Grid>
|
||||
<Box mt={2} mb={1}>
|
||||
<TextField
|
||||
name="license"
|
||||
label="License Key"
|
||||
multiline
|
||||
rows={6}
|
||||
fullWidth
|
||||
value={editItem.license}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
onClick={close}
|
||||
color="secondary"
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<DoneIcon />}
|
||||
variant="outlined"
|
||||
onClick={save}
|
||||
color="primary"
|
||||
>
|
||||
{LL.UPDATE()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModulesDialog;
|
||||
@@ -3,20 +3,26 @@ import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
|
||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||
import EditOffOutlinedIcon from '@mui/icons-material/EditOffOutlined';
|
||||
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
|
||||
|
||||
import InsertCommentOutlinedIcon from '@mui/icons-material/InsertCommentOutlined';
|
||||
import StarIcon from '@mui/icons-material/Star';
|
||||
import StarOutlineIcon from '@mui/icons-material/StarOutline';
|
||||
|
||||
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined';
|
||||
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
|
||||
|
||||
import type { SvgIconProps } from '@mui/material';
|
||||
import type { FC } from 'react';
|
||||
|
||||
type OptionType = 'deleted' | 'readonly' | 'web_exclude' | 'api_mqtt_exclude' | 'favorite';
|
||||
type OptionType =
|
||||
| 'deleted'
|
||||
| 'readonly'
|
||||
| 'web_exclude'
|
||||
| 'api_mqtt_exclude'
|
||||
| 'favorite';
|
||||
|
||||
const OPTION_ICONS: { [type in OptionType]: [React.ComponentType<SvgIconProps>, React.ComponentType<SvgIconProps>] } = {
|
||||
const OPTION_ICONS: {
|
||||
[type in OptionType]: [
|
||||
React.ComponentType<SvgIconProps>,
|
||||
React.ComponentType<SvgIconProps>
|
||||
];
|
||||
} = {
|
||||
deleted: [DeleteForeverIcon, DeleteOutlineIcon],
|
||||
readonly: [EditOffOutlinedIcon, EditOutlinedIcon],
|
||||
web_exclude: [VisibilityOffOutlinedIcon, VisibilityOutlinedIcon],
|
||||
@@ -24,12 +30,7 @@ const OPTION_ICONS: { [type in OptionType]: [React.ComponentType<SvgIconProps>,
|
||||
favorite: [StarIcon, StarOutlineIcon]
|
||||
};
|
||||
|
||||
interface OptionIconProps {
|
||||
type: OptionType;
|
||||
isSet: boolean;
|
||||
}
|
||||
|
||||
const OptionIcon: FC<OptionIconProps> = ({ type, isSet }) => {
|
||||
const OptionIcon = ({ type, isSet }: { type: OptionType; isSet: boolean }) => {
|
||||
const Icon = OPTION_ICONS[type][isSet ? 0 : 1];
|
||||
return isSet ? (
|
||||
<Icon color="primary" sx={{ fontSize: 16, verticalAlign: 'middle' }} />
|
||||
@@ -1,28 +1,40 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useBlocker } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import CircleIcon from '@mui/icons-material/Circle';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
import { Box, Button, Divider, Stack, Typography } from '@mui/material';
|
||||
|
||||
import { Box, Typography, Divider, Stack, Button } from '@mui/material';
|
||||
import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table';
|
||||
import {
|
||||
Body,
|
||||
Cell,
|
||||
Header,
|
||||
HeaderCell,
|
||||
HeaderRow,
|
||||
Row,
|
||||
Table
|
||||
} from '@table-library/react-table-library/table';
|
||||
import { useTheme } from '@table-library/react-table-library/theme';
|
||||
// eslint-disable-next-line import/named
|
||||
import { updateState, useRequest } from 'alova';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useBlocker } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
import SettingsSchedulerDialog from './SettingsSchedulerDialog';
|
||||
import * as EMSESP from './api';
|
||||
import { ScheduleFlag } from './types';
|
||||
import { schedulerItemValidation } from './validators';
|
||||
import type { ScheduleItem } from './types';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { ButtonRow, FormLoader, SectionContent, BlockNavigation } from 'components';
|
||||
|
||||
import { updateState, useRequest } from 'alova/client';
|
||||
import {
|
||||
BlockNavigation,
|
||||
ButtonRow,
|
||||
FormLoader,
|
||||
SectionContent,
|
||||
useLayoutTitle
|
||||
} from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
const SettingsScheduler: FC = () => {
|
||||
import { readSchedule, writeSchedule } from '../../api/app';
|
||||
import SettingsSchedulerDialog from './SchedulerDialog';
|
||||
import { ScheduleFlag } from './types';
|
||||
import type { Schedule, ScheduleItem } from './types';
|
||||
import { schedulerItemValidation } from './validators';
|
||||
|
||||
const Scheduler = () => {
|
||||
const { LL, locale } = useI18nContext();
|
||||
const [numChanges, setNumChanges] = useState<number>(0);
|
||||
const blocker = useBlocker(numChanges !== 0);
|
||||
@@ -31,16 +43,22 @@ const SettingsScheduler: FC = () => {
|
||||
const [creating, setCreating] = useState<boolean>(false);
|
||||
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||
|
||||
useLayoutTitle(LL.SCHEDULER());
|
||||
|
||||
const {
|
||||
data: schedule,
|
||||
send: fetchSchedule,
|
||||
error
|
||||
} = useRequest(EMSESP.readSchedule, {
|
||||
initialData: [],
|
||||
force: true
|
||||
} = useRequest(readSchedule, {
|
||||
initialData: []
|
||||
});
|
||||
|
||||
const { send: writeSchedule } = useRequest((data) => EMSESP.writeSchedule(data), { immediate: false });
|
||||
const { send: updateSchedule } = useRequest(
|
||||
(data: Schedule) => writeSchedule(data),
|
||||
{
|
||||
immediate: false
|
||||
}
|
||||
);
|
||||
|
||||
function hasScheduleChanged(si: ScheduleItem) {
|
||||
return (
|
||||
@@ -56,7 +74,10 @@ const SettingsScheduler: FC = () => {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const formatter = new Intl.DateTimeFormat(locale, { weekday: 'short', timeZone: 'UTC' });
|
||||
const formatter = new Intl.DateTimeFormat(locale, {
|
||||
weekday: 'short',
|
||||
timeZone: 'UTC'
|
||||
});
|
||||
const days = [1, 2, 3, 4, 5, 6, 7].map((day) => {
|
||||
const dd = day < 10 ? `0${day}` : day;
|
||||
return new Date(`2017-01-${dd}T00:00:00+00:00`);
|
||||
@@ -66,7 +87,7 @@ const SettingsScheduler: FC = () => {
|
||||
|
||||
const schedule_theme = useTheme({
|
||||
Table: `
|
||||
--data-table-library_grid-template-columns: 36px 324px 50px 192px repeat(1, minmax(100px, 1fr)) 160px;
|
||||
--data-table-library_grid-template-columns: 36px 210px 100px 192px repeat(1, minmax(100px, 1fr)) 160px;
|
||||
`,
|
||||
BaseRow: `
|
||||
font-size: 14px;
|
||||
@@ -96,21 +117,16 @@ const SettingsScheduler: FC = () => {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
.td {
|
||||
border-top: 1px solid #565656;
|
||||
border-bottom: 1px solid #565656;
|
||||
}
|
||||
&:hover .td {
|
||||
border-top: 1px solid #177ac9;
|
||||
border-bottom: 1px solid #177ac9;
|
||||
}
|
||||
&:nth-of-type(odd) .td {
|
||||
background-color: #303030;
|
||||
background-color: #177ac9;
|
||||
}
|
||||
`
|
||||
});
|
||||
|
||||
const saveSchedule = async () => {
|
||||
await writeSchedule({
|
||||
await updateSchedule({
|
||||
schedule: schedule
|
||||
.filter((si) => !si.deleted)
|
||||
.map((condensed_si) => ({
|
||||
@@ -126,8 +142,8 @@ const SettingsScheduler: FC = () => {
|
||||
.then(() => {
|
||||
toast.success(LL.SCHEDULE_UPDATED());
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
.catch((error: Error) => {
|
||||
toast.error(error.message);
|
||||
})
|
||||
.finally(async () => {
|
||||
await fetchSchedule();
|
||||
@@ -139,6 +155,9 @@ const SettingsScheduler: FC = () => {
|
||||
setCreating(false);
|
||||
setSelectedScheduleItem(si);
|
||||
setDialogOpen(true);
|
||||
if (si.o_name === undefined) {
|
||||
si.o_name = si.name;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onDialogClose = () => {
|
||||
@@ -153,12 +172,18 @@ const SettingsScheduler: FC = () => {
|
||||
|
||||
const onDialogSave = (updatedItem: ScheduleItem) => {
|
||||
setDialogOpen(false);
|
||||
|
||||
updateState('schedule', (data) => {
|
||||
void updateState(readSchedule(), (data: ScheduleItem[]) => {
|
||||
const new_data = creating
|
||||
? [...data.filter((si) => creating || si.o_id !== updatedItem.o_id), updatedItem]
|
||||
: data.map((si) => (si.id === updatedItem.id ? { ...si, ...updatedItem } : si));
|
||||
? [
|
||||
...data.filter((si) => creating || si.o_id !== updatedItem.o_id),
|
||||
updatedItem
|
||||
]
|
||||
: data.map((si) =>
|
||||
si.id === updatedItem.id ? { ...si, ...updatedItem } : si
|
||||
);
|
||||
|
||||
setNumChanges(new_data.filter((si) => hasScheduleChanged(si)).length);
|
||||
|
||||
return new_data;
|
||||
});
|
||||
};
|
||||
@@ -169,8 +194,8 @@ const SettingsScheduler: FC = () => {
|
||||
id: Math.floor(Math.random() * (Math.floor(200) - 100) + 100),
|
||||
active: false,
|
||||
deleted: false,
|
||||
flags: 0,
|
||||
time: '12:00',
|
||||
flags: ScheduleFlag.SCHEDULE_DAY,
|
||||
time: '',
|
||||
cmd: '',
|
||||
value: '',
|
||||
name: ''
|
||||
@@ -186,27 +211,52 @@ const SettingsScheduler: FC = () => {
|
||||
const dayBox = (si: ScheduleItem, flag: number) => (
|
||||
<>
|
||||
<Box>
|
||||
<Typography sx={{ fontSize: 11 }} color={(si.flags & flag) === flag ? 'primary' : 'grey'}>
|
||||
{flag === ScheduleFlag.SCHEDULE_TIMER ? LL.TIMER(0) : dow[Math.log(flag) / Math.log(2)]}
|
||||
<Typography
|
||||
sx={{ fontSize: 11 }}
|
||||
color={(si.flags & flag) === flag ? 'primary' : 'grey'}
|
||||
>
|
||||
{dow[Math.log(flag) / Math.log(2)]}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Divider orientation="vertical" flexItem />
|
||||
</>
|
||||
);
|
||||
|
||||
const scheduleType = (si: ScheduleItem) => (
|
||||
<Box>
|
||||
<Typography sx={{ fontSize: 11 }} color="primary">
|
||||
{si.flags === ScheduleFlag.SCHEDULE_IMMEDIATE ? (
|
||||
<>Immediate</>
|
||||
) : si.flags === ScheduleFlag.SCHEDULE_TIMER ? (
|
||||
<>Timer</>
|
||||
) : si.flags === ScheduleFlag.SCHEDULE_CONDITION ? (
|
||||
<>Condition</>
|
||||
) : si.flags === ScheduleFlag.SCHEDULE_ONCHANGE ? (
|
||||
<>On Change</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<Table
|
||||
data={{ nodes: schedule.filter((si) => !si.deleted).sort((a, b) => a.time.localeCompare(b.time)) }}
|
||||
data={{
|
||||
nodes: schedule
|
||||
.filter((si) => !si.deleted)
|
||||
.sort((a, b) => a.flags - b.flags)
|
||||
}}
|
||||
theme={schedule_theme}
|
||||
layout={{ custom: true }}
|
||||
>
|
||||
{(tableList: any) => (
|
||||
{(tableList: ScheduleItem[]) => (
|
||||
<>
|
||||
<Header>
|
||||
<HeaderRow>
|
||||
<HeaderCell />
|
||||
<HeaderCell stiff>{LL.SCHEDULE(0)}</HeaderCell>
|
||||
<HeaderCell stiff>{LL.TIME(0)}</HeaderCell>
|
||||
<HeaderCell stiff>{LL.TIME(0)}/Cond.</HeaderCell>
|
||||
<HeaderCell stiff>{LL.COMMAND(0)}</HeaderCell>
|
||||
<HeaderCell stiff>{LL.VALUE(0)}</HeaderCell>
|
||||
<HeaderCell stiff>{LL.NAME(0)}</HeaderCell>
|
||||
@@ -217,22 +267,33 @@ const SettingsScheduler: FC = () => {
|
||||
<Row key={si.id} item={si} onClick={() => editScheduleItem(si)}>
|
||||
<Cell stiff>
|
||||
{si.active ? (
|
||||
<CircleIcon color="success" sx={{ fontSize: 16, verticalAlign: 'middle' }} />
|
||||
<CircleIcon
|
||||
color="success"
|
||||
sx={{ fontSize: 16, verticalAlign: 'middle' }}
|
||||
/>
|
||||
) : (
|
||||
<CircleIcon color="error" sx={{ fontSize: 16, verticalAlign: 'middle' }} />
|
||||
<CircleIcon
|
||||
color="error"
|
||||
sx={{ fontSize: 16, verticalAlign: 'middle' }}
|
||||
/>
|
||||
)}
|
||||
</Cell>
|
||||
<Cell stiff>
|
||||
<Stack spacing={1} direction="row">
|
||||
<Stack spacing={0.5} direction="row">
|
||||
<Divider orientation="vertical" flexItem />
|
||||
{dayBox(si, ScheduleFlag.SCHEDULE_MON)}
|
||||
{dayBox(si, ScheduleFlag.SCHEDULE_TUE)}
|
||||
{dayBox(si, ScheduleFlag.SCHEDULE_WED)}
|
||||
{dayBox(si, ScheduleFlag.SCHEDULE_THU)}
|
||||
{dayBox(si, ScheduleFlag.SCHEDULE_FRI)}
|
||||
{dayBox(si, ScheduleFlag.SCHEDULE_SAT)}
|
||||
{dayBox(si, ScheduleFlag.SCHEDULE_SUN)}
|
||||
{dayBox(si, ScheduleFlag.SCHEDULE_TIMER)}
|
||||
{si.flags > 127 ? (
|
||||
scheduleType(si)
|
||||
) : (
|
||||
<>
|
||||
{dayBox(si, ScheduleFlag.SCHEDULE_MON)}
|
||||
{dayBox(si, ScheduleFlag.SCHEDULE_TUE)}
|
||||
{dayBox(si, ScheduleFlag.SCHEDULE_WED)}
|
||||
{dayBox(si, ScheduleFlag.SCHEDULE_THU)}
|
||||
{dayBox(si, ScheduleFlag.SCHEDULE_FRI)}
|
||||
{dayBox(si, ScheduleFlag.SCHEDULE_SAT)}
|
||||
{dayBox(si, ScheduleFlag.SCHEDULE_SUN)}
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Cell>
|
||||
<Cell>{si.time}</Cell>
|
||||
@@ -249,10 +310,10 @@ const SettingsScheduler: FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.SCHEDULER()} titleGutter>
|
||||
<SectionContent>
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
<Box mb={2} color="warning.main">
|
||||
<Typography variant="body2">{LL.SCHEDULER_HELP_1()}</Typography>
|
||||
<Typography variant="body1">{LL.SCHEDULER_HELP_1()}.</Typography>
|
||||
</Box>
|
||||
{renderSchedule()}
|
||||
|
||||
@@ -268,11 +329,16 @@ const SettingsScheduler: FC = () => {
|
||||
/>
|
||||
)}
|
||||
|
||||
<Box display="flex" flexWrap="wrap">
|
||||
<Box mt={1} display="flex" flexWrap="wrap">
|
||||
<Box flexGrow={1}>
|
||||
{numChanges !== 0 && (
|
||||
<ButtonRow>
|
||||
<Button startIcon={<CancelIcon />} variant="outlined" onClick={onDialogCancel} color="secondary">
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
onClick={onDialogCancel}
|
||||
color="secondary"
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
@@ -288,7 +354,12 @@ const SettingsScheduler: FC = () => {
|
||||
</Box>
|
||||
<Box flexWrap="nowrap" whiteSpace="nowrap">
|
||||
<ButtonRow>
|
||||
<Button startIcon={<AddIcon />} variant="outlined" color="secondary" onClick={addScheduleItem}>
|
||||
<Button
|
||||
startIcon={<AddIcon />}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={addScheduleItem}
|
||||
>
|
||||
{LL.ADD(0)}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
@@ -298,4 +369,4 @@ const SettingsScheduler: FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsScheduler;
|
||||
export default Scheduler;
|
||||
401
interface/src/app/main/SchedulerDialog.tsx
Normal file
401
interface/src/app/main/SchedulerDialog.tsx
Normal file
@@ -0,0 +1,401 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import DoneIcon from '@mui/icons-material/Done';
|
||||
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
||||
import RemoveIcon from '@mui/icons-material/RemoveCircleOutline';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Checkbox,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
TextField,
|
||||
ToggleButton,
|
||||
ToggleButtonGroup,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
import Grid from '@mui/material/Grid2';
|
||||
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import type Schema from 'async-validator';
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import { BlockFormControlLabel, ValidatedTextField } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { updateValue } from 'utils';
|
||||
import { validate } from 'validators';
|
||||
|
||||
import { ScheduleFlag } from './types';
|
||||
import type { ScheduleItem } from './types';
|
||||
|
||||
interface SchedulerDialogProps {
|
||||
open: boolean;
|
||||
creating: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (ei: ScheduleItem) => void;
|
||||
selectedItem: ScheduleItem;
|
||||
validator: Schema;
|
||||
dow: string[];
|
||||
}
|
||||
|
||||
const SchedulerDialog = ({
|
||||
open,
|
||||
creating,
|
||||
onClose,
|
||||
onSave,
|
||||
selectedItem,
|
||||
validator,
|
||||
dow
|
||||
}: SchedulerDialogProps) => {
|
||||
const { LL } = useI18nContext();
|
||||
const [editItem, setEditItem] = useState<ScheduleItem>(selectedItem);
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
|
||||
const [scheduleType, setScheduleType] = useState<ScheduleFlag>();
|
||||
|
||||
const updateFormValue = updateValue(setEditItem);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setFieldErrors(undefined);
|
||||
setEditItem(selectedItem);
|
||||
// set the flags based on type when page is loaded...
|
||||
// 0-127 is day schedule
|
||||
// 128 is timer
|
||||
// 129 is on change
|
||||
// 130 is on condition
|
||||
// 132 is immediate
|
||||
setScheduleType(
|
||||
selectedItem.flags < 128 ? ScheduleFlag.SCHEDULE_DAY : selectedItem.flags
|
||||
);
|
||||
}
|
||||
}, [open, selectedItem]);
|
||||
|
||||
const save = async () => {
|
||||
try {
|
||||
setFieldErrors(undefined);
|
||||
await validate(validator, editItem);
|
||||
onSave(editItem);
|
||||
} catch (error) {
|
||||
setFieldErrors(error as ValidateFieldsError);
|
||||
}
|
||||
};
|
||||
|
||||
const saveandactivate = async () => {
|
||||
editItem.active = true;
|
||||
try {
|
||||
setFieldErrors(undefined);
|
||||
await validate(validator, editItem);
|
||||
onSave(editItem);
|
||||
} catch (error) {
|
||||
setFieldErrors(error as ValidateFieldsError);
|
||||
}
|
||||
};
|
||||
|
||||
const remove = () => {
|
||||
editItem.deleted = true;
|
||||
onSave(editItem);
|
||||
};
|
||||
|
||||
const getFlagDOWnumber = (newFlag: string[]) => {
|
||||
let new_flag = 0;
|
||||
for (const entry of newFlag) {
|
||||
new_flag |= Number(entry);
|
||||
}
|
||||
return new_flag & 127;
|
||||
};
|
||||
|
||||
const getFlagDOWstring = (f: number) => {
|
||||
const new_flags: string[] = [];
|
||||
if ((f & 129) === 1) {
|
||||
new_flags.push('1');
|
||||
}
|
||||
if ((f & 130) === 2) {
|
||||
new_flags.push('2');
|
||||
}
|
||||
if ((f & 4) === 4) {
|
||||
new_flags.push('4');
|
||||
}
|
||||
if ((f & 8) === 8) {
|
||||
new_flags.push('8');
|
||||
}
|
||||
if ((f & 16) === 16) {
|
||||
new_flags.push('16');
|
||||
}
|
||||
if ((f & 32) === 32) {
|
||||
new_flags.push('32');
|
||||
}
|
||||
if ((f & 64) === 64) {
|
||||
new_flags.push('64');
|
||||
}
|
||||
|
||||
return new_flags;
|
||||
};
|
||||
|
||||
const showDOW = (si: ScheduleItem, flag: number) => (
|
||||
<Typography
|
||||
sx={{ fontSize: 10 }}
|
||||
color={(si.flags & flag) === flag ? 'primary' : 'grey'}
|
||||
>
|
||||
{dow[Math.log(flag) / Math.log(2)]}
|
||||
</Typography>
|
||||
);
|
||||
|
||||
const handleClose = (_event, reason: 'backdropClick' | 'escapeKeyDown') => {
|
||||
if (reason !== 'backdropClick') {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog sx={dialogStyle} open={open} onClose={handleClose}>
|
||||
<DialogTitle>
|
||||
{creating ? LL.ADD(1) + ' ' + LL.NEW(0) : LL.EDIT()}
|
||||
{LL.SCHEDULE(1)}
|
||||
</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<ToggleButtonGroup
|
||||
size="small"
|
||||
color="secondary"
|
||||
value={scheduleType}
|
||||
exclusive
|
||||
disabled={!creating}
|
||||
onChange={(_event, flag: ScheduleFlag) => {
|
||||
if (flag !== null) {
|
||||
setFieldErrors(undefined); // clear any validation errors
|
||||
setScheduleType(flag);
|
||||
// wipe the time field when changing the schedule type
|
||||
setEditItem({ ...editItem, time: '' });
|
||||
// set the flags based on type
|
||||
// 0-127 is day schedule
|
||||
// 128 is timer
|
||||
// 129 is on change
|
||||
// 130 is on condition
|
||||
// 132 is immediate
|
||||
setEditItem(
|
||||
flag === ScheduleFlag.SCHEDULE_DAY
|
||||
? { ...editItem, flags: 0 }
|
||||
: { ...editItem, flags: flag }
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ToggleButton value={ScheduleFlag.SCHEDULE_DAY}>
|
||||
<Typography
|
||||
sx={{ fontSize: 10 }}
|
||||
color={scheduleType === ScheduleFlag.SCHEDULE_DAY ? 'primary' : 'grey'}
|
||||
>
|
||||
{LL.SCHEDULE(0)}
|
||||
</Typography>
|
||||
</ToggleButton>
|
||||
<ToggleButton value={ScheduleFlag.SCHEDULE_TIMER}>
|
||||
<Typography
|
||||
sx={{ fontSize: 10 }}
|
||||
color={
|
||||
scheduleType === ScheduleFlag.SCHEDULE_TIMER ? 'primary' : 'grey'
|
||||
}
|
||||
>
|
||||
{LL.TIMER(0)}
|
||||
</Typography>
|
||||
</ToggleButton>
|
||||
<ToggleButton value={ScheduleFlag.SCHEDULE_ONCHANGE}>
|
||||
<Typography
|
||||
sx={{ fontSize: 10 }}
|
||||
color={
|
||||
scheduleType === ScheduleFlag.SCHEDULE_ONCHANGE ? 'primary' : 'grey'
|
||||
}
|
||||
>
|
||||
{LL.ONCHANGE()}
|
||||
</Typography>
|
||||
</ToggleButton>
|
||||
<ToggleButton value={ScheduleFlag.SCHEDULE_CONDITION}>
|
||||
<Typography
|
||||
sx={{ fontSize: 10 }}
|
||||
color={
|
||||
scheduleType === ScheduleFlag.SCHEDULE_CONDITION ? 'primary' : 'grey'
|
||||
}
|
||||
>
|
||||
{LL.CONDITION()}
|
||||
</Typography>
|
||||
</ToggleButton>
|
||||
<ToggleButton value={ScheduleFlag.SCHEDULE_IMMEDIATE}>
|
||||
<Typography
|
||||
sx={{ fontSize: 10 }}
|
||||
color={
|
||||
scheduleType === ScheduleFlag.SCHEDULE_IMMEDIATE ? 'primary' : 'grey'
|
||||
}
|
||||
>
|
||||
{LL.IMMEDIATE()}
|
||||
</Typography>
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
|
||||
{scheduleType === ScheduleFlag.SCHEDULE_DAY && (
|
||||
<ToggleButtonGroup
|
||||
size="small"
|
||||
color="secondary"
|
||||
value={getFlagDOWstring(editItem.flags)}
|
||||
onChange={(_event, flag: string[]) => {
|
||||
setEditItem({ ...editItem, flags: getFlagDOWnumber(flag) });
|
||||
}}
|
||||
>
|
||||
<ToggleButton value="2">
|
||||
{showDOW(editItem, ScheduleFlag.SCHEDULE_MON)}
|
||||
</ToggleButton>
|
||||
<ToggleButton value="4">
|
||||
{showDOW(editItem, ScheduleFlag.SCHEDULE_TUE)}
|
||||
</ToggleButton>
|
||||
<ToggleButton value="8">
|
||||
{showDOW(editItem, ScheduleFlag.SCHEDULE_WED)}
|
||||
</ToggleButton>
|
||||
<ToggleButton value="16">
|
||||
{showDOW(editItem, ScheduleFlag.SCHEDULE_THU)}
|
||||
</ToggleButton>
|
||||
<ToggleButton value="32">
|
||||
{showDOW(editItem, ScheduleFlag.SCHEDULE_FRI)}
|
||||
</ToggleButton>
|
||||
<ToggleButton value="64">
|
||||
{showDOW(editItem, ScheduleFlag.SCHEDULE_SAT)}
|
||||
</ToggleButton>
|
||||
<ToggleButton value="1">
|
||||
{showDOW(editItem, ScheduleFlag.SCHEDULE_SUN)}
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
)}
|
||||
|
||||
{scheduleType !== ScheduleFlag.SCHEDULE_IMMEDIATE && (
|
||||
<>
|
||||
<Grid container>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={editItem.active}
|
||||
onChange={updateFormValue}
|
||||
name="active"
|
||||
/>
|
||||
}
|
||||
label={LL.ACTIVE()}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid container>
|
||||
{scheduleType === ScheduleFlag.SCHEDULE_DAY ||
|
||||
scheduleType === ScheduleFlag.SCHEDULE_TIMER ? (
|
||||
<>
|
||||
<TextField
|
||||
name="time"
|
||||
type="time"
|
||||
label={
|
||||
scheduleType === ScheduleFlag.SCHEDULE_TIMER
|
||||
? LL.TIMER(1)
|
||||
: LL.TIME(1)
|
||||
}
|
||||
value={editItem.time === '' ? '00:00' : editItem.time}
|
||||
margin="normal"
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
{scheduleType === ScheduleFlag.SCHEDULE_TIMER && (
|
||||
<Box color="warning.main" ml={2} mt={4}>
|
||||
<Typography variant="body2">
|
||||
{LL.SCHEDULER_HELP_2()}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<TextField
|
||||
name="time"
|
||||
label={
|
||||
scheduleType === ScheduleFlag.SCHEDULE_CONDITION
|
||||
? LL.CONDITION()
|
||||
: scheduleType === ScheduleFlag.SCHEDULE_ONCHANGE
|
||||
? LL.ONCHANGE()
|
||||
: LL.IMMEDIATE()
|
||||
}
|
||||
multiline
|
||||
fullWidth
|
||||
value={editItem.time === '00:00' ? '' : editItem.time}
|
||||
margin="normal"
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
)}
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="cmd"
|
||||
label={LL.COMMAND(0)}
|
||||
multiline
|
||||
fullWidth
|
||||
value={editItem.cmd}
|
||||
margin="normal"
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
<TextField
|
||||
name="value"
|
||||
label={LL.VALUE(0)}
|
||||
multiline
|
||||
margin="normal"
|
||||
fullWidth
|
||||
value={editItem.value}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="name"
|
||||
label={LL.NAME(0) + ' (' + LL.OPTIONAL() + ')'}
|
||||
value={editItem.name}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
{!creating && (
|
||||
<Box flexGrow={1}>
|
||||
<Button
|
||||
startIcon={<RemoveIcon />}
|
||||
variant="outlined"
|
||||
color="warning"
|
||||
onClick={remove}
|
||||
>
|
||||
{LL.REMOVE()}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
onClick={onClose}
|
||||
color="secondary"
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={creating ? <AddIcon /> : <DoneIcon />}
|
||||
variant="outlined"
|
||||
onClick={save}
|
||||
color="primary"
|
||||
>
|
||||
{creating ? LL.ADD(0) : LL.UPDATE()}
|
||||
</Button>
|
||||
{scheduleType === ScheduleFlag.SCHEDULE_IMMEDIATE && editItem.cmd !== '' && (
|
||||
<Button
|
||||
startIcon={<PlayArrowIcon />}
|
||||
variant="outlined"
|
||||
onClick={saveandactivate}
|
||||
color="success"
|
||||
>
|
||||
{LL.EXECUTE()}
|
||||
</Button>
|
||||
)}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default SchedulerDialog;
|
||||
@@ -1,57 +1,96 @@
|
||||
import { useContext, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import AddCircleOutlineOutlinedIcon from '@mui/icons-material/AddCircleOutlineOutlined';
|
||||
import KeyboardArrowDownOutlinedIcon from '@mui/icons-material/KeyboardArrowDownOutlined';
|
||||
import KeyboardArrowUpOutlinedIcon from '@mui/icons-material/KeyboardArrowUpOutlined';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import UnfoldMoreOutlinedIcon from '@mui/icons-material/UnfoldMoreOutlined';
|
||||
import { Button, Typography, Box } from '@mui/material';
|
||||
import { useSort, SortToggleType } from '@table-library/react-table-library/sort';
|
||||
import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table';
|
||||
import { Box, Button, Typography } from '@mui/material';
|
||||
|
||||
import { SortToggleType, useSort } from '@table-library/react-table-library/sort';
|
||||
import {
|
||||
Body,
|
||||
Cell,
|
||||
Header,
|
||||
HeaderCell,
|
||||
HeaderRow,
|
||||
Row,
|
||||
Table
|
||||
} from '@table-library/react-table-library/table';
|
||||
import { useTheme } from '@table-library/react-table-library/theme';
|
||||
import { useRequest } from 'alova';
|
||||
import { useState, useContext, useEffect } from 'react';
|
||||
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import DashboardSensorsAnalogDialog from './DashboardSensorsAnalogDialog';
|
||||
import DashboardSensorsTemperatureDialog from './DashboardSensorsTemperatureDialog';
|
||||
import * as EMSESP from './api';
|
||||
|
||||
import { DeviceValueUOM, DeviceValueUOM_s, AnalogTypeNames, AnalogType } from './types';
|
||||
import { temperatureSensorItemValidation, analogSensorItemValidation } from './validators';
|
||||
import type { TemperatureSensor, AnalogSensor } from './types';
|
||||
import type { FC } from 'react';
|
||||
import { ButtonRow, SectionContent } from 'components';
|
||||
|
||||
import type { State } from '@table-library/react-table-library/types/common';
|
||||
import { useRequest } from 'alova/client';
|
||||
import { SectionContent, useLayoutTitle } from 'components';
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { useInterval } from 'utils';
|
||||
|
||||
const DashboardSensors: FC = () => {
|
||||
import {
|
||||
readSensorData,
|
||||
writeAnalogSensor,
|
||||
writeTemperatureSensor
|
||||
} from '../../api/app';
|
||||
import DashboardSensorsAnalogDialog from './SensorsAnalogDialog';
|
||||
import DashboardSensorsTemperatureDialog from './SensorsTemperatureDialog';
|
||||
import {
|
||||
AnalogType,
|
||||
AnalogTypeNames,
|
||||
DeviceValueUOM,
|
||||
DeviceValueUOM_s
|
||||
} from './types';
|
||||
import type {
|
||||
AnalogSensor,
|
||||
TemperatureSensor,
|
||||
WriteAnalogSensor,
|
||||
WriteTemperatureSensor
|
||||
} from './types';
|
||||
import {
|
||||
analogSensorItemValidation,
|
||||
temperatureSensorItemValidation
|
||||
} from './validators';
|
||||
|
||||
const Sensors = () => {
|
||||
const { LL } = useI18nContext();
|
||||
const { me } = useContext(AuthenticatedContext);
|
||||
const [selectedTemperatureSensor, setSelectedTemperatureSensor] = useState<TemperatureSensor>();
|
||||
|
||||
const [selectedTemperatureSensor, setSelectedTemperatureSensor] =
|
||||
useState<TemperatureSensor>();
|
||||
const [selectedAnalogSensor, setSelectedAnalogSensor] = useState<AnalogSensor>();
|
||||
const [temperatureDialogOpen, setTemperatureDialogOpen] = useState<boolean>(false);
|
||||
const [analogDialogOpen, setAnalogDialogOpen] = useState<boolean>(false);
|
||||
const [creating, setCreating] = useState<boolean>(false);
|
||||
|
||||
const { data: sensorData, send: fetchSensorData } = useRequest(() => EMSESP.readSensorData(), {
|
||||
initialData: {
|
||||
ts: [],
|
||||
as: [],
|
||||
analog_enabled: false,
|
||||
platform: 'ESP32'
|
||||
const { data: sensorData, send: fetchSensorData } = useRequest(
|
||||
() => readSensorData(),
|
||||
{
|
||||
initialData: {
|
||||
ts: [],
|
||||
as: [],
|
||||
analog_enabled: false,
|
||||
platform: 'ESP32'
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
const { send: writeTemperatureSensor } = useRequest((data) => EMSESP.writeTemperatureSensor(data), {
|
||||
immediate: false
|
||||
});
|
||||
const { send: sendTemperatureSensor } = useRequest(
|
||||
(data: WriteTemperatureSensor) => writeTemperatureSensor(data),
|
||||
{
|
||||
immediate: false
|
||||
}
|
||||
);
|
||||
|
||||
const { send: writeAnalogSensor } = useRequest((data) => EMSESP.writeAnalogSensor(data), {
|
||||
immediate: false
|
||||
});
|
||||
const { send: sendAnalogSensor } = useRequest(
|
||||
(data: WriteAnalogSensor) => writeAnalogSensor(data),
|
||||
{
|
||||
immediate: false
|
||||
}
|
||||
);
|
||||
|
||||
const isAdmin = me.admin;
|
||||
useInterval(() => {
|
||||
if (!temperatureDialogOpen && !analogDialogOpen) {
|
||||
void fetchSensorData();
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
const common_theme = useTheme({
|
||||
BaseRow: `
|
||||
@@ -77,19 +116,10 @@ const DashboardSensors: FC = () => {
|
||||
cursor: pointer;
|
||||
.td {
|
||||
padding: 8px;
|
||||
border-top: 1px solid #565656;
|
||||
border-bottom: 1px solid #565656;
|
||||
}
|
||||
&.tr.tr-body.row-select.row-select-single-selected {
|
||||
background-color: #3d4752;
|
||||
font-weight: normal;
|
||||
}
|
||||
&:hover .td {
|
||||
border-top: 1px solid #177ac9;
|
||||
border-bottom: 1px solid #177ac9;
|
||||
}
|
||||
&:nth-of-type(odd) .td {
|
||||
background-color: #303030;
|
||||
background-color: #177ac9;
|
||||
}
|
||||
`,
|
||||
Cell: `
|
||||
@@ -117,7 +147,57 @@ const DashboardSensors: FC = () => {
|
||||
}
|
||||
]);
|
||||
|
||||
const getSortIcon = (state: any, sortKey: any) => {
|
||||
const RenderTemperatureSensors = () => (
|
||||
<Table
|
||||
data={{ nodes: sensorData.ts }}
|
||||
theme={temperature_theme}
|
||||
sort={temperature_sort}
|
||||
layout={{ custom: true }}
|
||||
>
|
||||
{(tableList: TemperatureSensor[]) => (
|
||||
<>
|
||||
<Header>
|
||||
<HeaderRow>
|
||||
<HeaderCell resize>
|
||||
<Button
|
||||
fullWidth
|
||||
style={{ fontSize: '14px', justifyContent: 'flex-start' }}
|
||||
endIcon={getSortIcon(temperature_sort.state, 'NAME')}
|
||||
onClick={() =>
|
||||
temperature_sort.fns.onToggleSort({ sortKey: 'NAME' })
|
||||
}
|
||||
>
|
||||
{LL.NAME(0)}
|
||||
</Button>
|
||||
</HeaderCell>
|
||||
<HeaderCell stiff>
|
||||
<Button
|
||||
fullWidth
|
||||
style={{ fontSize: '14px', justifyContent: 'flex-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 key={ts.id} item={ts} onClick={() => updateTemperatureSensor(ts)}>
|
||||
<Cell>{ts.n}</Cell>
|
||||
<Cell>{formatValue(ts.t, ts.u)}</Cell>
|
||||
</Row>
|
||||
))}
|
||||
</Body>
|
||||
</>
|
||||
)}
|
||||
</Table>
|
||||
);
|
||||
|
||||
const getSortIcon = (state: State, sortKey: unknown) => {
|
||||
if (state.sortKey === sortKey && state.reverse) {
|
||||
return <KeyboardArrowDownOutlinedIcon />;
|
||||
}
|
||||
@@ -139,6 +219,7 @@ const DashboardSensors: FC = () => {
|
||||
sortToggleType: SortToggleType.AlternateWithReset,
|
||||
sortFns: {
|
||||
GPIO: (array) => array.sort((a, b) => a.g - b.g),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
NAME: (array) => array.sort((a, b) => a.n.localeCompare(b.n)),
|
||||
TYPE: (array) => array.sort((a, b) => a.t - b.t),
|
||||
VALUE: (array) => array.sort((a, b) => a.v - b.v)
|
||||
@@ -157,18 +238,14 @@ const DashboardSensors: FC = () => {
|
||||
},
|
||||
sortToggleType: SortToggleType.AlternateWithReset,
|
||||
sortFns: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
NAME: (array) => array.sort((a, b) => a.n.localeCompare(b.n)),
|
||||
VALUE: (array) => array.sort((a, b) => a.t - b.t)
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => fetchSensorData(), 30000);
|
||||
return () => {
|
||||
clearInterval(timer);
|
||||
};
|
||||
});
|
||||
useLayoutTitle(LL.SENSORS());
|
||||
|
||||
const formatDurationMin = (duration_min: number) => {
|
||||
const days = Math.trunc((duration_min * 60000) / 86400000);
|
||||
@@ -188,10 +265,13 @@ const DashboardSensors: FC = () => {
|
||||
return formatted;
|
||||
};
|
||||
|
||||
function formatValue(value: any, uom: number) {
|
||||
function formatValue(value: unknown, uom: DeviceValueUOM) {
|
||||
if (value === undefined) {
|
||||
return '';
|
||||
}
|
||||
if (typeof value !== 'number') {
|
||||
return value as string;
|
||||
}
|
||||
switch (uom) {
|
||||
case DeviceValueUOM.HOURS:
|
||||
return value ? formatDurationMin(value * 60) : LL.NUM_HOURS({ num: 0 });
|
||||
@@ -200,10 +280,7 @@ const DashboardSensors: FC = () => {
|
||||
case DeviceValueUOM.SECONDS:
|
||||
return LL.NUM_SECONDS({ num: value });
|
||||
case DeviceValueUOM.NONE:
|
||||
if (typeof value === 'number') {
|
||||
return new Intl.NumberFormat().format(value);
|
||||
}
|
||||
return value;
|
||||
return new Intl.NumberFormat().format(value);
|
||||
case DeviceValueUOM.DEGREES:
|
||||
case DeviceValueUOM.DEGREES_R:
|
||||
case DeviceValueUOM.FAHRENHEIT:
|
||||
@@ -220,7 +297,8 @@ const DashboardSensors: FC = () => {
|
||||
}
|
||||
|
||||
const updateTemperatureSensor = (ts: TemperatureSensor) => {
|
||||
if (isAdmin) {
|
||||
if (me.admin) {
|
||||
ts.o_n = ts.n;
|
||||
setSelectedTemperatureSensor(ts);
|
||||
setTemperatureDialogOpen(true);
|
||||
}
|
||||
@@ -228,26 +306,28 @@ const DashboardSensors: FC = () => {
|
||||
|
||||
const onTemperatureDialogClose = () => {
|
||||
setTemperatureDialogOpen(false);
|
||||
void fetchSensorData();
|
||||
};
|
||||
|
||||
const onTemperatureDialogSave = async (ts: TemperatureSensor) => {
|
||||
await writeTemperatureSensor({ id: ts.id, name: ts.n, offset: ts.o })
|
||||
await sendTemperatureSensor({ id: ts.id, name: ts.n, offset: ts.o })
|
||||
.then(() => {
|
||||
toast.success(LL.UPDATED_OF(LL.SENSOR(1)));
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(LL.UPDATE_OF(LL.SENSOR(2)) + ' ' + LL.FAILED(1));
|
||||
})
|
||||
.finally(async () => {
|
||||
.finally(() => {
|
||||
setTemperatureDialogOpen(false);
|
||||
setSelectedTemperatureSensor(undefined);
|
||||
await fetchSensorData();
|
||||
void fetchSensorData();
|
||||
});
|
||||
};
|
||||
|
||||
const updateAnalogSensor = (as: AnalogSensor) => {
|
||||
if (isAdmin) {
|
||||
if (me.admin) {
|
||||
setCreating(false);
|
||||
as.o_n = as.n;
|
||||
setSelectedAnalogSensor(as);
|
||||
setAnalogDialogOpen(true);
|
||||
}
|
||||
@@ -255,6 +335,7 @@ const DashboardSensors: FC = () => {
|
||||
|
||||
const onAnalogDialogClose = () => {
|
||||
setAnalogDialogOpen(false);
|
||||
void fetchSensorData();
|
||||
};
|
||||
|
||||
const addAnalogSensor = () => {
|
||||
@@ -262,19 +343,20 @@ const DashboardSensors: FC = () => {
|
||||
setSelectedAnalogSensor({
|
||||
id: Math.floor(Math.random() * (Math.floor(200) - 100) + 100),
|
||||
n: '',
|
||||
g: 40,
|
||||
g: 21, // default GPIO 21 which is safe for all platforms
|
||||
u: 0,
|
||||
v: 0,
|
||||
o: 0,
|
||||
t: 0,
|
||||
f: 1,
|
||||
d: false
|
||||
d: false,
|
||||
o_n: ''
|
||||
});
|
||||
setAnalogDialogOpen(true);
|
||||
};
|
||||
|
||||
const onAnalogDialogSave = async (as: AnalogSensor) => {
|
||||
await writeAnalogSensor({
|
||||
await sendAnalogSensor({
|
||||
id: as.id,
|
||||
gpio: as.g,
|
||||
name: as.n,
|
||||
@@ -290,57 +372,21 @@ const DashboardSensors: FC = () => {
|
||||
.catch(() => {
|
||||
toast.error(LL.UPDATE_OF(LL.ANALOG_SENSOR(5)) + ' ' + LL.FAILED(1));
|
||||
})
|
||||
.finally(async () => {
|
||||
.finally(() => {
|
||||
setAnalogDialogOpen(false);
|
||||
setSelectedAnalogSensor(undefined);
|
||||
await fetchSensorData();
|
||||
void fetchSensorData();
|
||||
});
|
||||
};
|
||||
|
||||
const RenderTemperatureSensors = () => (
|
||||
<Table data={{ nodes: sensorData.ts }} theme={temperature_theme} sort={temperature_sort} layout={{ custom: true }}>
|
||||
{(tableList: any) => (
|
||||
<>
|
||||
<Header>
|
||||
<HeaderRow>
|
||||
<HeaderCell resize>
|
||||
<Button
|
||||
fullWidth
|
||||
style={{ fontSize: '14px', justifyContent: 'flex-start' }}
|
||||
endIcon={getSortIcon(temperature_sort.state, 'NAME')}
|
||||
onClick={() => temperature_sort.fns.onToggleSort({ sortKey: 'NAME' })}
|
||||
>
|
||||
{LL.NAME(0)}
|
||||
</Button>
|
||||
</HeaderCell>
|
||||
<HeaderCell stiff>
|
||||
<Button
|
||||
fullWidth
|
||||
style={{ fontSize: '14px', justifyContent: 'flex-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 key={ts.id} item={ts} onClick={() => updateTemperatureSensor(ts)}>
|
||||
<Cell>{ts.n}</Cell>
|
||||
<Cell>{formatValue(ts.t, ts.u)}</Cell>
|
||||
</Row>
|
||||
))}
|
||||
</Body>
|
||||
</>
|
||||
)}
|
||||
</Table>
|
||||
);
|
||||
|
||||
const RenderAnalogSensors = () => (
|
||||
<Table data={{ nodes: sensorData.as }} theme={analog_theme} sort={analog_sort} layout={{ custom: true }}>
|
||||
{(tableList: any) => (
|
||||
<Table
|
||||
data={{ nodes: sensorData.as }}
|
||||
theme={analog_theme}
|
||||
sort={analog_sort}
|
||||
layout={{ custom: true }}
|
||||
>
|
||||
{(tableList: AnalogSensor[]) => (
|
||||
<>
|
||||
<Header>
|
||||
<HeaderRow>
|
||||
@@ -406,65 +452,56 @@ const DashboardSensors: FC = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.SENSOR_DATA()} titleGutter>
|
||||
{sensorData.ts.length > 0 && (
|
||||
<>
|
||||
<Typography sx={{ pt: 2, pb: 1 }} variant="h6" color="secondary">
|
||||
{LL.TEMP_SENSORS()}
|
||||
</Typography>
|
||||
<RenderTemperatureSensors />
|
||||
{selectedTemperatureSensor && (
|
||||
<DashboardSensorsTemperatureDialog
|
||||
open={temperatureDialogOpen}
|
||||
onClose={onTemperatureDialogClose}
|
||||
onSave={onTemperatureDialogSave}
|
||||
selectedItem={selectedTemperatureSensor}
|
||||
validator={temperatureSensorItemValidation()}
|
||||
/>
|
||||
<SectionContent>
|
||||
<Typography sx={{ pb: 1 }} variant="h6" color="secondary">
|
||||
{LL.TEMP_SENSORS()}
|
||||
</Typography>
|
||||
<RenderTemperatureSensors />
|
||||
{selectedTemperatureSensor && (
|
||||
<DashboardSensorsTemperatureDialog
|
||||
open={temperatureDialogOpen}
|
||||
onClose={onTemperatureDialogClose}
|
||||
onSave={onTemperatureDialogSave}
|
||||
selectedItem={selectedTemperatureSensor}
|
||||
validator={temperatureSensorItemValidation(
|
||||
sensorData.ts,
|
||||
selectedTemperatureSensor
|
||||
)}
|
||||
</>
|
||||
/>
|
||||
)}
|
||||
|
||||
{sensorData?.analog_enabled === true && (
|
||||
<>
|
||||
<Typography sx={{ pt: 4, pb: 1 }} variant="h6" color="secondary">
|
||||
{LL.ANALOG_SENSORS()}
|
||||
</Typography>
|
||||
<RenderAnalogSensors />
|
||||
{selectedAnalogSensor && (
|
||||
<DashboardSensorsAnalogDialog
|
||||
open={analogDialogOpen}
|
||||
onClose={onAnalogDialogClose}
|
||||
onSave={onAnalogDialogSave}
|
||||
creating={creating}
|
||||
selectedItem={selectedAnalogSensor}
|
||||
validator={analogSensorItemValidation(sensorData.as, creating, sensorData.platform)}
|
||||
/>
|
||||
<Typography sx={{ pt: 4, pb: 1 }} variant="h6" color="secondary">
|
||||
{LL.ANALOG_SENSORS()}
|
||||
</Typography>
|
||||
<RenderAnalogSensors />
|
||||
{selectedAnalogSensor && (
|
||||
<DashboardSensorsAnalogDialog
|
||||
open={analogDialogOpen}
|
||||
onClose={onAnalogDialogClose}
|
||||
onSave={onAnalogDialogSave}
|
||||
creating={creating}
|
||||
selectedItem={selectedAnalogSensor}
|
||||
validator={analogSensorItemValidation(
|
||||
sensorData.as,
|
||||
selectedAnalogSensor,
|
||||
creating,
|
||||
sensorData.platform
|
||||
)}
|
||||
</>
|
||||
/>
|
||||
)}
|
||||
|
||||
<ButtonRow>
|
||||
<Box mt={2} display="flex" flexWrap="wrap">
|
||||
<Box flexGrow={1}>
|
||||
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={fetchSensorData}>
|
||||
{LL.REFRESH()}
|
||||
</Button>
|
||||
</Box>
|
||||
{sensorData?.analog_enabled === true && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
startIcon={<AddCircleOutlineOutlinedIcon />}
|
||||
onClick={addAnalogSensor}
|
||||
>
|
||||
{LL.ADD(0) + ' ' + LL.ANALOG_SENSOR(1)}
|
||||
</Button>
|
||||
)}
|
||||
{sensorData?.analog_enabled === true && me.admin && (
|
||||
<Box mt={1} display="flex" flexWrap="wrap" justifyContent="flex-end">
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
startIcon={<AddCircleOutlineOutlinedIcon />}
|
||||
onClick={addAnalogSensor}
|
||||
>
|
||||
{LL.ADD(0)}
|
||||
</Button>
|
||||
</Box>
|
||||
</ButtonRow>
|
||||
)}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardSensors;
|
||||
export default Sensors;
|
||||
@@ -1,44 +1,43 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import RemoveIcon from '@mui/icons-material/RemoveCircleOutline';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Typography,
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
InputAdornment,
|
||||
Grid,
|
||||
MenuItem,
|
||||
TextField
|
||||
TextField,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
import { useState, useEffect } from 'react';
|
||||
import Grid from '@mui/material/Grid2';
|
||||
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import type Schema from 'async-validator';
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import { ValidatedTextField } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { numberValue, updateValue } from 'utils';
|
||||
import { validate } from 'validators';
|
||||
|
||||
import { AnalogType, AnalogTypeNames, DeviceValueUOM_s } from './types';
|
||||
import type { AnalogSensor } from './types';
|
||||
import type Schema from 'async-validator';
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import { ValidatedTextField } from 'components';
|
||||
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { numberValue, updateValue } from 'utils';
|
||||
|
||||
import { validate } from 'validators';
|
||||
|
||||
type DashboardSensorsAnalogDialogProps = {
|
||||
interface DashboardSensorsAnalogDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (as: AnalogSensor) => void;
|
||||
creating: boolean;
|
||||
selectedItem: AnalogSensor;
|
||||
validator: Schema;
|
||||
};
|
||||
}
|
||||
|
||||
const DashboardSensorsAnalogDialog = ({
|
||||
const SensorsAnalogDialog = ({
|
||||
open,
|
||||
onClose,
|
||||
onSave,
|
||||
@@ -58,8 +57,10 @@ const DashboardSensorsAnalogDialog = ({
|
||||
}
|
||||
}, [open, selectedItem]);
|
||||
|
||||
const close = () => {
|
||||
onClose();
|
||||
const handleClose = (_event, reason: 'backdropClick' | 'escapeKeyDown') => {
|
||||
if (reason !== 'backdropClick') {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
@@ -67,8 +68,8 @@ const DashboardSensorsAnalogDialog = ({
|
||||
setFieldErrors(undefined);
|
||||
await validate(validator, editItem);
|
||||
onSave(editItem);
|
||||
} catch (errors: any) {
|
||||
setFieldErrors(errors);
|
||||
} catch (error) {
|
||||
setFieldErrors(error as ValidateFieldsError);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -78,17 +79,19 @@ const DashboardSensorsAnalogDialog = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog sx={dialogStyle} open={open} onClose={close}>
|
||||
<Dialog sx={dialogStyle} open={open} onClose={handleClose}>
|
||||
<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)}
|
||||
</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={4}>
|
||||
<Grid>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="g"
|
||||
label="GPIO"
|
||||
sx={{ width: '11ch' }}
|
||||
value={numberValue(editItem.g)}
|
||||
type="number"
|
||||
variant="outlined"
|
||||
@@ -96,13 +99,13 @@ const DashboardSensorsAnalogDialog = ({
|
||||
/>
|
||||
</Grid>
|
||||
{creating && (
|
||||
<Grid item>
|
||||
<Grid>
|
||||
<Box color="warning.main" mt={2}>
|
||||
<Typography variant="body2">{LL.WARN_GPIO()}</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs={12}>
|
||||
<Grid>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="n"
|
||||
@@ -113,20 +116,34 @@ const DashboardSensorsAnalogDialog = ({
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={8}>
|
||||
<TextField name="t" label={LL.TYPE(0)} value={editItem.t} fullWidth select onChange={updateFormValue}>
|
||||
<Grid>
|
||||
<TextField
|
||||
name="t"
|
||||
label={LL.TYPE(0)}
|
||||
value={editItem.t}
|
||||
fullWidth
|
||||
select
|
||||
onChange={updateFormValue}
|
||||
>
|
||||
{AnalogTypeNames.map((val, i) => (
|
||||
<MenuItem key={i} value={i}>
|
||||
<MenuItem key={val} value={i}>
|
||||
{val}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid>
|
||||
{editItem.t >= AnalogType.COUNTER && editItem.t <= AnalogType.RATE && (
|
||||
<Grid item xs={4}>
|
||||
<TextField name="u" label={LL.UNIT()} value={editItem.u} fullWidth select onChange={updateFormValue}>
|
||||
<Grid>
|
||||
<TextField
|
||||
name="u"
|
||||
label={LL.UNIT()}
|
||||
value={editItem.u}
|
||||
sx={{ width: '15ch' }}
|
||||
select
|
||||
onChange={updateFormValue}
|
||||
>
|
||||
{DeviceValueUOM_s.map((val, i) => (
|
||||
<MenuItem key={i} value={i}>
|
||||
<MenuItem key={val} value={i}>
|
||||
{val}
|
||||
</MenuItem>
|
||||
))}
|
||||
@@ -134,142 +151,164 @@ const DashboardSensorsAnalogDialog = ({
|
||||
</Grid>
|
||||
)}
|
||||
{editItem.t === AnalogType.ADC && (
|
||||
<Grid item xs={4}>
|
||||
<Grid>
|
||||
<TextField
|
||||
name="o"
|
||||
label={LL.OFFSET()}
|
||||
value={numberValue(editItem.o)}
|
||||
fullWidth
|
||||
type="number"
|
||||
sx={{ width: '11ch' }}
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
inputProps={{ min: '0', max: '3300', step: '1' }}
|
||||
InputProps={{
|
||||
startAdornment: <InputAdornment position="start">mV</InputAdornment>
|
||||
slotProps={{
|
||||
input: {
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">mV</InputAdornment>
|
||||
)
|
||||
},
|
||||
htmlInput: { min: '0', max: '3300', step: '1' }
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
{editItem.t === AnalogType.COUNTER && (
|
||||
<Grid item xs={4}>
|
||||
<Grid>
|
||||
<TextField
|
||||
name="o"
|
||||
label={LL.STARTVALUE()}
|
||||
value={numberValue(editItem.o)}
|
||||
fullWidth
|
||||
type="number"
|
||||
sx={{ width: '11ch' }}
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
inputProps={{ step: '0.001' }}
|
||||
slotProps={{
|
||||
htmlInput: { step: '0.001' }
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
{editItem.t >= AnalogType.COUNTER && editItem.t <= AnalogType.RATE && (
|
||||
<Grid item xs={4}>
|
||||
<Grid>
|
||||
<TextField
|
||||
name="f"
|
||||
label={LL.FACTOR()}
|
||||
value={numberValue(editItem.f)}
|
||||
fullWidth
|
||||
sx={{ width: '11ch' }}
|
||||
type="number"
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
inputProps={{ step: '0.001' }}
|
||||
slotProps={{
|
||||
htmlInput: { step: '0.001' }
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
{editItem.t === AnalogType.DIGITAL_OUT && (editItem.g === 25 || editItem.g === 26) && (
|
||||
<Grid item xs={4}>
|
||||
<TextField
|
||||
name="o"
|
||||
label={LL.VALUE(0)}
|
||||
value={numberValue(editItem.o)}
|
||||
fullWidth
|
||||
type="number"
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
inputProps={{ min: '0', max: '255', step: '1' }}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
{editItem.t === AnalogType.DIGITAL_OUT && editItem.g !== 25 && editItem.g !== 26 && (
|
||||
<>
|
||||
<Grid item xs={4}>
|
||||
{editItem.t === AnalogType.DIGITAL_OUT &&
|
||||
(editItem.g === 25 || editItem.g === 26) && (
|
||||
<Grid>
|
||||
<TextField
|
||||
name="o"
|
||||
label={LL.VALUE(0)}
|
||||
value={numberValue(editItem.o)}
|
||||
fullWidth
|
||||
select
|
||||
sx={{ width: '11ch' }}
|
||||
type="number"
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
>
|
||||
<MenuItem value={0}>{LL.OFF()}</MenuItem>
|
||||
<MenuItem value={1}>{LL.ON()}</MenuItem>
|
||||
</TextField>
|
||||
slotProps={{
|
||||
htmlInput: { min: '0', max: '255', step: '1' }
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<TextField
|
||||
name="f"
|
||||
label={LL.POLARITY()}
|
||||
value={editItem.f}
|
||||
fullWidth
|
||||
select
|
||||
onChange={updateFormValue}
|
||||
>
|
||||
<MenuItem value={1}>{LL.ACTIVEHIGH()}</MenuItem>
|
||||
<MenuItem value={-1}>{LL.ACTIVELOW()}</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<TextField
|
||||
name="u"
|
||||
label={LL.STARTVALUE()}
|
||||
value={editItem.u}
|
||||
fullWidth
|
||||
select
|
||||
onChange={updateFormValue}
|
||||
>
|
||||
<MenuItem value={0}>{LL.UNCHANGED()}</MenuItem>
|
||||
<MenuItem value={1}>
|
||||
{LL.ALWAYS()} {LL.OFF()}
|
||||
</MenuItem>
|
||||
<MenuItem value={2}>
|
||||
{LL.ALWAYS()} {LL.ON()}
|
||||
</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
{(editItem.t === AnalogType.PWM_0 || editItem.t === AnalogType.PWM_1 || editItem.t === AnalogType.PWM_2) && (
|
||||
)}
|
||||
{editItem.t === AnalogType.DIGITAL_OUT &&
|
||||
editItem.g !== 25 &&
|
||||
editItem.g !== 26 && (
|
||||
<>
|
||||
<Grid>
|
||||
<TextField
|
||||
name="o"
|
||||
label={LL.VALUE(0)}
|
||||
value={numberValue(editItem.o)}
|
||||
select
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
>
|
||||
<MenuItem value={0}>{LL.OFF()}</MenuItem>
|
||||
<MenuItem value={1}>{LL.ON()}</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<TextField
|
||||
name="f"
|
||||
label={LL.POLARITY()}
|
||||
value={editItem.f}
|
||||
sx={{ width: '15ch' }}
|
||||
select
|
||||
onChange={updateFormValue}
|
||||
>
|
||||
<MenuItem value={1}>{LL.ACTIVEHIGH()}</MenuItem>
|
||||
<MenuItem value={-1}>{LL.ACTIVELOW()}</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<TextField
|
||||
name="u"
|
||||
label={LL.STARTVALUE()}
|
||||
sx={{ width: '15ch' }}
|
||||
value={editItem.u}
|
||||
select
|
||||
onChange={updateFormValue}
|
||||
>
|
||||
<MenuItem value={0}>{LL.UNCHANGED()}</MenuItem>
|
||||
<MenuItem value={1}>
|
||||
{LL.ALWAYS()} {LL.OFF()}
|
||||
</MenuItem>
|
||||
<MenuItem value={2}>
|
||||
{LL.ALWAYS()} {LL.ON()}
|
||||
</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
{(editItem.t === AnalogType.PWM_0 ||
|
||||
editItem.t === AnalogType.PWM_1 ||
|
||||
editItem.t === AnalogType.PWM_2) && (
|
||||
<>
|
||||
<Grid item xs={4}>
|
||||
<Grid>
|
||||
<TextField
|
||||
name="f"
|
||||
label={LL.FREQ()}
|
||||
value={numberValue(editItem.f)}
|
||||
fullWidth
|
||||
type="number"
|
||||
variant="outlined"
|
||||
sx={{ width: '11ch' }}
|
||||
onChange={updateFormValue}
|
||||
inputProps={{ min: '1', max: '5000', step: '1' }}
|
||||
InputProps={{
|
||||
startAdornment: <InputAdornment position="start">Hz</InputAdornment>
|
||||
slotProps={{
|
||||
input: {
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">Hz</InputAdornment>
|
||||
)
|
||||
},
|
||||
htmlInput: { min: '1', max: '5000', step: '1' }
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<Grid>
|
||||
<TextField
|
||||
name="o"
|
||||
label={LL.DUTY_CYCLE()}
|
||||
value={numberValue(editItem.o)}
|
||||
fullWidth
|
||||
type="number"
|
||||
sx={{ width: '11ch' }}
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
inputProps={{ min: '0', max: '100', step: '0.1' }}
|
||||
InputProps={{
|
||||
startAdornment: <InputAdornment position="start">%</InputAdornment>
|
||||
slotProps={{
|
||||
input: {
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">%</InputAdornment>
|
||||
)
|
||||
},
|
||||
htmlInput: { min: '0', max: '100', step: '0.1' }
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
@@ -280,15 +319,30 @@ const DashboardSensorsAnalogDialog = ({
|
||||
<DialogActions>
|
||||
{!creating && (
|
||||
<Box flexGrow={1} sx={{ '& button': { mt: 0 } }}>
|
||||
<Button startIcon={<RemoveIcon />} variant="outlined" color="error" onClick={remove}>
|
||||
<Button
|
||||
startIcon={<RemoveIcon />}
|
||||
variant="outlined"
|
||||
color="warning"
|
||||
onClick={remove}
|
||||
>
|
||||
{LL.REMOVE()}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
<Button startIcon={<CancelIcon />} variant="outlined" onClick={close} color="secondary">
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
onClick={onClose}
|
||||
color="secondary"
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button startIcon={<WarningIcon color="warning" />} variant="contained" onClick={save} color="info">
|
||||
<Button
|
||||
startIcon={<WarningIcon color="warning" />}
|
||||
variant="outlined"
|
||||
onClick={save}
|
||||
color="primary"
|
||||
>
|
||||
{creating ? LL.ADD(0) : LL.UPDATE()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
@@ -296,4 +350,4 @@ const DashboardSensorsAnalogDialog = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardSensorsAnalogDialog;
|
||||
export default SensorsAnalogDialog;
|
||||
@@ -1,46 +1,45 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Typography,
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
InputAdornment,
|
||||
Grid,
|
||||
TextField
|
||||
TextField,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
import { useState, useEffect } from 'react';
|
||||
import Grid from '@mui/material/Grid2';
|
||||
|
||||
import type { TemperatureSensor } from './types';
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import type Schema from 'async-validator';
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import { ValidatedTextField } from 'components';
|
||||
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { numberValue, updateValue } from 'utils';
|
||||
|
||||
import { validate } from 'validators';
|
||||
|
||||
type DashboardSensorsTemperatureDialogProps = {
|
||||
import type { TemperatureSensor } from './types';
|
||||
|
||||
interface SensorsTemperatureDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (ts: TemperatureSensor) => void;
|
||||
selectedItem: TemperatureSensor;
|
||||
validator: Schema;
|
||||
};
|
||||
}
|
||||
|
||||
const DashboardSensorsTemperatureDialog = ({
|
||||
const SensorsTemperatureDialog = ({
|
||||
open,
|
||||
onClose,
|
||||
onSave,
|
||||
selectedItem,
|
||||
validator
|
||||
}: DashboardSensorsTemperatureDialogProps) => {
|
||||
}: SensorsTemperatureDialogProps) => {
|
||||
const { LL } = useI18nContext();
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
const [editItem, setEditItem] = useState<TemperatureSensor>(selectedItem);
|
||||
@@ -53,8 +52,10 @@ const DashboardSensorsTemperatureDialog = ({
|
||||
}
|
||||
}, [open, selectedItem]);
|
||||
|
||||
const close = () => {
|
||||
onClose();
|
||||
const handleClose = (_event, reason: 'backdropClick' | 'escapeKeyDown') => {
|
||||
if (reason !== 'backdropClick') {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
@@ -62,13 +63,13 @@ const DashboardSensorsTemperatureDialog = ({
|
||||
setFieldErrors(undefined);
|
||||
await validate(validator, editItem);
|
||||
onSave(editItem);
|
||||
} catch (errors: any) {
|
||||
setFieldErrors(errors);
|
||||
} catch (error) {
|
||||
setFieldErrors(error as ValidateFieldsError);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog sx={dialogStyle} open={open} onClose={close}>
|
||||
<Dialog sx={dialogStyle} open={open} onClose={handleClose}>
|
||||
<DialogTitle>
|
||||
{LL.EDIT()} {LL.TEMP_SENSOR()}
|
||||
</DialogTitle>
|
||||
@@ -78,40 +79,53 @@ const DashboardSensorsTemperatureDialog = ({
|
||||
{LL.ID_OF(LL.SENSOR(0))}: {editItem.id}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Grid container spacing={1}>
|
||||
<Grid item>
|
||||
<Grid container spacing={2}>
|
||||
<Grid>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="n"
|
||||
label={LL.NAME(0)}
|
||||
value={editItem.n}
|
||||
autoFocus
|
||||
sx={{ width: '30ch' }}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Grid>
|
||||
<TextField
|
||||
name="o"
|
||||
label={LL.OFFSET()}
|
||||
value={numberValue(editItem.o)}
|
||||
sx={{ width: '12ch' }}
|
||||
sx={{ width: '11ch' }}
|
||||
type="number"
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
inputProps={{ min: '-5', max: '5', step: '0.1' }}
|
||||
InputProps={{
|
||||
startAdornment: <InputAdornment position="start">°C</InputAdornment>
|
||||
slotProps={{
|
||||
input: {
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">°C</InputAdornment>
|
||||
)
|
||||
},
|
||||
htmlInput: { min: '-5', max: '5', step: '0.1' }
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button startIcon={<CancelIcon />} variant="outlined" onClick={close} color="secondary">
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
onClick={onClose}
|
||||
color="secondary"
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button startIcon={<WarningIcon color="warning" />} variant="contained" onClick={save} color="info">
|
||||
<Button
|
||||
startIcon={<WarningIcon color="warning" />}
|
||||
variant="outlined"
|
||||
onClick={save}
|
||||
color="primary"
|
||||
>
|
||||
{LL.UPDATE()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
@@ -119,4 +133,4 @@ const DashboardSensorsTemperatureDialog = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardSensorsTemperatureDialog;
|
||||
export default SensorsTemperatureDialog;
|
||||
@@ -1,6 +1,7 @@
|
||||
import { DeviceValueUOM, DeviceValueUOM_s } from './types';
|
||||
import type { TranslationFunctions } from 'i18n/i18n-types';
|
||||
|
||||
import { DeviceValueUOM, DeviceValueUOM_s } from './types';
|
||||
|
||||
const formatDurationMin = (LL: TranslationFunctions, duration_min: number) => {
|
||||
const days = Math.trunc((duration_min * 60000) / 86400000);
|
||||
const hours = Math.trunc((duration_min * 60000) / 3600000) % 24;
|
||||
@@ -24,10 +25,18 @@ const formatDurationMin = (LL: TranslationFunctions, duration_min: number) => {
|
||||
return formatted;
|
||||
};
|
||||
|
||||
export function formatValue(LL: TranslationFunctions, value: any, uom: number) {
|
||||
if (value === undefined) {
|
||||
return '';
|
||||
export function formatValue(
|
||||
LL: TranslationFunctions,
|
||||
value?: unknown,
|
||||
uom?: DeviceValueUOM
|
||||
) {
|
||||
if (typeof value !== 'number' || uom === undefined || value === undefined) {
|
||||
if (value === undefined || typeof value === 'boolean') {
|
||||
return '';
|
||||
}
|
||||
return value as string;
|
||||
}
|
||||
|
||||
switch (uom) {
|
||||
case DeviceValueUOM.HOURS:
|
||||
return value ? formatDurationMin(LL, value * 60) : LL.NUM_HOURS({ num: 0 });
|
||||
@@ -36,10 +45,7 @@ export function formatValue(LL: TranslationFunctions, value: any, uom: number) {
|
||||
case DeviceValueUOM.SECONDS:
|
||||
return LL.NUM_SECONDS({ num: value });
|
||||
case DeviceValueUOM.NONE:
|
||||
if (typeof value === 'number') {
|
||||
return new Intl.NumberFormat().format(value);
|
||||
}
|
||||
return value;
|
||||
return new Intl.NumberFormat().format(value);
|
||||
case DeviceValueUOM.DEGREES:
|
||||
case DeviceValueUOM.DEGREES_R:
|
||||
case DeviceValueUOM.FAHRENHEIT:
|
||||
@@ -8,10 +8,13 @@ export interface Settings {
|
||||
syslog_host: string;
|
||||
syslog_port: number;
|
||||
boiler_heatingoff: boolean;
|
||||
remote_timeout_en: boolean;
|
||||
remote_timeout: number;
|
||||
shower_timer: boolean;
|
||||
shower_alert: boolean;
|
||||
shower_alert_coldshot: number;
|
||||
shower_alert_trigger: number;
|
||||
shower_min_duration: number;
|
||||
rx_gpio: number;
|
||||
tx_gpio: number;
|
||||
telnet_enabled: boolean;
|
||||
@@ -35,6 +38,11 @@ export interface Settings {
|
||||
eth_phy_addr: number;
|
||||
eth_clock_mode: number;
|
||||
platform: string;
|
||||
modbus_enabled: boolean;
|
||||
modbus_port: number;
|
||||
modbus_max_clients: number;
|
||||
modbus_timeout: number;
|
||||
developer_mode: boolean;
|
||||
}
|
||||
|
||||
export enum busConnectionStatus {
|
||||
@@ -50,13 +58,7 @@ export interface Stat {
|
||||
q: number; // quality
|
||||
}
|
||||
|
||||
export interface Status {
|
||||
status: busConnectionStatus;
|
||||
tx_mode: number;
|
||||
uptime: number;
|
||||
num_devices: number;
|
||||
num_sensors: number;
|
||||
num_analogs: number;
|
||||
export interface Activity {
|
||||
stats: Stat[];
|
||||
}
|
||||
|
||||
@@ -70,6 +72,7 @@ export interface Device {
|
||||
p: number; // productid
|
||||
v: string; // version
|
||||
e: number; // entities
|
||||
url?: string; // lowercase type name used in API URL
|
||||
}
|
||||
|
||||
export interface TemperatureSensor {
|
||||
@@ -78,6 +81,7 @@ export interface TemperatureSensor {
|
||||
t?: number; // temp, optional
|
||||
o: number; // offset
|
||||
u: number; // uom
|
||||
o_n?: string;
|
||||
}
|
||||
|
||||
export interface AnalogSensor {
|
||||
@@ -90,6 +94,7 @@ export interface AnalogSensor {
|
||||
f: number;
|
||||
t: number;
|
||||
d: boolean; // deleted flag
|
||||
o_n?: string;
|
||||
}
|
||||
|
||||
export interface WriteTemperatureSensor {
|
||||
@@ -110,40 +115,37 @@ export interface CoreData {
|
||||
devices: Device[];
|
||||
}
|
||||
|
||||
export interface DeviceShort {
|
||||
i: number; // id
|
||||
d?: number; // deviceid
|
||||
p?: number; // productid
|
||||
s: string; // shortname
|
||||
t?: number; // device type id
|
||||
tn?: string; // device type internal name
|
||||
}
|
||||
|
||||
export interface Devices {
|
||||
devices: DeviceShort[];
|
||||
export interface DashboardItem {
|
||||
id: number; // unique index
|
||||
t?: number; // type from DeviceType
|
||||
n?: string; // name, optional
|
||||
dv?: DeviceValue; // device value, optional
|
||||
nodes?: DashboardItem[]; // children nodes, optional
|
||||
}
|
||||
|
||||
export interface DeviceValue {
|
||||
id: string; // index, contains mask+name
|
||||
v: any; // value, Number or String
|
||||
u: number; // uom
|
||||
v?: unknown; // value, Number, String or Boolean - can be undefined
|
||||
u?: number; // uom, optional
|
||||
c?: string; // command, optional
|
||||
l?: string[]; // list, optional
|
||||
h?: string; // help text, optional
|
||||
s?: number; // steps for up/down, optional
|
||||
s?: string; // steps for up/down, optional
|
||||
m?: number; // min, optional
|
||||
x?: number; // max, optional
|
||||
}
|
||||
|
||||
export interface DeviceData {
|
||||
data: DeviceValue[];
|
||||
nodes: DeviceValue[];
|
||||
}
|
||||
|
||||
export interface DeviceEntity {
|
||||
id: string; // shortname
|
||||
v?: any; // value, in any format, optional
|
||||
v?: unknown; // value, in any format, optional
|
||||
n?: string; // fullname, optional
|
||||
cn?: string; // custom fullname, optional
|
||||
m: number; // mask
|
||||
t?: string; // tag for name
|
||||
m: DeviceEntityMask; // mask
|
||||
w: boolean; // writeable
|
||||
mi?: number; // min value
|
||||
ma?: number; // max value
|
||||
@@ -177,7 +179,9 @@ export enum DeviceValueUOM {
|
||||
L,
|
||||
KMIN,
|
||||
K,
|
||||
VOLTS
|
||||
VOLTS,
|
||||
MBAR,
|
||||
LH
|
||||
}
|
||||
|
||||
export const DeviceValueUOM_s = [
|
||||
@@ -204,39 +208,39 @@ export const DeviceValueUOM_s = [
|
||||
'l',
|
||||
'K*min',
|
||||
'K',
|
||||
'V'
|
||||
'V',
|
||||
'mbar',
|
||||
'l/h'
|
||||
];
|
||||
|
||||
export enum AnalogType {
|
||||
REMOVED = -1,
|
||||
NOTUSED = 0,
|
||||
DIGITAL_IN,
|
||||
COUNTER,
|
||||
ADC,
|
||||
TIMER,
|
||||
RATE,
|
||||
DIGITAL_OUT,
|
||||
PWM_0,
|
||||
PWM_1,
|
||||
PWM_2
|
||||
DIGITAL_IN = 1,
|
||||
COUNTER = 2,
|
||||
ADC = 3,
|
||||
TIMER = 4,
|
||||
RATE = 5,
|
||||
DIGITAL_OUT = 6,
|
||||
PWM_0 = 7,
|
||||
PWM_1 = 8,
|
||||
PWM_2 = 9
|
||||
}
|
||||
|
||||
export const AnalogTypeNames = [
|
||||
'(disabled)',
|
||||
'Digital in',
|
||||
'Digital In',
|
||||
'Counter',
|
||||
'ADC',
|
||||
'Timer',
|
||||
'Rate',
|
||||
'Digital out',
|
||||
'Digital Out',
|
||||
'PWM 0',
|
||||
'PWM 1',
|
||||
'PWM 2'
|
||||
];
|
||||
|
||||
type BoardProfiles = {
|
||||
[name: string]: string;
|
||||
};
|
||||
type BoardProfiles = Record<string, string>;
|
||||
|
||||
export const BOARD_PROFILES: BoardProfiles = {
|
||||
S32: 'BBQKees Gateway S32',
|
||||
@@ -268,9 +272,16 @@ export interface BoardProfile {
|
||||
|
||||
export interface APIcall {
|
||||
device: string;
|
||||
entity: string;
|
||||
id: any;
|
||||
cmd: string;
|
||||
id: number;
|
||||
data?: string; // optional
|
||||
}
|
||||
|
||||
export interface Action {
|
||||
action: string;
|
||||
param?: string; // optional
|
||||
}
|
||||
|
||||
export interface WriteAnalogSensor {
|
||||
id: number;
|
||||
gpio: number;
|
||||
@@ -294,12 +305,12 @@ export enum DeviceEntityMask {
|
||||
export interface ScheduleItem {
|
||||
id: number; // unique index
|
||||
active: boolean;
|
||||
deleted?: boolean; // optional
|
||||
deleted?: boolean;
|
||||
flags: number;
|
||||
time: string;
|
||||
time: string; // also used for Condition and On Change
|
||||
cmd: string;
|
||||
value: string;
|
||||
name: string; // optional
|
||||
name: string; // can be empty
|
||||
o_id?: number;
|
||||
o_active?: boolean;
|
||||
o_deleted?: boolean;
|
||||
@@ -310,6 +321,28 @@ export interface ScheduleItem {
|
||||
o_name?: string;
|
||||
}
|
||||
|
||||
export interface Schedule {
|
||||
schedule: ScheduleItem[];
|
||||
}
|
||||
|
||||
export interface ModuleItem {
|
||||
id: number; // unique index
|
||||
key: string;
|
||||
name: string;
|
||||
author: string;
|
||||
version: string;
|
||||
status: number;
|
||||
message: string;
|
||||
enabled: boolean;
|
||||
license: string;
|
||||
o_enabled?: boolean;
|
||||
o_license?: string;
|
||||
}
|
||||
|
||||
export interface Modules {
|
||||
modules: ModuleItem[];
|
||||
}
|
||||
|
||||
export enum ScheduleFlag {
|
||||
SCHEDULE_SUN = 1,
|
||||
SCHEDULE_MON = 2,
|
||||
@@ -318,11 +351,17 @@ export enum ScheduleFlag {
|
||||
SCHEDULE_THU = 16,
|
||||
SCHEDULE_FRI = 32,
|
||||
SCHEDULE_SAT = 64,
|
||||
SCHEDULE_TIMER = 128
|
||||
// types...
|
||||
SCHEDULE_DAY = 0, // no bits set
|
||||
SCHEDULE_TIMER = 128, // bit 8
|
||||
SCHEDULE_ONCHANGE = 129, // bit 1
|
||||
SCHEDULE_CONDITION = 130, // bit 2
|
||||
SCHEDULE_IMMEDIATE = 132 // bit 3
|
||||
}
|
||||
|
||||
export interface EntityItem {
|
||||
id: number; // unique number
|
||||
ram: number;
|
||||
name: string;
|
||||
device_id: number | string;
|
||||
type_id: number | string;
|
||||
@@ -330,10 +369,11 @@ export interface EntityItem {
|
||||
factor: number;
|
||||
uom: number;
|
||||
value_type: number;
|
||||
value?: any;
|
||||
value?: unknown;
|
||||
writeable: boolean;
|
||||
deleted?: boolean;
|
||||
o_id?: number;
|
||||
o_ram?: number;
|
||||
o_name?: string;
|
||||
o_device_id?: number | string;
|
||||
o_type_id?: number | string;
|
||||
@@ -343,6 +383,7 @@ export interface EntityItem {
|
||||
o_value_type?: number;
|
||||
o_deleted?: boolean;
|
||||
o_writeable?: boolean;
|
||||
o_value?: unknown;
|
||||
}
|
||||
|
||||
export interface Entities {
|
||||
@@ -352,9 +393,10 @@ export interface Entities {
|
||||
// matches emsdevice.h DeviceType
|
||||
export const enum DeviceType {
|
||||
SYSTEM = 0,
|
||||
TEMPERATURESENSOR,
|
||||
ANALOGSENSOR,
|
||||
SCHEDULER,
|
||||
TEMPERATURESENSOR = 1,
|
||||
ANALOGSENSOR = 2,
|
||||
SCHEDULER = 3,
|
||||
CUSTOM = 4,
|
||||
BOILER,
|
||||
THERMOSTAT,
|
||||
MIXER,
|
||||
@@ -368,33 +410,37 @@ export const enum DeviceType {
|
||||
EXTENSION,
|
||||
GENERIC,
|
||||
HEATSOURCE,
|
||||
CUSTOM,
|
||||
VENTILATION,
|
||||
WATER,
|
||||
POOL,
|
||||
UNKNOWN
|
||||
}
|
||||
|
||||
// matches emsdevicevalue.h
|
||||
export const enum DeviceValueType {
|
||||
BOOL,
|
||||
INT,
|
||||
UINT,
|
||||
SHORT,
|
||||
USHORT,
|
||||
ULONG,
|
||||
TIME, // same as ULONG (32 bits)
|
||||
INT8,
|
||||
UINT8,
|
||||
INT16,
|
||||
UINT16,
|
||||
UINT24,
|
||||
TIME, // same as UINT24
|
||||
UINT32,
|
||||
ENUM,
|
||||
STRING,
|
||||
STRING, // RAW
|
||||
CMD
|
||||
}
|
||||
|
||||
export const DeviceValueTypeNames = [
|
||||
'BOOL',
|
||||
'INT',
|
||||
'UINT',
|
||||
'SHORT',
|
||||
'USHORT',
|
||||
'ULONG',
|
||||
'INT8',
|
||||
'UINT8',
|
||||
'INT16',
|
||||
'UINT16',
|
||||
'UINT24',
|
||||
'TIME',
|
||||
'UINT32',
|
||||
'ENUM',
|
||||
'STRING',
|
||||
'RAW',
|
||||
'CMD'
|
||||
];
|
||||
504
interface/src/app/main/validators.ts
Normal file
504
interface/src/app/main/validators.ts
Normal file
@@ -0,0 +1,504 @@
|
||||
import Schema from 'async-validator';
|
||||
import type { InternalRuleItem } from 'async-validator';
|
||||
import { IP_OR_HOSTNAME_VALIDATOR } from 'validators/shared';
|
||||
|
||||
import type {
|
||||
AnalogSensor,
|
||||
DeviceValue,
|
||||
EntityItem,
|
||||
ScheduleItem,
|
||||
Settings,
|
||||
TemperatureSensor
|
||||
} from './types';
|
||||
|
||||
export const GPIO_VALIDATOR = {
|
||||
validator(
|
||||
rule: InternalRuleItem,
|
||||
value: number,
|
||||
callback: (error?: string) => void
|
||||
) {
|
||||
if (
|
||||
value &&
|
||||
(value === 1 ||
|
||||
(value >= 6 && value <= 11) ||
|
||||
value === 20 ||
|
||||
value === 24 ||
|
||||
(value >= 28 && value <= 31) ||
|
||||
value > 40 ||
|
||||
value < 0)
|
||||
) {
|
||||
callback('Must be an valid GPIO port');
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const GPIO_VALIDATORR = {
|
||||
validator(
|
||||
rule: InternalRuleItem,
|
||||
value: number,
|
||||
callback: (error?: string) => void
|
||||
) {
|
||||
if (
|
||||
value &&
|
||||
(value === 1 ||
|
||||
(value >= 6 && value <= 11) ||
|
||||
(value >= 16 && value <= 17) ||
|
||||
value === 20 ||
|
||||
value === 24 ||
|
||||
(value >= 28 && value <= 31) ||
|
||||
value > 40 ||
|
||||
value < 0)
|
||||
) {
|
||||
callback('Must be an valid GPIO port');
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const GPIO_VALIDATORC3 = {
|
||||
validator(
|
||||
rule: InternalRuleItem,
|
||||
value: number,
|
||||
callback: (error?: string) => void
|
||||
) {
|
||||
if (value && ((value >= 11 && value <= 19) || value > 21 || value < 0)) {
|
||||
callback('Must be an valid GPIO port');
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const GPIO_VALIDATORS2 = {
|
||||
validator(
|
||||
rule: InternalRuleItem,
|
||||
value: number,
|
||||
callback: (error?: string) => void
|
||||
) {
|
||||
if (
|
||||
value &&
|
||||
((value >= 19 && value <= 20) ||
|
||||
(value >= 22 && value <= 32) ||
|
||||
value > 40 ||
|
||||
value < 0)
|
||||
) {
|
||||
callback('Must be an valid GPIO port');
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const GPIO_VALIDATORS3 = {
|
||||
validator(
|
||||
rule: InternalRuleItem,
|
||||
value: number,
|
||||
callback: (error?: string) => void
|
||||
) {
|
||||
if (
|
||||
value &&
|
||||
((value >= 19 && value <= 20) ||
|
||||
(value >= 22 && value <= 37) ||
|
||||
(value >= 39 && value <= 42) ||
|
||||
value > 48 ||
|
||||
value < 0)
|
||||
) {
|
||||
callback('Must be an valid GPIO port');
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const createSettingsValidator = (settings: Settings) =>
|
||||
new Schema({
|
||||
...(settings.board_profile === 'CUSTOM' &&
|
||||
settings.platform === 'ESP32' && {
|
||||
led_gpio: [
|
||||
{ required: true, message: 'LED GPIO is required' },
|
||||
GPIO_VALIDATOR
|
||||
],
|
||||
dallas_gpio: [
|
||||
{ required: true, message: 'GPIO is required' },
|
||||
GPIO_VALIDATOR
|
||||
],
|
||||
pbutton_gpio: [
|
||||
{ required: true, message: 'Button GPIO is required' },
|
||||
GPIO_VALIDATOR
|
||||
],
|
||||
tx_gpio: [
|
||||
{ required: true, message: 'Tx GPIO is required' },
|
||||
GPIO_VALIDATOR
|
||||
],
|
||||
rx_gpio: [{ required: true, message: 'Rx GPIO is required' }, GPIO_VALIDATOR]
|
||||
}),
|
||||
...(settings.board_profile === 'CUSTOM' &&
|
||||
settings.platform === 'ESP32C3' && {
|
||||
led_gpio: [
|
||||
{ required: true, message: 'LED GPIO is required' },
|
||||
GPIO_VALIDATORC3
|
||||
],
|
||||
dallas_gpio: [
|
||||
{ required: true, message: 'GPIO is required' },
|
||||
GPIO_VALIDATORC3
|
||||
],
|
||||
pbutton_gpio: [
|
||||
{ required: true, message: 'Button GPIO is required' },
|
||||
GPIO_VALIDATORC3
|
||||
],
|
||||
tx_gpio: [
|
||||
{ required: true, message: 'Tx GPIO is required' },
|
||||
GPIO_VALIDATORC3
|
||||
],
|
||||
rx_gpio: [
|
||||
{ required: true, message: 'Rx GPIO is required' },
|
||||
GPIO_VALIDATORC3
|
||||
]
|
||||
}),
|
||||
...(settings.board_profile === 'CUSTOM' &&
|
||||
settings.platform === 'ESP32S2' && {
|
||||
led_gpio: [
|
||||
{ required: true, message: 'LED GPIO is required' },
|
||||
GPIO_VALIDATORS2
|
||||
],
|
||||
dallas_gpio: [
|
||||
{ required: true, message: 'GPIO is required' },
|
||||
GPIO_VALIDATORS2
|
||||
],
|
||||
pbutton_gpio: [
|
||||
{ required: true, message: 'Button GPIO is required' },
|
||||
GPIO_VALIDATORS2
|
||||
],
|
||||
tx_gpio: [
|
||||
{ required: true, message: 'Tx GPIO is required' },
|
||||
GPIO_VALIDATORS2
|
||||
],
|
||||
rx_gpio: [
|
||||
{ required: true, message: 'Rx GPIO is required' },
|
||||
GPIO_VALIDATORS2
|
||||
]
|
||||
}),
|
||||
...(settings.board_profile === 'CUSTOM' &&
|
||||
settings.platform === 'ESP32S3' && {
|
||||
led_gpio: [
|
||||
{ required: true, message: 'LED GPIO is required' },
|
||||
GPIO_VALIDATORS3
|
||||
],
|
||||
dallas_gpio: [
|
||||
{ required: true, message: 'GPIO is required' },
|
||||
GPIO_VALIDATORS3
|
||||
],
|
||||
pbutton_gpio: [
|
||||
{ required: true, message: 'Button GPIO is required' },
|
||||
GPIO_VALIDATORS3
|
||||
],
|
||||
tx_gpio: [
|
||||
{ required: true, message: 'Tx GPIO is required' },
|
||||
GPIO_VALIDATORS3
|
||||
],
|
||||
rx_gpio: [
|
||||
{ required: true, message: 'Rx GPIO is required' },
|
||||
GPIO_VALIDATORS3
|
||||
]
|
||||
}),
|
||||
...(settings.syslog_enabled && {
|
||||
syslog_host: [
|
||||
{ required: true, message: 'Host is required' },
|
||||
IP_OR_HOSTNAME_VALIDATOR
|
||||
],
|
||||
syslog_port: [
|
||||
{ required: true, message: 'Port is required' },
|
||||
{ type: 'number', min: 0, max: 65535, message: 'Invalid Port' }
|
||||
],
|
||||
syslog_mark_interval: [
|
||||
{ required: true, message: 'Mark interval is required' },
|
||||
{ type: 'number', min: 0, max: 10, message: 'Must be between 0 and 10' }
|
||||
]
|
||||
}),
|
||||
...(settings.modbus_enabled && {
|
||||
modbus_max_clients: [
|
||||
{ required: true, message: 'Max clients is required' },
|
||||
{ type: 'number', min: 0, max: 50, message: 'Invalid number' }
|
||||
],
|
||||
modbus_port: [
|
||||
{ required: true, message: 'Port is required' },
|
||||
{ type: 'number', min: 0, max: 65535, message: 'Invalid Port' }
|
||||
],
|
||||
modbus_timeout: [
|
||||
{ required: true, message: 'Timeout is required' },
|
||||
{
|
||||
type: 'number',
|
||||
min: 100,
|
||||
max: 20000,
|
||||
message: 'Must be between 100 and 20000'
|
||||
}
|
||||
]
|
||||
}),
|
||||
...(settings.shower_timer && {
|
||||
shower_min_duration: [
|
||||
{
|
||||
type: 'number',
|
||||
min: 10,
|
||||
max: 360,
|
||||
message: 'Time must be between 10 and 360 seconds'
|
||||
}
|
||||
]
|
||||
}),
|
||||
...(settings.shower_alert && {
|
||||
shower_alert_trigger: [
|
||||
{
|
||||
type: 'number',
|
||||
min: 1,
|
||||
max: 20,
|
||||
message: 'Time must be between 1 and 20 minutes'
|
||||
}
|
||||
],
|
||||
shower_alert_coldshot: [
|
||||
{
|
||||
type: 'number',
|
||||
min: 1,
|
||||
max: 10,
|
||||
message: 'Time must be between 1 and 10 seconds'
|
||||
}
|
||||
]
|
||||
}),
|
||||
...(settings.remote_timeout_en && {
|
||||
remote_timeout: [
|
||||
{
|
||||
type: 'number',
|
||||
min: 1,
|
||||
max: 240,
|
||||
message: 'Timeout must be between 1 and 240 hours'
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
export const uniqueNameValidator = (schedule: ScheduleItem[], o_name?: string) => ({
|
||||
validator(
|
||||
rule: InternalRuleItem,
|
||||
name: string,
|
||||
callback: (error?: string) => void
|
||||
) {
|
||||
if (
|
||||
name !== '' &&
|
||||
(o_name === undefined || o_name.toLowerCase() !== name.toLowerCase()) &&
|
||||
schedule.find((si) => si.name.toLowerCase() === name.toLowerCase())
|
||||
) {
|
||||
callback('Name already in use');
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const schedulerItemValidation = (
|
||||
schedule: ScheduleItem[],
|
||||
scheduleItem: ScheduleItem
|
||||
) =>
|
||||
new Schema({
|
||||
name: [
|
||||
{
|
||||
type: 'string',
|
||||
pattern: /^[a-zA-Z0-9_]{0,19}$/,
|
||||
message: "Must be <20 characters: alphanumeric or '_'"
|
||||
},
|
||||
...[uniqueNameValidator(schedule, scheduleItem.o_name)]
|
||||
],
|
||||
cmd: [
|
||||
{ required: true, message: 'Command is required' },
|
||||
{
|
||||
type: 'string',
|
||||
min: 1,
|
||||
max: 300,
|
||||
message: 'Command must be 1-300 characters'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
export const uniqueCustomNameValidator = (
|
||||
entity: EntityItem[],
|
||||
o_name?: string
|
||||
) => ({
|
||||
validator(
|
||||
rule: InternalRuleItem,
|
||||
name: string,
|
||||
callback: (error?: string) => void
|
||||
) {
|
||||
if (
|
||||
(o_name === undefined || o_name.toLowerCase() !== name.toLowerCase()) &&
|
||||
entity.find((ei) => ei.name.toLowerCase() === name.toLowerCase())
|
||||
) {
|
||||
callback('Name already in use');
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const entityItemValidation = (entity: EntityItem[], entityItem: EntityItem) =>
|
||||
new Schema({
|
||||
name: [
|
||||
{ required: true, message: 'Name is required' },
|
||||
{
|
||||
type: 'string',
|
||||
pattern: /^[a-zA-Z0-9_]{1,19}$/,
|
||||
message: "Must be <20 characters: alphanumeric or '_'"
|
||||
},
|
||||
...[uniqueCustomNameValidator(entity, entityItem.o_name)]
|
||||
],
|
||||
device_id: [
|
||||
{
|
||||
validator(
|
||||
rule: InternalRuleItem,
|
||||
value: string,
|
||||
callback: (error?: string) => void
|
||||
) {
|
||||
if (isNaN(parseInt(value, 16))) {
|
||||
callback('Is required and must be in hex format');
|
||||
}
|
||||
callback();
|
||||
}
|
||||
}
|
||||
],
|
||||
type_id: [
|
||||
{
|
||||
validator(
|
||||
rule: InternalRuleItem,
|
||||
value: string,
|
||||
callback: (error?: string) => void
|
||||
) {
|
||||
if (isNaN(parseInt(value, 16))) {
|
||||
callback('Is required and must be in hex format');
|
||||
}
|
||||
callback();
|
||||
}
|
||||
}
|
||||
],
|
||||
offset: [
|
||||
{ required: true, message: 'Offset is required' },
|
||||
{ type: 'number', min: 0, max: 255, message: 'Must be between 0 and 255' }
|
||||
],
|
||||
factor: [
|
||||
{ required: true, message: 'Bytes is required' },
|
||||
{ type: 'number', min: 1, max: 255, message: 'Must be between 1 and 255' }
|
||||
]
|
||||
});
|
||||
|
||||
export const uniqueTemperatureNameValidator = (
|
||||
sensors: TemperatureSensor[],
|
||||
o_name?: string
|
||||
) => ({
|
||||
validator(rule: InternalRuleItem, n: string, callback: (error?: string) => void) {
|
||||
if (
|
||||
(o_name === undefined || o_name.toLowerCase() !== n.toLowerCase()) &&
|
||||
n !== '' &&
|
||||
sensors.find((ts) => ts.n.toLowerCase() === n.toLowerCase())
|
||||
) {
|
||||
callback('Name already in use');
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const temperatureSensorItemValidation = (
|
||||
sensors: TemperatureSensor[],
|
||||
sensor: TemperatureSensor
|
||||
) =>
|
||||
new Schema({
|
||||
n: [
|
||||
{
|
||||
type: 'string',
|
||||
pattern: /^[a-zA-Z0-9_]{0,19}$/,
|
||||
message: "Must be <20 characters: alphanumeric or '_'"
|
||||
},
|
||||
...[uniqueTemperatureNameValidator(sensors, sensor.o_n)]
|
||||
]
|
||||
});
|
||||
|
||||
export const isGPIOUniqueValidator = (sensors: AnalogSensor[]) => ({
|
||||
validator(
|
||||
rule: InternalRuleItem,
|
||||
gpio: number,
|
||||
callback: (error?: string) => void
|
||||
) {
|
||||
if (sensors.find((as) => as.g === gpio)) {
|
||||
callback('GPIO already in use');
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const uniqueAnalogNameValidator = (
|
||||
sensors: AnalogSensor[],
|
||||
o_name?: string
|
||||
) => ({
|
||||
validator(rule: InternalRuleItem, n: string, callback: (error?: string) => void) {
|
||||
if (
|
||||
(o_name === undefined || o_name.toLowerCase() !== n.toLowerCase()) &&
|
||||
n !== '' &&
|
||||
sensors.find((as) => as.n.toLowerCase() === n.toLowerCase())
|
||||
) {
|
||||
callback('Name already in use');
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const analogSensorItemValidation = (
|
||||
sensors: AnalogSensor[],
|
||||
sensor: AnalogSensor,
|
||||
creating: boolean,
|
||||
platform: string
|
||||
) =>
|
||||
new Schema({
|
||||
n: [
|
||||
{
|
||||
type: 'string',
|
||||
pattern: /^[a-zA-Z0-9_]{0,19}$/,
|
||||
message: "Must be <20 characters: alphanumeric or '_'"
|
||||
},
|
||||
...[uniqueAnalogNameValidator(sensors, sensor.o_n)]
|
||||
],
|
||||
g: [
|
||||
{ required: true, message: 'GPIO is required' },
|
||||
platform === 'ESP32S3'
|
||||
? GPIO_VALIDATORS3
|
||||
: platform === 'ESP32S2'
|
||||
? GPIO_VALIDATORS2
|
||||
: platform === 'ESP32C3'
|
||||
? GPIO_VALIDATORC3
|
||||
: GPIO_VALIDATOR,
|
||||
...(creating ? [isGPIOUniqueValidator(sensors)] : [])
|
||||
]
|
||||
});
|
||||
|
||||
export const deviceValueItemValidation = (dv: DeviceValue) =>
|
||||
new Schema({
|
||||
v: [
|
||||
{ required: true, message: 'Value is required' },
|
||||
{
|
||||
validator(
|
||||
rule: InternalRuleItem,
|
||||
value: unknown,
|
||||
callback: (error?: string) => void
|
||||
) {
|
||||
if (
|
||||
typeof value === 'number' &&
|
||||
dv.m &&
|
||||
dv.x &&
|
||||
(value < dv.m || value > dv.x)
|
||||
) {
|
||||
callback('Value out of range');
|
||||
}
|
||||
callback();
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
@@ -1,33 +1,33 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
import { Button, Checkbox, MenuItem } from '@mui/material';
|
||||
import { range } from 'lodash-es';
|
||||
import { useState } from 'react';
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import type { APSettings } from 'types';
|
||||
import * as APApi from 'api/ap';
|
||||
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import {
|
||||
BlockFormControlLabel,
|
||||
BlockNavigation,
|
||||
ButtonRow,
|
||||
FormLoader,
|
||||
SectionContent,
|
||||
ValidatedPasswordField,
|
||||
ValidatedTextField,
|
||||
BlockNavigation
|
||||
useLayoutTitle
|
||||
} from 'components';
|
||||
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import type { APSettingsType } from 'types';
|
||||
import { APProvisionMode } from 'types';
|
||||
import { numberValue, updateValueDirty, useRest } from 'utils';
|
||||
|
||||
import { createAPSettingsValidator, validate } from 'validators';
|
||||
|
||||
export const isAPEnabled = ({ provision_mode }: APSettings) =>
|
||||
provision_mode === APProvisionMode.AP_MODE_ALWAYS || provision_mode === APProvisionMode.AP_MODE_DISCONNECTED;
|
||||
export const isAPEnabled = ({ provision_mode }: APSettingsType) =>
|
||||
provision_mode === APProvisionMode.AP_MODE_ALWAYS ||
|
||||
provision_mode === APProvisionMode.AP_MODE_DISCONNECTED;
|
||||
|
||||
const APSettingsForm: FC = () => {
|
||||
const APSettings = () => {
|
||||
const {
|
||||
loadData,
|
||||
saving,
|
||||
@@ -39,16 +39,23 @@ const APSettingsForm: FC = () => {
|
||||
blocker,
|
||||
saveData,
|
||||
errorMessage
|
||||
} = useRest<APSettings>({
|
||||
} = useRest<APSettingsType>({
|
||||
read: APApi.readAPSettings,
|
||||
update: APApi.updateAPSettings
|
||||
});
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
useLayoutTitle(LL.SETTINGS_OF(LL.ACCESS_POINT(0)));
|
||||
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
|
||||
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue);
|
||||
const updateFormValue = updateValueDirty(
|
||||
origData,
|
||||
dirtyFlags,
|
||||
setDirtyFlags,
|
||||
updateDataValue
|
||||
);
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
@@ -60,11 +67,16 @@ const APSettingsForm: FC = () => {
|
||||
setFieldErrors(undefined);
|
||||
await validate(createAPSettingsValidator(data), data);
|
||||
await saveData();
|
||||
} catch (errors: any) {
|
||||
setFieldErrors(errors);
|
||||
} catch (error) {
|
||||
setFieldErrors(error as ValidateFieldsError);
|
||||
}
|
||||
};
|
||||
|
||||
// no lodash - https://asleepace.com/blog/typescript-range-without-a-loop/
|
||||
function range(a: number, b: number): number[] {
|
||||
return a < b ? [a, ...range(a + 1, b)] : [b];
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ValidatedTextField
|
||||
@@ -78,9 +90,15 @@ const APSettingsForm: FC = () => {
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
>
|
||||
<MenuItem value={APProvisionMode.AP_MODE_ALWAYS}>{LL.AP_PROVIDE_TEXT_1()}</MenuItem>
|
||||
<MenuItem value={APProvisionMode.AP_MODE_DISCONNECTED}>{LL.AP_PROVIDE_TEXT_2()}</MenuItem>
|
||||
<MenuItem value={APProvisionMode.AP_NEVER}>{LL.AP_PROVIDE_TEXT_3()}</MenuItem>
|
||||
<MenuItem value={APProvisionMode.AP_MODE_ALWAYS}>
|
||||
{LL.AP_PROVIDE_TEXT_1()}
|
||||
</MenuItem>
|
||||
<MenuItem value={APProvisionMode.AP_MODE_DISCONNECTED}>
|
||||
{LL.AP_PROVIDE_TEXT_2()}
|
||||
</MenuItem>
|
||||
<MenuItem value={APProvisionMode.AP_NEVER}>
|
||||
{LL.AP_PROVIDE_TEXT_3()}
|
||||
</MenuItem>
|
||||
</ValidatedTextField>
|
||||
{isAPEnabled(data) && (
|
||||
<>
|
||||
@@ -123,7 +141,13 @@ const APSettingsForm: FC = () => {
|
||||
))}
|
||||
</ValidatedTextField>
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox name="ssid_hidden" checked={data.ssid_hidden} onChange={updateFormValue} />}
|
||||
control={
|
||||
<Checkbox
|
||||
name="ssid_hidden"
|
||||
checked={data.ssid_hidden}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
}
|
||||
label={LL.AP_HIDE_SSID()}
|
||||
/>
|
||||
<ValidatedTextField
|
||||
@@ -182,7 +206,7 @@ const APSettingsForm: FC = () => {
|
||||
startIcon={<CancelIcon />}
|
||||
disabled={saving}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
color="secondary"
|
||||
type="submit"
|
||||
onClick={loadData}
|
||||
>
|
||||
@@ -205,11 +229,11 @@ const APSettingsForm: FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.SETTINGS_OF(LL.ACCESS_POINT(1))} titleGutter>
|
||||
<SectionContent>
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default APSettingsForm;
|
||||
export default APSettings;
|
||||
File diff suppressed because it is too large
Load Diff
131
interface/src/app/settings/DownloadUpload.tsx
Normal file
131
interface/src/app/settings/DownloadUpload.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import DownloadIcon from '@mui/icons-material/GetApp';
|
||||
import { Box, Button, Typography } from '@mui/material';
|
||||
import Grid from '@mui/material/Grid2';
|
||||
|
||||
import * as SystemApi from 'api/system';
|
||||
import { API, callAction } from 'api/app';
|
||||
|
||||
import { useRequest } from 'alova/client';
|
||||
import type { APIcall } from 'app/main/types';
|
||||
import RestartMonitor from 'app/status/RestartMonitor';
|
||||
import {
|
||||
FormLoader,
|
||||
SectionContent,
|
||||
SingleUpload,
|
||||
useLayoutTitle
|
||||
} from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { saveFile } from 'utils';
|
||||
|
||||
const DownloadUpload = () => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const [restarting, setRestarting] = useState<boolean>(false);
|
||||
|
||||
const { send: sendExportData } = useRequest(
|
||||
(type: string) => callAction({ action: 'export', param: type }),
|
||||
{
|
||||
immediate: false
|
||||
}
|
||||
)
|
||||
.onSuccess((event) => {
|
||||
saveFile(event.data, event.args[0], '.json');
|
||||
toast.info(LL.DOWNLOAD_SUCCESSFUL());
|
||||
})
|
||||
.onError((error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
|
||||
const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
const { data, send: loadData, error } = useRequest(SystemApi.readSystemStatus);
|
||||
|
||||
const doRestart = async () => {
|
||||
setRestarting(true);
|
||||
await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch(
|
||||
(error: Error) => {
|
||||
toast.error(error.message);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
useLayoutTitle(LL.DOWNLOAD_UPLOAD());
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography sx={{ pb: 2 }} variant="h6" color="primary">
|
||||
{LL.DOWNLOAD(0)}
|
||||
</Typography>
|
||||
|
||||
<Typography mb={1} variant="body1" color="warning">
|
||||
{LL.DOWNLOAD_SETTINGS_TEXT()}.
|
||||
</Typography>
|
||||
<Grid container spacing={1}>
|
||||
<Button
|
||||
sx={{ ml: 2 }}
|
||||
startIcon={<DownloadIcon />}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => sendExportData('settings')}
|
||||
>
|
||||
{LL.SETTINGS_OF(LL.APPLICATION())}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
sx={{ ml: 2 }}
|
||||
startIcon={<DownloadIcon />}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => sendExportData('customizations')}
|
||||
>
|
||||
{LL.CUSTOMIZATIONS()}
|
||||
</Button>
|
||||
<Button
|
||||
sx={{ ml: 2 }}
|
||||
startIcon={<DownloadIcon />}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => sendExportData('entities')}
|
||||
>
|
||||
{LL.CUSTOM_ENTITIES(0)}
|
||||
</Button>
|
||||
<Button
|
||||
sx={{ ml: 2 }}
|
||||
startIcon={<DownloadIcon />}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => sendExportData('schedule')}
|
||||
>
|
||||
{LL.SCHEDULE(0)}
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
|
||||
{LL.UPLOAD()}
|
||||
</Typography>
|
||||
|
||||
<Box color="warning.main" sx={{ pb: 2 }}>
|
||||
<Typography variant="body1">{LL.UPLOAD_TEXT()}.</Typography>
|
||||
</Box>
|
||||
|
||||
<SingleUpload doRestart={doRestart} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent>{restarting ? <RestartMonitor /> : content()}</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default DownloadUpload;
|
||||
@@ -1,27 +1,36 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
import { Button, Checkbox, MenuItem, Grid, Typography, InputAdornment, TextField } from '@mui/material';
|
||||
import { useState } from 'react';
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import type { FC } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
InputAdornment,
|
||||
MenuItem,
|
||||
TextField,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
import Grid from '@mui/material/Grid2';
|
||||
|
||||
import type { MqttSettings } from 'types';
|
||||
import * as MqttApi from 'api/mqtt';
|
||||
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import {
|
||||
BlockFormControlLabel,
|
||||
BlockNavigation,
|
||||
ButtonRow,
|
||||
FormLoader,
|
||||
SectionContent,
|
||||
ValidatedPasswordField,
|
||||
ValidatedTextField,
|
||||
BlockNavigation
|
||||
useLayoutTitle
|
||||
} from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import type { MqttSettingsType } from 'types';
|
||||
import { numberValue, updateValueDirty, useRest } from 'utils';
|
||||
|
||||
import { createMqttSettingsValidator, validate } from 'validators';
|
||||
|
||||
const MqttSettingsForm: FC = () => {
|
||||
const MqttSettings = () => {
|
||||
const {
|
||||
loadData,
|
||||
saving,
|
||||
@@ -33,16 +42,26 @@ const MqttSettingsForm: FC = () => {
|
||||
blocker,
|
||||
saveData,
|
||||
errorMessage
|
||||
} = useRest<MqttSettings>({
|
||||
} = useRest<MqttSettingsType>({
|
||||
read: MqttApi.readMqttSettings,
|
||||
update: MqttApi.updateMqttSettings
|
||||
});
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
useLayoutTitle(LL.SETTINGS_OF('MQTT'));
|
||||
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
|
||||
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue);
|
||||
const updateFormValue = updateValueDirty(
|
||||
origData,
|
||||
dirtyFlags,
|
||||
setDirtyFlags,
|
||||
updateDataValue
|
||||
);
|
||||
|
||||
const SecondsInputProps = {
|
||||
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||
};
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
@@ -54,36 +73,41 @@ const MqttSettingsForm: FC = () => {
|
||||
setFieldErrors(undefined);
|
||||
await validate(createMqttSettingsValidator(data), data);
|
||||
await saveData();
|
||||
} catch (errors: any) {
|
||||
setFieldErrors(errors);
|
||||
} catch (error) {
|
||||
setFieldErrors(error as ValidateFieldsError);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox name="enabled" checked={data.enabled} onChange={updateFormValue} />}
|
||||
control={
|
||||
<Checkbox
|
||||
name="enabled"
|
||||
checked={data.enabled}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
}
|
||||
label={LL.ENABLE_MQTT()}
|
||||
/>
|
||||
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Grid container spacing={2} rowSpacing={0}>
|
||||
<Grid>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="host"
|
||||
label={LL.ADDRESS_OF(LL.BROKER())}
|
||||
fullWidth
|
||||
multiline
|
||||
variant="outlined"
|
||||
value={data.host}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Grid>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="port"
|
||||
label="Port"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.port)}
|
||||
type="number"
|
||||
@@ -91,60 +115,55 @@ const MqttSettingsForm: FC = () => {
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Grid>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="base"
|
||||
label={LL.BASE_TOPIC()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.base}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Grid>
|
||||
<TextField
|
||||
name="client_id"
|
||||
label={LL.ID_OF(LL.CLIENT()) + ' (' + LL.OPTIONAL() + ')'}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.client_id}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Grid>
|
||||
<TextField
|
||||
name="username"
|
||||
label={LL.USERNAME(0)}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.username}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Grid>
|
||||
<ValidatedPasswordField
|
||||
name="password"
|
||||
label={LL.PASSWORD()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.password}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Grid>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="keep_alive"
|
||||
label="Keep Alive"
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||
slotProps={{
|
||||
input: SecondsInputProps
|
||||
}}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.keep_alive)}
|
||||
type="number"
|
||||
@@ -152,12 +171,11 @@ const MqttSettingsForm: FC = () => {
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Grid>
|
||||
<TextField
|
||||
name="mqtt_qos"
|
||||
label="QoS"
|
||||
value={data.mqtt_qos}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
@@ -168,30 +186,49 @@ const MqttSettingsForm: FC = () => {
|
||||
<MenuItem value={2}>2</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
{data.rootCA !== undefined && (
|
||||
<Grid item xs={12} sm={6}>
|
||||
<ValidatedPasswordField
|
||||
name="rootCA"
|
||||
label={LL.CERT()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.rootCA}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
{data.enableTLS !== undefined && (
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name="enableTLS"
|
||||
checked={data.enableTLS}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
}
|
||||
label={LL.ENABLE_TLS()}
|
||||
/>
|
||||
)}
|
||||
{data.enableTLS === true && (
|
||||
<ValidatedPasswordField
|
||||
name="rootCA"
|
||||
label={LL.CERT()}
|
||||
variant="outlined"
|
||||
value={data.rootCA}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
)}
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox name="clean_session" checked={data.clean_session} onChange={updateFormValue} />}
|
||||
control={
|
||||
<Checkbox
|
||||
name="clean_session"
|
||||
checked={data.clean_session}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
}
|
||||
label={LL.MQTT_CLEAN_SESSION()}
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox name="mqtt_retain" checked={data.mqtt_retain} onChange={updateFormValue} />}
|
||||
control={
|
||||
<Checkbox
|
||||
name="mqtt_retain"
|
||||
checked={data.mqtt_retain}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
}
|
||||
label={LL.MQTT_RETAIN_FLAG()}
|
||||
/>
|
||||
|
||||
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
||||
{LL.FORMATTING()}
|
||||
</Typography>
|
||||
@@ -199,7 +236,6 @@ const MqttSettingsForm: FC = () => {
|
||||
name="nested_format"
|
||||
label={LL.MQTT_FORMAT()}
|
||||
value={data.nested_format}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
@@ -209,29 +245,38 @@ const MqttSettingsForm: FC = () => {
|
||||
<MenuItem value={2}>{LL.MQTT_NEST_2()}</MenuItem>
|
||||
</TextField>
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox name="send_response" checked={data.send_response} onChange={updateFormValue} />}
|
||||
control={
|
||||
<Checkbox
|
||||
name="send_response"
|
||||
checked={data.send_response}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
}
|
||||
label={LL.MQTT_RESPONSE()}
|
||||
/>
|
||||
{!data.ha_enabled && (
|
||||
<Grid
|
||||
container
|
||||
rowSpacing={-1}
|
||||
spacing={1}
|
||||
direction="row"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
<Grid item>
|
||||
<Grid container spacing={2} rowSpacing={0}>
|
||||
<Grid>
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox name="publish_single" checked={data.publish_single} onChange={updateFormValue} />}
|
||||
control={
|
||||
<Checkbox
|
||||
name="publish_single"
|
||||
checked={data.publish_single}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
}
|
||||
label={LL.MQTT_PUBLISH_TEXT_1()}
|
||||
/>
|
||||
</Grid>
|
||||
{data.publish_single && (
|
||||
<Grid item>
|
||||
<Grid>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox name="publish_single2cmd" checked={data.publish_single2cmd} onChange={updateFormValue} />
|
||||
<Checkbox
|
||||
name="publish_single2cmd"
|
||||
checked={data.publish_single2cmd}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
}
|
||||
label={LL.MQTT_PUBLISH_TEXT_2()}
|
||||
/>
|
||||
@@ -240,28 +285,26 @@ const MqttSettingsForm: FC = () => {
|
||||
</Grid>
|
||||
)}
|
||||
{!data.publish_single && (
|
||||
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
||||
<Grid item>
|
||||
<Grid container spacing={2} rowSpacing={0}>
|
||||
<Grid>
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox name="ha_enabled" checked={data.ha_enabled} onChange={updateFormValue} />}
|
||||
control={
|
||||
<Checkbox
|
||||
name="ha_enabled"
|
||||
checked={data.ha_enabled}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
}
|
||||
label={LL.MQTT_PUBLISH_TEXT_3()}
|
||||
/>
|
||||
</Grid>
|
||||
{data.ha_enabled && (
|
||||
<Grid
|
||||
container
|
||||
sx={{ pl: 1 }}
|
||||
spacing={1}
|
||||
direction="row"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<Grid container spacing={2} rowSpacing={0}>
|
||||
<Grid>
|
||||
<TextField
|
||||
name="discovery_type"
|
||||
label={LL.MQTT_PUBLISH_TEXT_5()}
|
||||
value={data.discovery_type}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
@@ -269,31 +312,36 @@ const MqttSettingsForm: FC = () => {
|
||||
>
|
||||
<MenuItem value={0}>Home Assistant</MenuItem>
|
||||
<MenuItem value={1}>Domoticz</MenuItem>
|
||||
<MenuItem value={2}>Domoticz (latest)</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<Grid>
|
||||
<TextField
|
||||
name="discovery_prefix"
|
||||
label={LL.MQTT_PUBLISH_TEXT_4()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.discovery_prefix}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<Grid>
|
||||
<TextField
|
||||
name="entity_format"
|
||||
label={LL.MQTT_ENTITY_FORMAT()}
|
||||
value={data.entity_format}
|
||||
fullWidth
|
||||
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.6)
|
||||
</MenuItem>
|
||||
<MenuItem value={4}>
|
||||
{LL.MQTT_ENTITY_FORMAT_2()} (v3.6)
|
||||
</MenuItem>
|
||||
<MenuItem value={1}>{LL.MQTT_ENTITY_FORMAT_1()}</MenuItem>
|
||||
<MenuItem value={2}>{LL.MQTT_ENTITY_FORMAT_2()}</MenuItem>
|
||||
</TextField>
|
||||
@@ -305,16 +353,15 @@ const MqttSettingsForm: FC = () => {
|
||||
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
||||
{LL.MQTT_PUBLISH_INTERVALS()} (0=auto)
|
||||
</Typography>
|
||||
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<Grid container spacing={2} rowSpacing={0}>
|
||||
<Grid>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="publish_time_heartbeat"
|
||||
label="Heartbeat"
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||
slotProps={{
|
||||
input: SecondsInputProps
|
||||
}}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.publish_time_heartbeat)}
|
||||
type="number"
|
||||
@@ -322,105 +369,112 @@ const MqttSettingsForm: FC = () => {
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<Grid>
|
||||
<TextField
|
||||
name="publish_time_boiler"
|
||||
label={LL.MQTT_INT_BOILER()}
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||
}}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.publish_time_boiler)}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
slotProps={{
|
||||
input: SecondsInputProps
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<Grid>
|
||||
<TextField
|
||||
name="publish_time_thermostat"
|
||||
label={LL.MQTT_INT_THERMOSTATS()}
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||
}}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.publish_time_thermostat)}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
slotProps={{
|
||||
input: SecondsInputProps
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<Grid>
|
||||
<TextField
|
||||
name="publish_time_solar"
|
||||
label={LL.MQTT_INT_SOLAR()}
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||
}}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.publish_time_solar)}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
slotProps={{
|
||||
input: SecondsInputProps
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<Grid>
|
||||
<TextField
|
||||
name="publish_time_mixer"
|
||||
label={LL.MQTT_INT_MIXER()}
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||
}}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.publish_time_mixer)}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
slotProps={{
|
||||
input: SecondsInputProps
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<Grid>
|
||||
<TextField
|
||||
name="publish_time_water"
|
||||
label={LL.MQTT_INT_WATER()}
|
||||
variant="outlined"
|
||||
value={numberValue(data.publish_time_water)}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
slotProps={{
|
||||
input: SecondsInputProps
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<TextField
|
||||
name="publish_time_sensor"
|
||||
label={LL.TEMP_SENSORS()}
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||
}}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.publish_time_sensor)}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
slotProps={{
|
||||
input: SecondsInputProps
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<Grid>
|
||||
<TextField
|
||||
name="publish_time_other"
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||
}}
|
||||
label={LL.DEFAULT(0)}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.publish_time_other)}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
slotProps={{
|
||||
input: SecondsInputProps
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{dirtyFlags && dirtyFlags.length !== 0 && (
|
||||
<ButtonRow>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
disabled={saving}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
color="secondary"
|
||||
type="submit"
|
||||
onClick={loadData}
|
||||
>
|
||||
@@ -443,11 +497,11 @@ const MqttSettingsForm: FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.SETTINGS_OF('MQTT')} titleGutter>
|
||||
<SectionContent>
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default MqttSettingsForm;
|
||||
export default MqttSettings;
|
||||
@@ -1,29 +1,32 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
import { Button, Checkbox, MenuItem } from '@mui/material';
|
||||
// eslint-disable-next-line import/named
|
||||
import { updateState } from 'alova';
|
||||
import { useState } from 'react';
|
||||
import { selectedTimeZone, timeZoneSelectItems, TIME_ZONES } from './TZ';
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import type { NTPSettings } from 'types';
|
||||
import * as NTPApi from 'api/ntp';
|
||||
import { readNTPSettings } from 'api/ntp';
|
||||
|
||||
import { updateState } from 'alova/client';
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import {
|
||||
BlockFormControlLabel,
|
||||
BlockNavigation,
|
||||
ButtonRow,
|
||||
FormLoader,
|
||||
SectionContent,
|
||||
ValidatedTextField,
|
||||
BlockNavigation
|
||||
useLayoutTitle
|
||||
} from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import type { NTPSettingsType } from 'types';
|
||||
import { updateValueDirty, useRest } from 'utils';
|
||||
import { validate } from 'validators';
|
||||
import { NTP_SETTINGS_VALIDATOR } from 'validators/ntp';
|
||||
|
||||
const NTPSettingsForm: FC = () => {
|
||||
import { TIME_ZONES, selectedTimeZone, timeZoneSelectItems } from './TZ';
|
||||
|
||||
const NTPSettings = () => {
|
||||
const {
|
||||
loadData,
|
||||
saving,
|
||||
@@ -35,14 +38,20 @@ const NTPSettingsForm: FC = () => {
|
||||
blocker,
|
||||
saveData,
|
||||
errorMessage
|
||||
} = useRest<NTPSettings>({
|
||||
} = useRest<NTPSettingsType>({
|
||||
read: NTPApi.readNTPSettings,
|
||||
update: NTPApi.updateNTPSettings
|
||||
});
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
useLayoutTitle(LL.SETTINGS_OF('NTP'));
|
||||
|
||||
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue);
|
||||
const updateFormValue = updateValueDirty(
|
||||
origData,
|
||||
dirtyFlags,
|
||||
setDirtyFlags,
|
||||
updateDataValue
|
||||
);
|
||||
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
|
||||
@@ -56,15 +65,14 @@ const NTPSettingsForm: FC = () => {
|
||||
setFieldErrors(undefined);
|
||||
await validate(NTP_SETTINGS_VALIDATOR, data);
|
||||
await saveData();
|
||||
} catch (errors: any) {
|
||||
setFieldErrors(errors);
|
||||
} catch (error) {
|
||||
setFieldErrors(error as ValidateFieldsError);
|
||||
}
|
||||
};
|
||||
|
||||
const changeTimeZone = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
updateFormValue(event);
|
||||
|
||||
updateState('ntpSettings', (settings) => ({
|
||||
void updateState(readNTPSettings(), (settings: NTPSettingsType) => ({
|
||||
...settings,
|
||||
tz_label: event.target.value,
|
||||
tz_format: TIME_ZONES[event.target.value]
|
||||
@@ -74,7 +82,13 @@ const NTPSettingsForm: FC = () => {
|
||||
return (
|
||||
<>
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox name="enabled" checked={data.enabled} onChange={updateFormValue} />}
|
||||
control={
|
||||
<Checkbox
|
||||
name="enabled"
|
||||
checked={data.enabled}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
}
|
||||
label={LL.ENABLE_NTP()}
|
||||
/>
|
||||
<ValidatedTextField
|
||||
@@ -107,7 +121,7 @@ const NTPSettingsForm: FC = () => {
|
||||
startIcon={<CancelIcon />}
|
||||
disabled={saving}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
color="secondary"
|
||||
type="submit"
|
||||
onClick={loadData}
|
||||
>
|
||||
@@ -130,11 +144,11 @@ const NTPSettingsForm: FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.SETTINGS_OF('NTP')} titleGutter>
|
||||
<SectionContent>
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default NTPSettingsForm;
|
||||
export default NTPSettings;
|
||||
176
interface/src/app/settings/Settings.tsx
Normal file
176
interface/src/app/settings/Settings.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
||||
import BuildIcon from '@mui/icons-material/Build';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
|
||||
import ImportExportIcon from '@mui/icons-material/ImportExport';
|
||||
import LockIcon from '@mui/icons-material/Lock';
|
||||
import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore';
|
||||
import SettingsEthernetIcon from '@mui/icons-material/SettingsEthernet';
|
||||
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
|
||||
import TuneIcon from '@mui/icons-material/Tune';
|
||||
import ViewModuleIcon from '@mui/icons-material/ViewModule';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
List
|
||||
} from '@mui/material';
|
||||
|
||||
import { API, callAction } from 'api/app';
|
||||
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import { useRequest } from 'alova/client';
|
||||
import type { APIcall } from 'app/main/types';
|
||||
import { SectionContent, useLayoutTitle } from 'components';
|
||||
import ListMenuItem from 'components/layout/ListMenuItem';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
const Settings = () => {
|
||||
const { LL } = useI18nContext();
|
||||
useLayoutTitle(LL.SETTINGS(0));
|
||||
|
||||
const [confirmFactoryReset, setConfirmFactoryReset] = useState<boolean>(false);
|
||||
|
||||
const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
// call checkUpgrade with no param to fetch EMS-ESP version
|
||||
const { data } = useRequest(() => callAction({ action: 'checkUpgrade' }), {
|
||||
initialData: { emsesp_version: '...' }
|
||||
});
|
||||
|
||||
const doFormat = async () => {
|
||||
await sendAPI({ device: 'system', cmd: 'format', id: 0 }).then(() => {
|
||||
setConfirmFactoryReset(false);
|
||||
});
|
||||
};
|
||||
|
||||
const renderFactoryResetDialog = () => (
|
||||
<Dialog
|
||||
sx={dialogStyle}
|
||||
open={confirmFactoryReset}
|
||||
onClose={() => setConfirmFactoryReset(false)}
|
||||
>
|
||||
<DialogTitle>{LL.FACTORY_RESET()}</DialogTitle>
|
||||
<DialogContent dividers>{LL.SYSTEM_FACTORY_TEXT_DIALOG()}</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
onClick={() => setConfirmFactoryReset(false)}
|
||||
color="secondary"
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<SettingsBackupRestoreIcon />}
|
||||
variant="outlined"
|
||||
onClick={doFormat}
|
||||
color="error"
|
||||
>
|
||||
{LL.FACTORY_RESET()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
const content = () => (
|
||||
<>
|
||||
<List sx={{ borderRadius: 3, border: '2px solid grey' }}>
|
||||
<ListMenuItem
|
||||
icon={BuildIcon}
|
||||
bgcolor="#72caf9"
|
||||
label={LL.EMS_ESP_VER()}
|
||||
text={data.emsesp_version}
|
||||
to="version"
|
||||
/>
|
||||
|
||||
<ListMenuItem
|
||||
icon={TuneIcon}
|
||||
bgcolor="#134ba2"
|
||||
label={LL.APPLICATION()}
|
||||
text={LL.APPLICATION_SETTINGS_1()}
|
||||
to="application"
|
||||
/>
|
||||
|
||||
<ListMenuItem
|
||||
icon={SettingsEthernetIcon}
|
||||
bgcolor="#40828f"
|
||||
label={LL.NETWORK(0)}
|
||||
text={LL.CONFIGURE(LL.SETTINGS_OF(LL.NETWORK(1)))}
|
||||
to="network"
|
||||
/>
|
||||
|
||||
<ListMenuItem
|
||||
icon={SettingsInputAntennaIcon}
|
||||
bgcolor="#5f9a5f"
|
||||
label={LL.ACCESS_POINT(0)}
|
||||
text={LL.CONFIGURE(LL.ACCESS_POINT(1))}
|
||||
to="ap"
|
||||
/>
|
||||
|
||||
<ListMenuItem
|
||||
icon={AccessTimeIcon}
|
||||
bgcolor="#c5572c"
|
||||
label="NTP"
|
||||
text={LL.CONFIGURE(LL.LOCAL_TIME(1))}
|
||||
to="ntp"
|
||||
/>
|
||||
|
||||
<ListMenuItem
|
||||
icon={DeviceHubIcon}
|
||||
bgcolor="#68374d"
|
||||
label="MQTT"
|
||||
text={LL.CONFIGURE('MQTT')}
|
||||
to="mqtt"
|
||||
/>
|
||||
|
||||
<ListMenuItem
|
||||
icon={LockIcon}
|
||||
label={LL.SECURITY(0)}
|
||||
text={LL.SECURITY_1()}
|
||||
to="security"
|
||||
/>
|
||||
|
||||
<ListMenuItem
|
||||
icon={ViewModuleIcon}
|
||||
bgcolor="#efc34b"
|
||||
label={LL.MODULES()}
|
||||
text={LL.MODULES_1()}
|
||||
to="modules"
|
||||
/>
|
||||
|
||||
<ListMenuItem
|
||||
icon={ImportExportIcon}
|
||||
bgcolor="#5d89f7"
|
||||
label={LL.DOWNLOAD_UPLOAD()}
|
||||
text={LL.DOWNLOAD_UPLOAD_1()}
|
||||
to="upload"
|
||||
/>
|
||||
</List>
|
||||
|
||||
{renderFactoryResetDialog()}
|
||||
|
||||
<Box mt={2} display="flex" flexWrap="wrap">
|
||||
<Button
|
||||
startIcon={<SettingsBackupRestoreIcon />}
|
||||
variant="outlined"
|
||||
onClick={() => setConfirmFactoryReset(true)}
|
||||
color="error"
|
||||
>
|
||||
{LL.FACTORY_RESET()}
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
||||
return <SectionContent>{content()}</SectionContent>;
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
@@ -1,8 +1,6 @@
|
||||
import { MenuItem } from '@mui/material';
|
||||
|
||||
type TimeZones = {
|
||||
[name: string]: string;
|
||||
};
|
||||
type TimeZones = Record<string, string>;
|
||||
|
||||
export const TIME_ZONES: TimeZones = {
|
||||
'Africa/Abidjan': 'GMT0',
|
||||
277
interface/src/app/settings/Version.tsx
Normal file
277
interface/src/app/settings/Version.tsx
Normal file
@@ -0,0 +1,277 @@
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Divider,
|
||||
Link,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
import Grid from '@mui/material/Grid2';
|
||||
|
||||
import * as SystemApi from 'api/system';
|
||||
import { callAction } from 'api/app';
|
||||
import { getDevVersion, getStableVersion } from 'api/system';
|
||||
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import { useRequest } from 'alova/client';
|
||||
import RestartMonitor from 'app/status/RestartMonitor';
|
||||
import { FormLoader, SectionContent, useLayoutTitle } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
const Version = () => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const [restarting, setRestarting] = useState<boolean>(false);
|
||||
const [openDialog, setOpenDialog] = useState<boolean>(false);
|
||||
const [useDev, setUseDev] = useState<boolean>(false);
|
||||
const [upgradeAvailable, setUpgradeAvailable] = useState<boolean>(false);
|
||||
|
||||
const { send: sendCheckUpgrade } = useRequest(
|
||||
(version: string) => callAction({ action: 'checkUpgrade', param: version }),
|
||||
{
|
||||
immediate: false
|
||||
}
|
||||
).onSuccess((event) => {
|
||||
const data = event.data as { emsesp_version: string; upgradeable: boolean };
|
||||
setUpgradeAvailable(data.upgradeable);
|
||||
});
|
||||
|
||||
const { data, send: loadData, error } = useRequest(SystemApi.readSystemStatus);
|
||||
|
||||
const { send: sendUploadURL } = useRequest(
|
||||
(url: string) => callAction({ action: 'uploadURL', param: url }),
|
||||
{
|
||||
immediate: false
|
||||
}
|
||||
);
|
||||
|
||||
// called immediately to get the latest version, on page load
|
||||
const { data: latestVersion } = useRequest(getStableVersion, {
|
||||
// uncomment next 2 lines for testing, uses https://github.com/emsesp/EMS-ESP32/releases/download/v3.6.5/EMS-ESP-3_6_5-ESP32-16MB+.bin
|
||||
// immediate: false,
|
||||
// initialData: '3.6.5'
|
||||
});
|
||||
|
||||
// called immediately to get the latest version, on page load, then check for upgrade (works for both dev and stable)
|
||||
const { data: latestDevVersion } = useRequest(getDevVersion, {
|
||||
// uncomment next 2 lines for testing, uses https://github.com/emsesp/EMS-ESP32/releases/download/latest/EMS-ESP-3_7_0-dev_31-ESP32-16MB+.bin
|
||||
// immediate: false,
|
||||
// initialData: '3.7.0-dev.32'
|
||||
}).onSuccess((event) => {
|
||||
void sendCheckUpgrade(event.data);
|
||||
});
|
||||
|
||||
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';
|
||||
|
||||
const getBinURL = (useDevVersion: boolean) => {
|
||||
if (!latestVersion || !latestDevVersion) {
|
||||
return '';
|
||||
}
|
||||
const filename =
|
||||
'EMS-ESP-' +
|
||||
(useDevVersion ? latestDevVersion : latestVersion).replaceAll('.', '_') +
|
||||
'-' +
|
||||
getPlatform() +
|
||||
'.bin';
|
||||
return useDevVersion
|
||||
? DEV_URL + filename
|
||||
: STABLE_URL + 'v' + latestVersion + '/' + filename;
|
||||
};
|
||||
|
||||
const getPlatform = () => {
|
||||
return (
|
||||
[data.esp_platform, data.flash_chip_size >= 16384 ? '16MB' : '4MB'].join('-') +
|
||||
(data.psram ? '+' : '')
|
||||
);
|
||||
};
|
||||
|
||||
const installFirmwareURL = async (url: string) => {
|
||||
await sendUploadURL(url).catch((error: Error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
setRestarting(true);
|
||||
};
|
||||
|
||||
useLayoutTitle(LL.EMS_ESP_VER());
|
||||
|
||||
const internet_live =
|
||||
latestDevVersion !== undefined && latestVersion !== undefined;
|
||||
|
||||
const renderUploadDialog = () => {
|
||||
if (!internet_live) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
sx={dialogStyle}
|
||||
open={openDialog}
|
||||
onClose={() => setOpenDialog(false)}
|
||||
>
|
||||
<DialogTitle>
|
||||
{LL.INSTALL('') +
|
||||
' ' +
|
||||
(useDev ? LL.DEVELOPMENT() : LL.STABLE()) +
|
||||
' Firmware'}
|
||||
</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Typography mb={2}>
|
||||
{LL.INSTALL_VERSION(useDev ? latestDevVersion : latestVersion)}
|
||||
</Typography>
|
||||
<Link
|
||||
target="_blank"
|
||||
href={useDev ? DEV_RELNOTES_URL : STABLE_RELNOTES_URL}
|
||||
color="primary"
|
||||
>
|
||||
changelog
|
||||
</Link>
|
||||
|
|
||||
<Link target="_blank" href={getBinURL(useDev)} color="primary">
|
||||
{LL.DOWNLOAD(1)}
|
||||
</Link>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
onClick={() => setOpenDialog(false)}
|
||||
color="secondary"
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<WarningIcon color="warning" />}
|
||||
variant="outlined"
|
||||
onClick={() => installFirmwareURL(getBinURL(useDev))}
|
||||
color="primary"
|
||||
>
|
||||
{LL.INSTALL('')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
// useDevVersion = true to force using the dev version
|
||||
const showFirmwareDialog = (useDevVersion: boolean) => {
|
||||
if (useDevVersion || data.emsesp_version.includes('dev')) {
|
||||
setUseDev(true);
|
||||
}
|
||||
setOpenDialog(true);
|
||||
};
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
|
||||
}
|
||||
|
||||
const isDev = data.emsesp_version.includes('dev');
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box p={2} border="1px solid grey" borderRadius={2}>
|
||||
<Grid container spacing={3}>
|
||||
<Grid mb={1}>
|
||||
<Typography mb={1} fontWeight={'fontWeightBold'}>
|
||||
{LL.VERSION()}
|
||||
</Typography>
|
||||
<Typography mb={1} fontWeight={'fontWeightBold'}>
|
||||
Platform
|
||||
</Typography>
|
||||
<Typography mb={1} fontWeight={'fontWeightBold'}>
|
||||
Release
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<Typography mb={1}>
|
||||
{data.emsesp_version}
|
||||
{data.build_flags && (
|
||||
<Typography variant="caption">
|
||||
({data.build_flags})
|
||||
</Typography>
|
||||
)}
|
||||
</Typography>
|
||||
<Typography mb={1}>{getPlatform()}</Typography>
|
||||
<Typography>
|
||||
{isDev ? LL.DEVELOPMENT() : LL.STABLE()}
|
||||
<Link
|
||||
target="_blank"
|
||||
href={useDev ? DEV_RELNOTES_URL : STABLE_RELNOTES_URL}
|
||||
color="primary"
|
||||
>
|
||||
(changelog)
|
||||
</Link>
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Divider />
|
||||
|
||||
{!isDev && (
|
||||
<Button
|
||||
sx={{ mt: 2 }}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
size="small"
|
||||
onClick={() => showFirmwareDialog(true)}
|
||||
>
|
||||
{LL.SWITCH_DEV()}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Typography mt={2} color="warning">
|
||||
<InfoOutlinedIcon color="warning" sx={{ verticalAlign: 'middle' }} />
|
||||
|
||||
{upgradeAvailable ? LL.UPGRADE_AVAILABLE() : LL.LATEST_VERSION()}
|
||||
{upgradeAvailable &&
|
||||
internet_live &&
|
||||
(data.psram ? (
|
||||
<Button
|
||||
sx={{ ml: 2, textTransform: 'none' }}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
size="small"
|
||||
onClick={() => showFirmwareDialog(false)}
|
||||
>
|
||||
{isDev
|
||||
? LL.INSTALL('v' + latestDevVersion)
|
||||
: LL.INSTALL('v' + latestVersion)}
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
|
||||
<Link target="_blank" href={getBinURL(isDev)} color="primary">
|
||||
{LL.DOWNLOAD(1)} v
|
||||
{isDev ? latestDevVersion : latestVersion}
|
||||
</Link>
|
||||
</>
|
||||
))}
|
||||
</Typography>
|
||||
|
||||
{renderUploadDialog()}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent>{restarting ? <RestartMonitor /> : content()}</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default Version;
|
||||
57
interface/src/app/settings/network/Network.tsx
Normal file
57
interface/src/app/settings/network/Network.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Navigate, Route, Routes, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { Tab } from '@mui/material';
|
||||
|
||||
import { RouterTabs, useLayoutTitle, useRouterTab } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import type { WiFiNetwork } from 'types';
|
||||
|
||||
import NetworkSettings from './NetworkSettings';
|
||||
import { WiFiConnectionContext } from './WiFiConnectionContext';
|
||||
import WiFiNetworkScanner from './WiFiNetworkScanner';
|
||||
|
||||
const Network = () => {
|
||||
const { LL } = useI18nContext();
|
||||
useLayoutTitle(LL.SETTINGS_OF(LL.NETWORK(0)));
|
||||
|
||||
const { routerTab } = useRouterTab();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [selectedNetwork, setSelectedNetwork] = useState<WiFiNetwork>();
|
||||
|
||||
const selectNetwork = useCallback(
|
||||
(network: WiFiNetwork) => {
|
||||
setSelectedNetwork(network);
|
||||
navigate('settings');
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
|
||||
const deselectNetwork = useCallback(() => {
|
||||
setSelectedNetwork(undefined);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<WiFiConnectionContext.Provider
|
||||
value={{
|
||||
selectedNetwork,
|
||||
selectNetwork,
|
||||
deselectNetwork
|
||||
}}
|
||||
>
|
||||
<RouterTabs value={routerTab}>
|
||||
<Tab value="settings" label={LL.SETTINGS_OF(LL.NETWORK(1))} />
|
||||
<Tab value="scan" label={LL.NETWORK_SCAN()} />
|
||||
</RouterTabs>
|
||||
<Routes>
|
||||
<Route path="scan" element={<WiFiNetworkScanner />} />
|
||||
<Route path="settings" element={<NetworkSettings />} />
|
||||
<Route path="*" element={<Navigate replace to="settings" />} />
|
||||
</Routes>
|
||||
</WiFiConnectionContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default Network;
|
||||
@@ -1,3 +1,6 @@
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import LockIcon from '@mui/icons-material/Lock';
|
||||
@@ -12,43 +15,39 @@ import {
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemSecondaryAction,
|
||||
ListItemText,
|
||||
Typography,
|
||||
InputAdornment,
|
||||
TextField
|
||||
MenuItem,
|
||||
TextField,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
// eslint-disable-next-line import/named
|
||||
import { updateState, useRequest } from 'alova';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import RestartMonitor from '../system/RestartMonitor';
|
||||
import { WiFiConnectionContext } from './WiFiConnectionContext';
|
||||
import { isNetworkOpen, networkSecurityMode } from './WiFiNetworkSelector';
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import type { NetworkSettings } from 'types';
|
||||
import * as NetworkApi from 'api/network';
|
||||
import * as SystemApi from 'api/system';
|
||||
import { API } from 'api/app';
|
||||
|
||||
import { updateState, useRequest } from 'alova/client';
|
||||
import type { APIcall } from 'app/main/types';
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import {
|
||||
BlockFormControlLabel,
|
||||
BlockNavigation,
|
||||
ButtonRow,
|
||||
FormLoader,
|
||||
MessageBox,
|
||||
SectionContent,
|
||||
ValidatedPasswordField,
|
||||
ValidatedTextField,
|
||||
MessageBox,
|
||||
BlockNavigation
|
||||
ValidatedTextField
|
||||
} from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
import { numberValue, updateValueDirty, useRest } from 'utils';
|
||||
|
||||
import type { NetworkSettingsType } from 'types';
|
||||
import { updateValueDirty, useRest } from 'utils';
|
||||
import { validate } from 'validators';
|
||||
import { createNetworkSettingsValidator } from 'validators/network';
|
||||
|
||||
const WiFiSettingsForm: FC = () => {
|
||||
import RestartMonitor from '../../status/RestartMonitor';
|
||||
import { WiFiConnectionContext } from './WiFiConnectionContext';
|
||||
import { isNetworkOpen, networkSecurityMode } from './WiFiNetworkSelector';
|
||||
|
||||
const NetworkSettings = () => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const { selectedNetwork, deselectNetwork } = useContext(WiFiConnectionContext);
|
||||
@@ -68,38 +67,45 @@ const WiFiSettingsForm: FC = () => {
|
||||
saveData,
|
||||
errorMessage,
|
||||
restartNeeded
|
||||
} = useRest<NetworkSettings>({
|
||||
} = useRest<NetworkSettingsType>({
|
||||
read: NetworkApi.readNetworkSettings,
|
||||
update: NetworkApi.updateNetworkSettings
|
||||
});
|
||||
|
||||
const { send: restartCommand } = useRequest(SystemApi.restart(), {
|
||||
const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialized && data) {
|
||||
if (selectedNetwork) {
|
||||
updateState('networkSettings', (current_data) => ({
|
||||
ssid: selectedNetwork.ssid,
|
||||
bssid: selectedNetwork.bssid,
|
||||
password: current_data ? current_data.password : '',
|
||||
hostname: current_data?.hostname,
|
||||
static_ip_config: false,
|
||||
enableIPv6: false,
|
||||
bandwidth20: false,
|
||||
tx_power: 20,
|
||||
nosleep: false,
|
||||
enableMDNS: true,
|
||||
enableCORS: false,
|
||||
CORSOrigin: '*'
|
||||
}));
|
||||
void updateState(
|
||||
NetworkApi.readNetworkSettings(),
|
||||
(current_data: NetworkSettingsType) => ({
|
||||
ssid: selectedNetwork.ssid,
|
||||
bssid: selectedNetwork.bssid,
|
||||
password: current_data ? current_data.password : '',
|
||||
hostname: current_data?.hostname,
|
||||
static_ip_config: false,
|
||||
bandwidth20: false,
|
||||
tx_power: 0,
|
||||
nosleep: false,
|
||||
enableMDNS: true,
|
||||
enableCORS: false,
|
||||
CORSOrigin: '*'
|
||||
})
|
||||
);
|
||||
}
|
||||
setInitialized(true);
|
||||
}
|
||||
}, [initialized, setInitialized, data, selectedNetwork]);
|
||||
|
||||
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue);
|
||||
const updateFormValue = updateValueDirty(
|
||||
origData,
|
||||
dirtyFlags,
|
||||
setDirtyFlags,
|
||||
updateDataValue
|
||||
);
|
||||
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
|
||||
@@ -115,8 +121,8 @@ const WiFiSettingsForm: FC = () => {
|
||||
setFieldErrors(undefined);
|
||||
await validate(createNetworkSettingsValidator(data), data);
|
||||
await saveData();
|
||||
} catch (errors: any) {
|
||||
setFieldErrors(errors);
|
||||
} catch (error) {
|
||||
setFieldErrors(error as ValidateFieldsError);
|
||||
}
|
||||
deselectNetwork();
|
||||
};
|
||||
@@ -126,23 +132,27 @@ const WiFiSettingsForm: FC = () => {
|
||||
await loadData();
|
||||
};
|
||||
|
||||
const restart = async () => {
|
||||
await restartCommand().catch((error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
const doRestart = async () => {
|
||||
setRestarting(true);
|
||||
await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch(
|
||||
(error: Error) => {
|
||||
toast.error(error.message);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
||||
<Typography variant="h6" color="primary">
|
||||
WiFi
|
||||
</Typography>
|
||||
{selectedNetwork ? (
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>{isNetworkOpen(selectedNetwork) ? <LockOpenIcon /> : <LockIcon />}</Avatar>
|
||||
<Avatar>
|
||||
{isNetworkOpen(selectedNetwork) ? <LockOpenIcon /> : <LockIcon />}
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={selectedNetwork.ssid}
|
||||
@@ -155,11 +165,9 @@ const WiFiSettingsForm: FC = () => {
|
||||
selectedNetwork.bssid
|
||||
}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<IconButton onClick={setCancel}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</ListItemSecondaryAction>
|
||||
<IconButton onClick={setCancel}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</ListItem>
|
||||
</List>
|
||||
) : (
|
||||
@@ -196,26 +204,47 @@ const WiFiSettingsForm: FC = () => {
|
||||
margin="normal"
|
||||
/>
|
||||
)}
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
<TextField
|
||||
name="tx_power"
|
||||
label={LL.TX_POWER()}
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">dBm</InputAdornment>
|
||||
}}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.tx_power)}
|
||||
value={data.tx_power}
|
||||
onChange={updateFormValue}
|
||||
type="number"
|
||||
margin="normal"
|
||||
/>
|
||||
select
|
||||
>
|
||||
<MenuItem value={0}>Auto</MenuItem>
|
||||
<MenuItem value={78}>19.5 dBm</MenuItem>
|
||||
<MenuItem value={76}>19 dBm</MenuItem>
|
||||
<MenuItem value={74}>18.5 dBm</MenuItem>
|
||||
<MenuItem value={68}>17 dBm</MenuItem>
|
||||
<MenuItem value={60}>15 dBm</MenuItem>
|
||||
<MenuItem value={52}>13 dBm</MenuItem>
|
||||
<MenuItem value={44}>11 dBm</MenuItem>
|
||||
<MenuItem value={34}>8.5 dBm</MenuItem>
|
||||
<MenuItem value={28}>7 dBm</MenuItem>
|
||||
<MenuItem value={20}>5 dBm</MenuItem>
|
||||
<MenuItem value={8}>2 dBm</MenuItem>
|
||||
</TextField>
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox name="nosleep" checked={data.nosleep} onChange={updateFormValue} />}
|
||||
control={
|
||||
<Checkbox
|
||||
name="nosleep"
|
||||
checked={data.nosleep}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
}
|
||||
label={LL.NETWORK_DISABLE_SLEEP()}
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox name="bandwidth20" checked={data.bandwidth20} onChange={updateFormValue} />}
|
||||
control={
|
||||
<Checkbox
|
||||
name="bandwidth20"
|
||||
checked={data.bandwidth20}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
}
|
||||
label={LL.NETWORK_LOW_BAND()}
|
||||
/>
|
||||
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
||||
@@ -232,11 +261,23 @@ const WiFiSettingsForm: FC = () => {
|
||||
margin="normal"
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox name="enableMDNS" checked={data.enableMDNS} onChange={updateFormValue} />}
|
||||
control={
|
||||
<Checkbox
|
||||
name="enableMDNS"
|
||||
checked={data.enableMDNS}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
}
|
||||
label={LL.NETWORK_USE_DNS()}
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox name="enableCORS" checked={data.enableCORS} onChange={updateFormValue} />}
|
||||
control={
|
||||
<Checkbox
|
||||
name="enableCORS"
|
||||
checked={data.enableCORS}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
}
|
||||
label={LL.NETWORK_ENABLE_CORS()}
|
||||
/>
|
||||
{data.enableCORS && (
|
||||
@@ -251,11 +292,13 @@ const WiFiSettingsForm: FC = () => {
|
||||
/>
|
||||
)}
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox name="enableIPv6" checked={data.enableIPv6} onChange={updateFormValue} />}
|
||||
label={LL.NETWORK_ENABLE_IPV6()}
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox name="static_ip_config" checked={data.static_ip_config} onChange={updateFormValue} />}
|
||||
control={
|
||||
<Checkbox
|
||||
name="static_ip_config"
|
||||
checked={data.static_ip_config}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
}
|
||||
label={LL.NETWORK_FIXED_IP()}
|
||||
/>
|
||||
{data.static_ip_config && (
|
||||
@@ -313,47 +356,53 @@ const WiFiSettingsForm: FC = () => {
|
||||
</>
|
||||
)}
|
||||
{restartNeeded && (
|
||||
<MessageBox my={2} level="warning" message={LL.RESTART_TEXT()}>
|
||||
<Button startIcon={<PowerSettingsNewIcon />} variant="contained" color="error" onClick={restart}>
|
||||
<MessageBox my={2} level="warning" message={LL.RESTART_TEXT(0)}>
|
||||
<Button
|
||||
startIcon={<PowerSettingsNewIcon />}
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={doRestart}
|
||||
>
|
||||
{LL.RESTART()}
|
||||
</Button>
|
||||
</MessageBox>
|
||||
)}
|
||||
|
||||
{!restartNeeded && (selectedNetwork || (dirtyFlags && dirtyFlags.length !== 0)) && (
|
||||
<ButtonRow>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
disabled={saving}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
type="submit"
|
||||
onClick={loadData}
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<WarningIcon color="warning" />}
|
||||
disabled={saving}
|
||||
variant="contained"
|
||||
color="info"
|
||||
type="submit"
|
||||
onClick={validateAndSubmit}
|
||||
>
|
||||
{LL.APPLY_CHANGES(dirtyFlags.length)}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
)}
|
||||
{!restartNeeded &&
|
||||
(selectedNetwork || (dirtyFlags && dirtyFlags.length !== 0)) && (
|
||||
<ButtonRow>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
disabled={saving}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
type="submit"
|
||||
onClick={loadData}
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<WarningIcon color="warning" />}
|
||||
disabled={saving}
|
||||
variant="contained"
|
||||
color="info"
|
||||
type="submit"
|
||||
onClick={validateAndSubmit}
|
||||
>
|
||||
{LL.APPLY_CHANGES(dirtyFlags.length)}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.SETTINGS_OF(LL.NETWORK(1))} titleGutter>
|
||||
<SectionContent>
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
{restarting ? <RestartMonitor /> : content()}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default WiFiSettingsForm;
|
||||
export default NetworkSettings;
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
import type { WiFiNetwork } from 'types';
|
||||
|
||||
export interface WiFiConnectionContextValue {
|
||||
@@ -8,4 +9,6 @@ export interface WiFiConnectionContextValue {
|
||||
}
|
||||
|
||||
const WiFiConnectionContextDefaultValue = {} as WiFiConnectionContextValue;
|
||||
export const WiFiConnectionContext = createContext(WiFiConnectionContextDefaultValue);
|
||||
export const WiFiConnectionContext = createContext(
|
||||
WiFiConnectionContextDefaultValue
|
||||
);
|
||||
@@ -1,34 +1,41 @@
|
||||
import { useRef, useState } from 'react';
|
||||
|
||||
import PermScanWifiIcon from '@mui/icons-material/PermScanWifi';
|
||||
import { Button } from '@mui/material';
|
||||
// eslint-disable-next-line import/named
|
||||
import { updateState, useRequest } from 'alova';
|
||||
import { useState, useRef } from 'react';
|
||||
|
||||
import * as NetworkApi from 'api/network';
|
||||
|
||||
import { updateState, useRequest } from 'alova/client';
|
||||
import { ButtonRow, FormLoader, SectionContent } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
import WiFiNetworkSelector from './WiFiNetworkSelector';
|
||||
import type { FC } from 'react';
|
||||
import * as NetworkApi from 'api/network';
|
||||
import { ButtonRow, FormLoader, SectionContent } from 'components';
|
||||
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
const NUM_POLLS = 10;
|
||||
const POLLING_FREQUENCY = 1000;
|
||||
|
||||
const WiFiNetworkScanner: FC = () => {
|
||||
const WiFiNetworkScanner = () => {
|
||||
const pollCount = useRef(0);
|
||||
const { LL } = useI18nContext();
|
||||
const [errorMessage, setErrorMessage] = useState<string>();
|
||||
|
||||
const { send: scanNetworks, onComplete: onCompleteScanNetworks } = useRequest(NetworkApi.scanNetworks); // is called on page load to start network scan
|
||||
const {
|
||||
data: networkList,
|
||||
send: getNetworkList,
|
||||
onSuccess: onSuccessNetworkList
|
||||
} = useRequest(NetworkApi.listNetworks, {
|
||||
immediate: false
|
||||
});
|
||||
// is called on page load to start network scan
|
||||
const { send: scanNetworks } = useRequest(NetworkApi.scanNetworks).onComplete(
|
||||
() => {
|
||||
pollCount.current = 0;
|
||||
setErrorMessage(undefined);
|
||||
void updateState(NetworkApi.listNetworks(), () => undefined);
|
||||
void getNetworkList();
|
||||
}
|
||||
);
|
||||
|
||||
onSuccessNetworkList((event) => {
|
||||
const { data: networkList, send: getNetworkList } = useRequest(
|
||||
NetworkApi.listNetworks,
|
||||
{
|
||||
immediate: false
|
||||
}
|
||||
).onSuccess((event) => {
|
||||
// is called when network scan is completed
|
||||
if (!event.data) {
|
||||
const completedPollCount = pollCount.current + 1;
|
||||
if (completedPollCount < NUM_POLLS) {
|
||||
@@ -41,22 +48,17 @@ const WiFiNetworkScanner: FC = () => {
|
||||
}
|
||||
});
|
||||
|
||||
onCompleteScanNetworks(() => {
|
||||
pollCount.current = 0;
|
||||
setErrorMessage(undefined);
|
||||
updateState('listNetworks', () => undefined);
|
||||
void getNetworkList();
|
||||
});
|
||||
|
||||
const renderNetworkScanner = () => {
|
||||
if (!networkList) {
|
||||
return <FormLoader message={LL.SCANNING() + '...'} errorMessage={errorMessage} />;
|
||||
return (
|
||||
<FormLoader message={LL.SCANNING() + '...'} errorMessage={errorMessage} />
|
||||
);
|
||||
}
|
||||
return <WiFiNetworkSelector networkList={networkList} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.NETWORK_SCANNER()}>
|
||||
<SectionContent>
|
||||
{renderNetworkScanner()}
|
||||
<ButtonRow>
|
||||
<Button
|
||||
@@ -1,21 +1,26 @@
|
||||
import { useContext } from 'react';
|
||||
|
||||
import LockIcon from '@mui/icons-material/Lock';
|
||||
import LockOpenIcon from '@mui/icons-material/LockOpen';
|
||||
import WifiIcon from '@mui/icons-material/Wifi';
|
||||
import { Avatar, Badge, List, ListItem, ListItemAvatar, ListItemIcon, ListItemText, useTheme } from '@mui/material';
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { WiFiConnectionContext } from './WiFiConnectionContext';
|
||||
import {
|
||||
Avatar,
|
||||
Badge,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import type { Theme } from '@mui/material';
|
||||
import type { FC } from 'react';
|
||||
import type { WiFiNetwork, WiFiNetworkList } from 'types';
|
||||
import { MessageBox } from 'components';
|
||||
|
||||
import { MessageBox } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import type { WiFiNetwork, WiFiNetworkList } from 'types';
|
||||
import { WiFiEncryptionType } from 'types';
|
||||
|
||||
interface WiFiNetworkSelectorProps {
|
||||
networkList: WiFiNetworkList;
|
||||
}
|
||||
import { WiFiConnectionContext } from './WiFiConnectionContext';
|
||||
|
||||
export const isNetworkOpen = ({ encryption_type }: WiFiNetwork) =>
|
||||
encryption_type === WiFiEncryptionType.WIFI_AUTH_OPEN;
|
||||
@@ -39,7 +44,7 @@ export const networkSecurityMode = ({ encryption_type }: WiFiNetwork) => {
|
||||
case WiFiEncryptionType.WIFI_AUTH_WPA2_WPA3_PSK:
|
||||
return 'WPA2/WPA3';
|
||||
default:
|
||||
return 'Unknown: ' + encryption_type;
|
||||
return 'Unknown: ' + String(encryption_type);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -52,21 +57,29 @@ const networkQualityHighlight = ({ rssi }: WiFiNetwork, theme: Theme) => {
|
||||
return theme.palette.success.main;
|
||||
};
|
||||
|
||||
const WiFiNetworkSelector: FC<WiFiNetworkSelectorProps> = ({ networkList }) => {
|
||||
const WiFiNetworkSelector = ({ networkList }: { networkList: WiFiNetworkList }) => {
|
||||
const { LL } = useI18nContext();
|
||||
const theme = useTheme();
|
||||
|
||||
const wifiConnectionContext = useContext(WiFiConnectionContext);
|
||||
|
||||
const renderNetwork = (network: WiFiNetwork) => (
|
||||
<ListItem key={network.bssid} onClick={() => wifiConnectionContext.selectNetwork(network)}>
|
||||
<ListItem
|
||||
key={network.bssid}
|
||||
onClick={() => wifiConnectionContext.selectNetwork(network)}
|
||||
>
|
||||
<ListItemAvatar>
|
||||
<Avatar>{isNetworkOpen(network) ? <LockOpenIcon /> : <LockIcon />}</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={network.ssid}
|
||||
secondary={
|
||||
'Security: ' + networkSecurityMode(network) + ', Ch: ' + network.channel + ', bssid: ' + network.bssid
|
||||
'Security: ' +
|
||||
networkSecurityMode(network) +
|
||||
', Ch: ' +
|
||||
network.channel +
|
||||
', bssid: ' +
|
||||
network.bssid
|
||||
}
|
||||
/>
|
||||
<ListItemIcon>
|
||||
@@ -1,23 +1,23 @@
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Box,
|
||||
LinearProgress,
|
||||
Typography,
|
||||
TextField,
|
||||
Button
|
||||
} from '@mui/material';
|
||||
import { useRequest } from 'alova';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import type { FC } from 'react';
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import * as SecurityApi from 'api/security';
|
||||
import { MessageBox } from 'components';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
LinearProgress,
|
||||
TextField,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
|
||||
import * as SecurityApi from 'api/security';
|
||||
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import { useRequest } from 'alova/client';
|
||||
import { MessageBox } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
interface GenerateTokenProps {
|
||||
@@ -25,29 +25,44 @@ interface GenerateTokenProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const GenerateToken: FC<GenerateTokenProps> = ({ username, onClose }) => {
|
||||
const GenerateToken = ({ username, onClose }: GenerateTokenProps) => {
|
||||
const { LL } = useI18nContext();
|
||||
const open = !!username;
|
||||
|
||||
const { data: token, send: generateToken } = useRequest(SecurityApi.generateToken(username), {
|
||||
immediate: false
|
||||
});
|
||||
const { data: token, send: generateToken } = useRequest(
|
||||
SecurityApi.generateToken(username),
|
||||
{
|
||||
immediate: false
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
void generateToken();
|
||||
}
|
||||
}, [open, generateToken]);
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<Dialog sx={dialogStyle} onClose={onClose} open={!!username} fullWidth maxWidth="sm">
|
||||
<Dialog
|
||||
sx={dialogStyle}
|
||||
onClose={onClose}
|
||||
open={!!username}
|
||||
fullWidth
|
||||
maxWidth="sm"
|
||||
>
|
||||
<DialogTitle>{LL.ACCESS_TOKEN_FOR() + ' ' + username}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
{token ? (
|
||||
<>
|
||||
<MessageBox message={LL.ACCESS_TOKEN_TEXT()} level="info" my={2} />
|
||||
<Box mt={2} mb={2}>
|
||||
<TextField label="Token" multiline value={token.token} fullWidth contentEditable={false} />
|
||||
<TextField
|
||||
label="Token"
|
||||
multiline
|
||||
value={token.token}
|
||||
fullWidth
|
||||
contentEditable={false}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
@@ -58,7 +73,12 @@ const GenerateToken: FC<GenerateTokenProps> = ({ username, onClose }) => {
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button startIcon={<CloseIcon />} variant="outlined" onClick={onClose} color="secondary">
|
||||
<Button
|
||||
startIcon={<CloseIcon />}
|
||||
variant="outlined"
|
||||
onClick={onClose}
|
||||
color="secondary"
|
||||
>
|
||||
{LL.CLOSE()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
@@ -1,3 +1,6 @@
|
||||
import { useContext, useState } from 'react';
|
||||
import { useBlocker } from 'react-router-dom';
|
||||
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import CheckIcon from '@mui/icons-material/Check';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
@@ -6,31 +9,44 @@ import EditIcon from '@mui/icons-material/Edit';
|
||||
import PersonAddIcon from '@mui/icons-material/PersonAdd';
|
||||
import VpnKeyIcon from '@mui/icons-material/VpnKey';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
import { Button, IconButton, Box } from '@mui/material';
|
||||
import { Box, Button, IconButton } from '@mui/material';
|
||||
|
||||
import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table';
|
||||
import { useTheme } from '@table-library/react-table-library/theme';
|
||||
import { useContext, useState } from 'react';
|
||||
|
||||
import { useBlocker } from 'react-router-dom';
|
||||
import GenerateToken from './GenerateToken';
|
||||
import UserForm from './UserForm';
|
||||
import type { FC } from 'react';
|
||||
import type { SecuritySettings, User } from 'types';
|
||||
import * as SecurityApi from 'api/security';
|
||||
import { ButtonRow, FormLoader, MessageBox, SectionContent, BlockNavigation } from 'components';
|
||||
|
||||
import {
|
||||
Body,
|
||||
Cell,
|
||||
Header,
|
||||
HeaderCell,
|
||||
HeaderRow,
|
||||
Row,
|
||||
Table
|
||||
} from '@table-library/react-table-library/table';
|
||||
import { useTheme } from '@table-library/react-table-library/theme';
|
||||
import {
|
||||
BlockNavigation,
|
||||
ButtonRow,
|
||||
FormLoader,
|
||||
MessageBox,
|
||||
SectionContent
|
||||
} from 'components';
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import type { SecuritySettingsType, UserType } from 'types';
|
||||
import { useRest } from 'utils';
|
||||
import { createUserValidator } from 'validators';
|
||||
|
||||
const ManageUsersForm: FC = () => {
|
||||
const { loadData, saveData, saving, data, updateDataValue, errorMessage } = useRest<SecuritySettings>({
|
||||
read: SecurityApi.readSecuritySettings,
|
||||
update: SecurityApi.updateSecuritySettings
|
||||
});
|
||||
import GenerateToken from './GenerateToken';
|
||||
import User from './User';
|
||||
|
||||
const [user, setUser] = useState<User>();
|
||||
const ManageUsers = () => {
|
||||
const { loadData, saveData, saving, data, updateDataValue, errorMessage } =
|
||||
useRest<SecuritySettingsType>({
|
||||
read: SecurityApi.readSecuritySettings,
|
||||
update: SecurityApi.updateSecuritySettings
|
||||
});
|
||||
|
||||
const [user, setUser] = useState<UserType>();
|
||||
const [creating, setCreating] = useState<boolean>(false);
|
||||
const [changed, setChanged] = useState<number>(0);
|
||||
const [generatingToken, setGeneratingToken] = useState<string>();
|
||||
@@ -86,7 +102,7 @@ const ManageUsersForm: FC = () => {
|
||||
|
||||
const noAdminConfigured = () => !data.users.find((u) => u.admin);
|
||||
|
||||
const removeUser = (toRemove: User) => {
|
||||
const removeUser = (toRemove: UserType) => {
|
||||
const users = data.users.filter((u) => u.username !== toRemove.username);
|
||||
updateDataValue({ ...data, users });
|
||||
setChanged(changed + 1);
|
||||
@@ -101,7 +117,7 @@ const ManageUsersForm: FC = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const editUser = (toEdit: User) => {
|
||||
const editUser = (toEdit: UserType) => {
|
||||
setCreating(false);
|
||||
setUser({ ...toEdit });
|
||||
};
|
||||
@@ -112,7 +128,12 @@ const ManageUsersForm: FC = () => {
|
||||
|
||||
const doneEditingUser = () => {
|
||||
if (user) {
|
||||
const users = [...data.users.filter((u: { username: string }) => u.username !== user.username), user];
|
||||
const users = [
|
||||
...data.users.filter(
|
||||
(u: { username: string }) => u.username !== user.username
|
||||
),
|
||||
user
|
||||
];
|
||||
updateDataValue({ ...data, users });
|
||||
setUser(undefined);
|
||||
setChanged(changed + 1);
|
||||
@@ -138,12 +159,27 @@ const ManageUsersForm: FC = () => {
|
||||
setChanged(0);
|
||||
};
|
||||
|
||||
const user_table = data.users.map((u) => ({ ...u, id: u.username }));
|
||||
interface UserType2 {
|
||||
id: string;
|
||||
username: string;
|
||||
password: string;
|
||||
admin: boolean;
|
||||
}
|
||||
|
||||
// add id to the type, needed for the table
|
||||
const user_table = data.users.map((u) => ({
|
||||
...u,
|
||||
id: u.username
|
||||
})) as UserType2[];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table data={{ nodes: user_table }} theme={table_theme} layout={{ custom: true }}>
|
||||
{(tableList: any) => (
|
||||
<Table
|
||||
data={{ nodes: user_table }}
|
||||
theme={table_theme}
|
||||
layout={{ custom: true }}
|
||||
>
|
||||
{(tableList: UserType2[]) => (
|
||||
<>
|
||||
<Header>
|
||||
<HeaderRow>
|
||||
@@ -153,7 +189,7 @@ const ManageUsersForm: FC = () => {
|
||||
</HeaderRow>
|
||||
</Header>
|
||||
<Body>
|
||||
{tableList.map((u: any) => (
|
||||
{tableList.map((u: UserType2) => (
|
||||
<Row key={u.id} item={u}>
|
||||
<Cell>{u.username}</Cell>
|
||||
<Cell stiff>{u.admin ? <CheckIcon /> : <CloseIcon />}</Cell>
|
||||
@@ -179,17 +215,19 @@ const ManageUsersForm: FC = () => {
|
||||
)}
|
||||
</Table>
|
||||
|
||||
{noAdminConfigured() && <MessageBox level="warning" message={LL.USER_WARNING()} my={2} />}
|
||||
{noAdminConfigured() && (
|
||||
<MessageBox level="warning" message={LL.USER_WARNING()} my={2} />
|
||||
)}
|
||||
|
||||
<Box display="flex" flexWrap="wrap">
|
||||
<Box flexGrow={1} sx={{ '& button': { mt: 2 } }}>
|
||||
{changed !== 0 && (
|
||||
{changed !== 0 && (
|
||||
<Box flexGrow={1} sx={{ '& button': { mt: 2 } }}>
|
||||
<ButtonRow>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
disabled={saving}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
color="secondary"
|
||||
type="submit"
|
||||
onClick={onCancelSubmit}
|
||||
>
|
||||
@@ -206,12 +244,16 @@ const ManageUsersForm: FC = () => {
|
||||
{LL.APPLY_CHANGES(changed)}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
</Box>
|
||||
)}
|
||||
<Box flexWrap="nowrap" whiteSpace="nowrap">
|
||||
<ButtonRow>
|
||||
<Button startIcon={<PersonAddIcon />} variant="outlined" color="secondary" onClick={createUser}>
|
||||
<Button
|
||||
startIcon={<PersonAddIcon />}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={createUser}
|
||||
>
|
||||
{LL.ADD(0)}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
@@ -219,7 +261,7 @@ const ManageUsersForm: FC = () => {
|
||||
</Box>
|
||||
|
||||
<GenerateToken username={generatingToken} onClose={closeGenerateToken} />
|
||||
<UserForm
|
||||
<User
|
||||
user={user}
|
||||
setUser={setUser}
|
||||
creating={creating}
|
||||
@@ -232,11 +274,11 @@ const ManageUsersForm: FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.MANAGE_USERS()} titleGutter>
|
||||
<SectionContent>
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManageUsersForm;
|
||||
export default ManageUsers;
|
||||
32
interface/src/app/settings/security/Security.tsx
Normal file
32
interface/src/app/settings/security/Security.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import { Tab } from '@mui/material';
|
||||
|
||||
import { RouterTabs, useLayoutTitle, useRouterTab } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
import ManageUsers from './ManageUsers';
|
||||
import SecuritySettings from './SecuritySettings';
|
||||
|
||||
const Security = () => {
|
||||
const { LL } = useI18nContext();
|
||||
useLayoutTitle(LL.SETTINGS_OF(LL.SECURITY(0)));
|
||||
|
||||
const { routerTab } = useRouterTab();
|
||||
|
||||
return (
|
||||
<>
|
||||
<RouterTabs value={routerTab}>
|
||||
<Tab value="settings" label={LL.SETTINGS_OF(LL.SECURITY(1))} />
|
||||
<Tab value="users" label={LL.MANAGE_USERS()} />
|
||||
</RouterTabs>
|
||||
<Routes>
|
||||
<Route path="users" element={<ManageUsers />} />
|
||||
<Route path="settings" element={<SecuritySettings />} />
|
||||
<Route path="*" element={<Navigate replace to="settings" />} />
|
||||
</Routes>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Security;
|
||||
@@ -1,20 +1,27 @@
|
||||
import { useContext, useState } from 'react';
|
||||
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
import { Button } from '@mui/material';
|
||||
import { useContext, useState } from 'react';
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import type { SecuritySettings } from 'types';
|
||||
import * as SecurityApi from 'api/security';
|
||||
import { ButtonRow, FormLoader, MessageBox, SectionContent, ValidatedPasswordField, BlockNavigation } from 'components';
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import {
|
||||
BlockNavigation,
|
||||
ButtonRow,
|
||||
FormLoader,
|
||||
MessageBox,
|
||||
SectionContent,
|
||||
ValidatedPasswordField
|
||||
} from 'components';
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import type { SecuritySettingsType } from 'types';
|
||||
import { updateValueDirty, useRest } from 'utils';
|
||||
import { SECURITY_SETTINGS_VALIDATOR, validate } from 'validators';
|
||||
|
||||
const SecuritySettingsForm: FC = () => {
|
||||
const SecuritySettings = () => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
@@ -29,14 +36,19 @@ const SecuritySettingsForm: FC = () => {
|
||||
blocker,
|
||||
saveData,
|
||||
errorMessage
|
||||
} = useRest<SecuritySettings>({
|
||||
} = useRest<SecuritySettingsType>({
|
||||
read: SecurityApi.readSecuritySettings,
|
||||
update: SecurityApi.updateSecuritySettings
|
||||
});
|
||||
|
||||
const authenticatedContext = useContext(AuthenticatedContext);
|
||||
|
||||
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue);
|
||||
const updateFormValue = updateValueDirty(
|
||||
origData,
|
||||
dirtyFlags,
|
||||
setDirtyFlags,
|
||||
updateDataValue
|
||||
);
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
@@ -49,8 +61,8 @@ const SecuritySettingsForm: FC = () => {
|
||||
await validate(SECURITY_SETTINGS_VALIDATOR, data);
|
||||
await saveData();
|
||||
await authenticatedContext.refresh();
|
||||
} catch (errors: any) {
|
||||
setFieldErrors(errors);
|
||||
} catch (error) {
|
||||
setFieldErrors(error as ValidateFieldsError);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -73,7 +85,7 @@ const SecuritySettingsForm: FC = () => {
|
||||
startIcon={<CancelIcon />}
|
||||
disabled={saving}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
color="secondary"
|
||||
type="submit"
|
||||
onClick={loadData}
|
||||
>
|
||||
@@ -96,11 +108,11 @@ const SecuritySettingsForm: FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.SETTINGS_OF(LL.SECURITY(1))} titleGutter>
|
||||
<SectionContent>
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default SecuritySettingsForm;
|
||||
export default SecuritySettings;
|
||||
@@ -1,32 +1,48 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import PersonAddIcon from '@mui/icons-material/PersonAdd';
|
||||
import SaveIcon from '@mui/icons-material/Save';
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle
|
||||
} from '@mui/material';
|
||||
|
||||
import { Button, Checkbox, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import type Schema from 'async-validator';
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import type { User } from 'types';
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import { BlockFormControlLabel, ValidatedPasswordField, ValidatedTextField } from 'components';
|
||||
import {
|
||||
BlockFormControlLabel,
|
||||
ValidatedPasswordField,
|
||||
ValidatedTextField
|
||||
} from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import type { UserType } from 'types';
|
||||
import { updateValue } from 'utils';
|
||||
import { validate } from 'validators';
|
||||
|
||||
interface UserFormProps {
|
||||
creating: boolean;
|
||||
validator: Schema;
|
||||
|
||||
user?: User;
|
||||
setUser: React.Dispatch<React.SetStateAction<User | undefined>>;
|
||||
|
||||
user?: UserType;
|
||||
setUser: React.Dispatch<React.SetStateAction<UserType | undefined>>;
|
||||
onDoneEditing: () => void;
|
||||
onCancelEditing: () => void;
|
||||
}
|
||||
|
||||
const UserForm: FC<UserFormProps> = ({ creating, validator, user, setUser, onDoneEditing, onCancelEditing }) => {
|
||||
const User: FC<UserFormProps> = ({
|
||||
creating,
|
||||
validator,
|
||||
user,
|
||||
setUser,
|
||||
onDoneEditing,
|
||||
onCancelEditing
|
||||
}) => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const updateFormValue = updateValue(setUser);
|
||||
@@ -45,14 +61,20 @@ const UserForm: FC<UserFormProps> = ({ creating, validator, user, setUser, onDon
|
||||
setFieldErrors(undefined);
|
||||
await validate(validator, user);
|
||||
onDoneEditing();
|
||||
} catch (errors: any) {
|
||||
setFieldErrors(errors);
|
||||
} catch (error) {
|
||||
setFieldErrors(error as ValidateFieldsError);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog sx={dialogStyle} onClose={onCancelEditing} open={!!user} fullWidth maxWidth="sm">
|
||||
<Dialog
|
||||
sx={dialogStyle}
|
||||
onClose={onCancelEditing}
|
||||
open={!!user}
|
||||
fullWidth
|
||||
maxWidth="sm"
|
||||
>
|
||||
{user && (
|
||||
<>
|
||||
<DialogTitle id="user-form-dialog-title">
|
||||
@@ -81,12 +103,23 @@ const UserForm: FC<UserFormProps> = ({ creating, validator, user, setUser, onDon
|
||||
margin="normal"
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox name="admin" checked={user.admin} onChange={updateFormValue} />}
|
||||
control={
|
||||
<Checkbox
|
||||
name="admin"
|
||||
checked={user.admin}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
}
|
||||
label={LL.IS_ADMIN(1)}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button startIcon={<CancelIcon />} variant="outlined" onClick={onCancelEditing} color="secondary">
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
onClick={onCancelEditing}
|
||||
color="secondary"
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
@@ -104,4 +137,4 @@ const UserForm: FC<UserFormProps> = ({ creating, validator, user, setUser, onDon
|
||||
);
|
||||
};
|
||||
|
||||
export default UserForm;
|
||||
export default User;
|
||||
112
interface/src/app/status/APStatus.tsx
Normal file
112
interface/src/app/status/APStatus.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import ComputerIcon from '@mui/icons-material/Computer';
|
||||
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
|
||||
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
|
||||
import {
|
||||
Avatar,
|
||||
Divider,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import type { Theme } from '@mui/material';
|
||||
|
||||
import * as APApi from 'api/ap';
|
||||
|
||||
import { useAutoRequest } from 'alova/client';
|
||||
import { FormLoader, SectionContent, useLayoutTitle } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import type { APStatusType } from 'types';
|
||||
import { APNetworkStatus } from 'types';
|
||||
|
||||
export const apStatusHighlight = ({ status }: APStatusType, theme: Theme) => {
|
||||
switch (status) {
|
||||
case APNetworkStatus.ACTIVE:
|
||||
return theme.palette.success.main;
|
||||
case APNetworkStatus.INACTIVE:
|
||||
return theme.palette.info.main;
|
||||
case APNetworkStatus.LINGERING:
|
||||
return theme.palette.warning.main;
|
||||
default:
|
||||
return theme.palette.warning.main;
|
||||
}
|
||||
};
|
||||
|
||||
const APStatus = () => {
|
||||
const {
|
||||
data,
|
||||
send: loadData,
|
||||
error
|
||||
} = useAutoRequest(APApi.readAPStatus, { pollingTime: 3000 });
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
useLayoutTitle(LL.STATUS_OF(LL.ACCESS_POINT(0)));
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const apStatus = ({ status }: APStatusType) => {
|
||||
switch (status) {
|
||||
case APNetworkStatus.ACTIVE:
|
||||
return LL.ACTIVE();
|
||||
case APNetworkStatus.INACTIVE:
|
||||
return LL.INACTIVE(0);
|
||||
case APNetworkStatus.LINGERING:
|
||||
return 'Lingering until idle';
|
||||
default:
|
||||
return LL.UNKNOWN();
|
||||
}
|
||||
};
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: apStatusHighlight(data, theme) }}>
|
||||
<SettingsInputAntennaIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={LL.STATUS_OF('')} secondary={apStatus(data)} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>IP</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={LL.ADDRESS_OF('IP')} secondary={data.ip_address} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<DeviceHubIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={LL.ADDRESS_OF('MAC')}
|
||||
secondary={data.mac_address}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<ComputerIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={LL.AP_CLIENTS()} secondary={data.station_num} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
return <SectionContent>{content()}</SectionContent>;
|
||||
};
|
||||
|
||||
export default APStatus;
|
||||
127
interface/src/app/status/Activity.tsx
Normal file
127
interface/src/app/status/Activity.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import {
|
||||
Body,
|
||||
Cell,
|
||||
Header,
|
||||
HeaderCell,
|
||||
HeaderRow,
|
||||
Row,
|
||||
Table
|
||||
} from '@table-library/react-table-library/table';
|
||||
import { useTheme as tableTheme } from '@table-library/react-table-library/theme';
|
||||
import { useAutoRequest } from 'alova/client';
|
||||
import { FormLoader, SectionContent, useLayoutTitle } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import type { Translation } from 'i18n/i18n-types';
|
||||
|
||||
import { readActivity } from '../../api/app';
|
||||
import type { Stat } from '../main/types';
|
||||
|
||||
const SystemActivity = () => {
|
||||
const {
|
||||
data,
|
||||
send: loadData,
|
||||
error
|
||||
} = useAutoRequest(readActivity, { pollingTime: 3000 });
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
useLayoutTitle(LL.DATA_TRAFFIC());
|
||||
|
||||
const stats_theme = tableTheme({
|
||||
Table: `
|
||||
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) 90px 90px 80px;
|
||||
`,
|
||||
BaseRow: `
|
||||
font-size: 14px;
|
||||
`,
|
||||
HeaderRow: `
|
||||
text-transform: uppercase;
|
||||
background-color: black;
|
||||
color: #90CAF9;
|
||||
|
||||
.th {
|
||||
height: 36px;
|
||||
border-bottom: 1px solid #565656;
|
||||
}
|
||||
`,
|
||||
Row: `
|
||||
.td {
|
||||
padding: 8px;
|
||||
border-top: 1px solid #565656;
|
||||
border-bottom: 1px solid #565656;
|
||||
}
|
||||
|
||||
&:nth-of-type(odd) .td {
|
||||
background-color: #303030;
|
||||
}
|
||||
&:nth-of-type(even) .td {
|
||||
background-color: #1e1e1e;
|
||||
}
|
||||
`,
|
||||
BaseCell: `
|
||||
&:not(:first-of-type) {
|
||||
text-align: center;
|
||||
}
|
||||
`
|
||||
});
|
||||
|
||||
const showName = (id: number) => {
|
||||
const name: keyof Translation['STATUS_NAMES'] = id;
|
||||
return LL.STATUS_NAMES[name]();
|
||||
};
|
||||
|
||||
const showQuality = (stat: Stat) => {
|
||||
if (stat.q === 0 || stat.s + stat.f === 0) {
|
||||
return;
|
||||
}
|
||||
if (stat.q === 100) {
|
||||
return <div style={{ color: '#00FF7F' }}>{stat.q}%</div>;
|
||||
}
|
||||
if (stat.q >= 95) {
|
||||
return <div style={{ color: 'orange' }}>{stat.q}%</div>;
|
||||
} else {
|
||||
return <div style={{ color: 'red' }}>{stat.q}%</div>;
|
||||
}
|
||||
};
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Table
|
||||
data={{ nodes: data.stats }}
|
||||
theme={stats_theme}
|
||||
layout={{ custom: true }}
|
||||
>
|
||||
{(tableList: Stat[]) => (
|
||||
<>
|
||||
<Header>
|
||||
<HeaderRow>
|
||||
<HeaderCell resize />
|
||||
<HeaderCell stiff>{LL.SUCCESS()}</HeaderCell>
|
||||
<HeaderCell stiff>{LL.FAIL()}</HeaderCell>
|
||||
<HeaderCell stiff>{LL.QUALITY()}</HeaderCell>
|
||||
</HeaderRow>
|
||||
</Header>
|
||||
<Body>
|
||||
{tableList.map((stat: Stat) => (
|
||||
<Row key={stat.id} item={stat}>
|
||||
<Cell>{showName(stat.id)}</Cell>
|
||||
<Cell stiff>{Intl.NumberFormat().format(stat.s)}</Cell>
|
||||
<Cell stiff>{Intl.NumberFormat().format(stat.f)}</Cell>
|
||||
<Cell stiff>{showQuality(stat)}</Cell>
|
||||
</Row>
|
||||
))}
|
||||
</Body>
|
||||
</>
|
||||
)}
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
||||
return <SectionContent>{content()}</SectionContent>;
|
||||
};
|
||||
|
||||
export default SystemActivity;
|
||||
206
interface/src/app/status/HardwareStatus.tsx
Normal file
206
interface/src/app/status/HardwareStatus.tsx
Normal file
@@ -0,0 +1,206 @@
|
||||
import AppsIcon from '@mui/icons-material/Apps';
|
||||
import DeveloperBoardIcon from '@mui/icons-material/DeveloperBoard';
|
||||
import DevicesIcon from '@mui/icons-material/Devices';
|
||||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
import MemoryIcon from '@mui/icons-material/Memory';
|
||||
import SdCardAlertIcon from '@mui/icons-material/SdCardAlert';
|
||||
import SdStorageIcon from '@mui/icons-material/SdStorage';
|
||||
import TapAndPlayIcon from '@mui/icons-material/TapAndPlay';
|
||||
import {
|
||||
Avatar,
|
||||
Divider,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText
|
||||
} from '@mui/material';
|
||||
|
||||
import * as SystemApi from 'api/system';
|
||||
|
||||
import { useAutoRequest } from 'alova/client';
|
||||
import { FormLoader, SectionContent, useLayoutTitle } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
import BBQKeesIcon from './bbqkees.svg';
|
||||
|
||||
function formatNumber(num: number) {
|
||||
return new Intl.NumberFormat().format(num);
|
||||
}
|
||||
|
||||
const HardwareStatus = () => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
useLayoutTitle(LL.STATUS_OF(LL.HARDWARE()));
|
||||
|
||||
const {
|
||||
data,
|
||||
send: loadData,
|
||||
error
|
||||
} = useAutoRequest(SystemApi.readSystemStatus, { pollingTime: 3000 });
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
{data.model ? (
|
||||
<Avatar sx={{ bgcolor: '#003289', color: 'white' }}>
|
||||
<img
|
||||
alt="BBQKees"
|
||||
src={BBQKeesIcon}
|
||||
style={{ width: 16, verticalAlign: 'middle' }}
|
||||
/>
|
||||
</Avatar>
|
||||
) : (
|
||||
<Avatar sx={{ bgcolor: '#5f9a5f', color: 'white' }}>
|
||||
<TapAndPlayIcon />
|
||||
</Avatar>
|
||||
)}
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={LL.HARDWARE() + ' ' + LL.DEVICE()}
|
||||
secondary={data.model ? data.model : data.cpu_type}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: '#5f9a5f', color: 'white' }}>
|
||||
<DevicesIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary="SDK"
|
||||
secondary={data.arduino_version + ' / ESP-IDF ' + data.sdk_version}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: '#5f9a5f', color: 'white' }}>
|
||||
<DeveloperBoardIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary="CPU"
|
||||
secondary={
|
||||
data.esp_platform +
|
||||
'/' +
|
||||
data.cpu_type +
|
||||
' (rev.' +
|
||||
data.cpu_rev +
|
||||
', ' +
|
||||
(data.cpu_cores === 1 ? 'single-core)' : 'dual-core)') +
|
||||
' @ ' +
|
||||
data.cpu_freq_mhz +
|
||||
' Mhz' +
|
||||
(data.temperature ? ', T: ' + data.temperature + ' °C' : '')
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: '#5f9a5f', color: 'white' }}>
|
||||
<MemoryIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={LL.FREE_MEMORY()}
|
||||
secondary={
|
||||
formatNumber(data.free_heap) +
|
||||
' KB (' +
|
||||
formatNumber(data.max_alloc_heap) +
|
||||
' KB max alloc, ' +
|
||||
formatNumber(data.free_caps) +
|
||||
' KB caps)'
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
{data.psram_size !== undefined && data.free_psram !== undefined && (
|
||||
<>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: '#5f9a5f', color: 'white' }}>
|
||||
<AppsIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={LL.PSRAM()}
|
||||
secondary={
|
||||
formatNumber(data.psram_size) +
|
||||
' KB / ' +
|
||||
formatNumber(data.free_psram) +
|
||||
' KB'
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
</>
|
||||
)}
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: '#5f9a5f', color: 'white' }}>
|
||||
<SdStorageIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={LL.FLASH()}
|
||||
secondary={
|
||||
formatNumber(data.flash_chip_size) +
|
||||
' KB , ' +
|
||||
(data.flash_chip_speed / 1000000).toFixed(0) +
|
||||
' MHz'
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: '#5f9a5f', color: 'white' }}>
|
||||
<SdCardAlertIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={LL.APPSIZE()}
|
||||
secondary={
|
||||
data.partition +
|
||||
': ' +
|
||||
formatNumber(data.app_used) +
|
||||
' KB / ' +
|
||||
formatNumber(data.app_free) +
|
||||
' KB'
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: '#5f9a5f', color: 'white' }}>
|
||||
<FolderIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={LL.FILESYSTEM()}
|
||||
secondary={
|
||||
formatNumber(data.fs_used) +
|
||||
' KB / ' +
|
||||
formatNumber(data.fs_free) +
|
||||
' KB'
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
return <SectionContent>{content()}</SectionContent>;
|
||||
};
|
||||
|
||||
export default HardwareStatus;
|
||||
@@ -1,20 +1,30 @@
|
||||
import AutoAwesomeMotionIcon from '@mui/icons-material/AutoAwesomeMotion';
|
||||
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import ReportIcon from '@mui/icons-material/Report';
|
||||
import SpeakerNotesOffIcon from '@mui/icons-material/SpeakerNotesOff';
|
||||
import { Avatar, Button, Divider, List, ListItem, ListItemAvatar, ListItemText, useTheme } from '@mui/material';
|
||||
import { useRequest } from 'alova';
|
||||
import {
|
||||
Avatar,
|
||||
Divider,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import type { Theme } from '@mui/material';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import type { MqttStatus } from 'types';
|
||||
import * as MqttApi from 'api/mqtt';
|
||||
import { ButtonRow, FormLoader, SectionContent } from 'components';
|
||||
|
||||
import { useAutoRequest } from 'alova/client';
|
||||
import { FormLoader, SectionContent, useLayoutTitle } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import type { MqttStatusType } from 'types';
|
||||
import { MqttDisconnectReason } from 'types';
|
||||
|
||||
export const mqttStatusHighlight = ({ enabled, connected }: MqttStatus, theme: Theme) => {
|
||||
export const mqttStatusHighlight = (
|
||||
{ enabled, connected }: MqttStatusType,
|
||||
theme: Theme
|
||||
) => {
|
||||
if (!enabled) {
|
||||
return theme.palette.info.main;
|
||||
}
|
||||
@@ -24,27 +34,38 @@ export const mqttStatusHighlight = ({ enabled, connected }: MqttStatus, theme: T
|
||||
return theme.palette.error.main;
|
||||
};
|
||||
|
||||
export const mqttPublishHighlight = ({ mqtt_fails }: MqttStatus, theme: Theme) => {
|
||||
export const mqttPublishHighlight = (
|
||||
{ mqtt_fails }: MqttStatusType,
|
||||
theme: Theme
|
||||
) => {
|
||||
if (mqtt_fails === 0) return theme.palette.success.main;
|
||||
if (mqtt_fails < 10) return theme.palette.warning.main;
|
||||
|
||||
return theme.palette.error.main;
|
||||
};
|
||||
|
||||
export const mqttQueueHighlight = ({ mqtt_queued }: MqttStatus, theme: Theme) => {
|
||||
export const mqttQueueHighlight = (
|
||||
{ mqtt_queued }: MqttStatusType,
|
||||
theme: Theme
|
||||
) => {
|
||||
if (mqtt_queued <= 1) return theme.palette.success.main;
|
||||
|
||||
return theme.palette.warning.main;
|
||||
};
|
||||
|
||||
const MqttStatusForm: FC = () => {
|
||||
const { data: data, send: loadData, error } = useRequest(MqttApi.readMqttStatus);
|
||||
const MqttStatus = () => {
|
||||
const {
|
||||
data,
|
||||
send: loadData,
|
||||
error
|
||||
} = useAutoRequest(MqttApi.readMqttStatus, { pollingTime: 3000 });
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
useLayoutTitle(LL.STATUS_OF('MQTT'));
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const mqttStatus = ({ enabled, connected, connect_count }: MqttStatus) => {
|
||||
const mqttStatus = ({ enabled, connected, connect_count }: MqttStatusType) => {
|
||||
if (!enabled) {
|
||||
return LL.NOT_ENABLED();
|
||||
}
|
||||
@@ -54,7 +75,7 @@ const MqttStatusForm: FC = () => {
|
||||
return LL.DISCONNECTED() + (connect_count > 1 ? ' (' + connect_count + ')' : '');
|
||||
};
|
||||
|
||||
const disconnectReason = ({ disconnect_reason }: MqttStatus) => {
|
||||
const disconnectReason = ({ disconnect_reason }: MqttStatusType) => {
|
||||
switch (disconnect_reason) {
|
||||
case MqttDisconnectReason.TCP_DISCONNECTED:
|
||||
return 'TCP disconnected';
|
||||
@@ -90,7 +111,10 @@ const MqttStatusForm: FC = () => {
|
||||
<ReportIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={LL.DISCONNECT_REASON()} secondary={disconnectReason(data)} />
|
||||
<ListItemText
|
||||
primary={LL.DISCONNECT_REASON()}
|
||||
secondary={disconnectReason(data)}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
</>
|
||||
@@ -124,33 +148,22 @@ const MqttStatusForm: FC = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: mqttStatusHighlight(data, theme) }}>
|
||||
<DeviceHubIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={LL.STATUS_OF('')} secondary={mqttStatus(data)} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
{data.enabled && renderConnectionStatus()}
|
||||
</List>
|
||||
<ButtonRow>
|
||||
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
|
||||
{LL.REFRESH()}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
</>
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: mqttStatusHighlight(data, theme) }}>
|
||||
<DeviceHubIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={LL.STATUS_OF('')} secondary={mqttStatus(data)} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
{data.enabled && renderConnectionStatus()}
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.STATUS_OF('MQTT')} titleGutter>
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
return <SectionContent>{content()}</SectionContent>;
|
||||
};
|
||||
|
||||
export default MqttStatusForm;
|
||||
export default MqttStatus;
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import DnsIcon from '@mui/icons-material/Dns';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import SwapVerticalCircleIcon from '@mui/icons-material/SwapVerticalCircle';
|
||||
import UpdateIcon from '@mui/icons-material/Update';
|
||||
import {
|
||||
@@ -18,58 +20,64 @@ import {
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
TextField,
|
||||
useTheme,
|
||||
Typography
|
||||
Typography,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import { useRequest } from 'alova';
|
||||
import { useContext, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import type { Theme } from '@mui/material';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import type { NTPStatus } from 'types';
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import * as NTPApi from 'api/ntp';
|
||||
import { ButtonRow, FormLoader, SectionContent } from 'components';
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import { useAutoRequest, useRequest } from 'alova/client';
|
||||
import { ButtonRow, FormLoader, SectionContent, useLayoutTitle } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import type { NTPStatusType, Time } from 'types';
|
||||
import { NTPSyncStatus } from 'types';
|
||||
import { formatDateTime, formatLocalDateTime } from 'utils';
|
||||
|
||||
export const isNtpActive = ({ status }: NTPStatus) => status === NTPSyncStatus.NTP_ACTIVE;
|
||||
export const isNtpEnabled = ({ status }: NTPStatus) => status !== NTPSyncStatus.NTP_DISABLED;
|
||||
|
||||
export const ntpStatusHighlight = ({ status }: NTPStatus, theme: Theme) => {
|
||||
switch (status) {
|
||||
case NTPSyncStatus.NTP_DISABLED:
|
||||
return theme.palette.info.main;
|
||||
case NTPSyncStatus.NTP_INACTIVE:
|
||||
return theme.palette.error.main;
|
||||
case NTPSyncStatus.NTP_ACTIVE:
|
||||
return theme.palette.success.main;
|
||||
default:
|
||||
return theme.palette.error.main;
|
||||
}
|
||||
};
|
||||
|
||||
const NTPStatusForm: FC = () => {
|
||||
const { data: data, send: loadData, error } = useRequest(NTPApi.readNTPStatus);
|
||||
const NTPStatus = () => {
|
||||
const {
|
||||
data,
|
||||
send: loadData,
|
||||
error
|
||||
} = useAutoRequest(NTPApi.readNTPStatus, { pollingTime: 3000 });
|
||||
|
||||
const [localTime, setLocalTime] = useState<string>('');
|
||||
const [settingTime, setSettingTime] = useState<boolean>(false);
|
||||
const [processing, setProcessing] = useState<boolean>(false);
|
||||
const { me } = useContext(AuthenticatedContext);
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
useLayoutTitle(LL.STATUS_OF('NTP'));
|
||||
|
||||
const { send: updateTime } = useRequest((local_time) => NTPApi.updateTime(local_time), {
|
||||
immediate: false
|
||||
});
|
||||
const { send: updateTime } = useRequest(
|
||||
(local_time: Time) => NTPApi.updateTime(local_time),
|
||||
{
|
||||
immediate: false
|
||||
}
|
||||
);
|
||||
|
||||
NTPApi.updateTime;
|
||||
|
||||
const updateLocalTime = (event: React.ChangeEvent<HTMLInputElement>) => setLocalTime(event.target.value);
|
||||
const isNtpActive = ({ status }: NTPStatusType) =>
|
||||
status === NTPSyncStatus.NTP_ACTIVE;
|
||||
const isNtpEnabled = ({ status }: NTPStatusType) =>
|
||||
status !== NTPSyncStatus.NTP_DISABLED;
|
||||
|
||||
const ntpStatusHighlight = ({ status }: NTPStatusType, theme: Theme) => {
|
||||
switch (status) {
|
||||
case NTPSyncStatus.NTP_DISABLED:
|
||||
return theme.palette.info.main;
|
||||
case NTPSyncStatus.NTP_INACTIVE:
|
||||
return theme.palette.error.main;
|
||||
case NTPSyncStatus.NTP_ACTIVE:
|
||||
return theme.palette.success.main;
|
||||
default:
|
||||
return theme.palette.error.main;
|
||||
}
|
||||
};
|
||||
|
||||
const updateLocalTime = (event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setLocalTime(event.target.value);
|
||||
|
||||
const openSetTime = () => {
|
||||
setLocalTime(formatLocalDateTime(new Date()));
|
||||
@@ -78,7 +86,7 @@ const NTPStatusForm: FC = () => {
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const ntpStatus = ({ status }: NTPStatus) => {
|
||||
const ntpStatus = ({ status }: NTPStatusType) => {
|
||||
switch (status) {
|
||||
case NTPSyncStatus.NTP_DISABLED:
|
||||
return LL.NOT_ENABLED();
|
||||
@@ -109,26 +117,37 @@ const NTPStatusForm: FC = () => {
|
||||
};
|
||||
|
||||
const renderSetTimeDialog = () => (
|
||||
<Dialog sx={dialogStyle} open={settingTime} onClose={() => setSettingTime(false)}>
|
||||
<Dialog
|
||||
sx={dialogStyle}
|
||||
open={settingTime}
|
||||
onClose={() => setSettingTime(false)}
|
||||
>
|
||||
<DialogTitle>{LL.SET_TIME(1)}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Box color="warning.main" p={0} pl={0} pr={0} mt={0} mb={2}>
|
||||
<Typography variant="body2">{LL.SET_TIME_TEXT()}</Typography>
|
||||
</Box>
|
||||
<TextField
|
||||
label={LL.LOCAL_TIME()}
|
||||
label={LL.LOCAL_TIME(0)}
|
||||
type="datetime-local"
|
||||
value={localTime}
|
||||
onChange={updateLocalTime}
|
||||
disabled={processing}
|
||||
fullWidth
|
||||
InputLabelProps={{
|
||||
shrink: true
|
||||
slotProps={{
|
||||
inputLabel: {
|
||||
shrink: true
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button startIcon={<CancelIcon />} variant="outlined" onClick={() => setSettingTime(false)} color="secondary">
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
onClick={() => setSettingTime(false)}
|
||||
color="secondary"
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
@@ -180,7 +199,10 @@ const NTPStatusForm: FC = () => {
|
||||
<AccessTimeIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={LL.LOCAL_TIME()} secondary={formatDateTime(data.local_time)} />
|
||||
<ListItemText
|
||||
primary={LL.LOCAL_TIME(0)}
|
||||
secondary={formatDateTime(data.local_time)}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
@@ -189,22 +211,23 @@ const NTPStatusForm: FC = () => {
|
||||
<SwapVerticalCircleIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={LL.UTC_TIME()} secondary={formatDateTime(data.utc_time)} />
|
||||
<ListItemText
|
||||
primary={LL.UTC_TIME()}
|
||||
secondary={formatDateTime(data.utc_time)}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
</List>
|
||||
<Box display="flex" flexWrap="wrap">
|
||||
<Box flexGrow={1}>
|
||||
<ButtonRow>
|
||||
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
|
||||
{LL.REFRESH()}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
</Box>
|
||||
{me.admin && data && !isNtpActive(data) && (
|
||||
{data && !isNtpActive(data) && (
|
||||
<Box flexWrap="nowrap" whiteSpace="nowrap">
|
||||
<ButtonRow>
|
||||
<Button onClick={openSetTime} variant="outlined" color="primary" startIcon={<AccessTimeIcon />}>
|
||||
<Button
|
||||
onClick={openSetTime}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
startIcon={<AccessTimeIcon />}
|
||||
>
|
||||
{LL.SET_TIME(0)}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
@@ -216,11 +239,7 @@ const NTPStatusForm: FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.STATUS_OF('NTP')} titleGutter>
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
return <SectionContent>{content()}</SectionContent>;
|
||||
};
|
||||
|
||||
export default NTPStatusForm;
|
||||
export default NTPStatus;
|
||||
223
interface/src/app/status/NetworkStatus.tsx
Normal file
223
interface/src/app/status/NetworkStatus.tsx
Normal file
@@ -0,0 +1,223 @@
|
||||
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
|
||||
import DnsIcon from '@mui/icons-material/Dns';
|
||||
import GiteIcon from '@mui/icons-material/Gite';
|
||||
import RouterIcon from '@mui/icons-material/Router';
|
||||
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
|
||||
import SettingsInputComponentIcon from '@mui/icons-material/SettingsInputComponent';
|
||||
import WifiIcon from '@mui/icons-material/Wifi';
|
||||
import {
|
||||
Avatar,
|
||||
Divider,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import type { Theme } from '@mui/material';
|
||||
|
||||
import * as NetworkApi from 'api/network';
|
||||
|
||||
import { useAutoRequest } from 'alova/client';
|
||||
import { FormLoader, SectionContent, useLayoutTitle } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import type { NetworkStatusType } from 'types';
|
||||
import { NetworkConnectionStatus } from 'types';
|
||||
|
||||
const isConnected = ({ status }: NetworkStatusType) =>
|
||||
status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED ||
|
||||
status === NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED;
|
||||
|
||||
const networkStatusHighlight = ({ status }: NetworkStatusType, theme: Theme) => {
|
||||
switch (status) {
|
||||
case NetworkConnectionStatus.WIFI_STATUS_IDLE:
|
||||
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
|
||||
case NetworkConnectionStatus.WIFI_STATUS_NO_SHIELD:
|
||||
return theme.palette.info.main;
|
||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECTED:
|
||||
case NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED:
|
||||
return theme.palette.success.main;
|
||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECT_FAILED:
|
||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECTION_LOST:
|
||||
return theme.palette.error.main;
|
||||
default:
|
||||
return theme.palette.warning.main;
|
||||
}
|
||||
};
|
||||
|
||||
const networkQualityHighlight = ({ rssi }: NetworkStatusType, theme: Theme) => {
|
||||
if (rssi <= -85) {
|
||||
return theme.palette.error.main;
|
||||
} else if (rssi <= -75) {
|
||||
return theme.palette.warning.main;
|
||||
}
|
||||
return theme.palette.success.main;
|
||||
};
|
||||
|
||||
export const isWiFi = ({ status }: NetworkStatusType) =>
|
||||
status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED;
|
||||
export const isEthernet = ({ status }: NetworkStatusType) =>
|
||||
status === NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED;
|
||||
|
||||
const dnsServers = ({ dns_ip_1, dns_ip_2 }: NetworkStatusType) => {
|
||||
if (!dns_ip_1) {
|
||||
return 'none';
|
||||
}
|
||||
return dns_ip_1 + (!dns_ip_2 || dns_ip_2 === '0.0.0.0' ? '' : ', ' + dns_ip_2);
|
||||
};
|
||||
|
||||
const IPs = (status: NetworkStatusType) => {
|
||||
if (
|
||||
!status.local_ipv6 ||
|
||||
status.local_ipv6 === '0000:0000:0000:0000:0000:0000:0000:0000' ||
|
||||
status.local_ipv6 === '::'
|
||||
) {
|
||||
return status.local_ip;
|
||||
}
|
||||
if (!status.local_ip || status.local_ip === '0.0.0.0') {
|
||||
return status.local_ipv6;
|
||||
}
|
||||
return status.local_ip + ', ' + status.local_ipv6;
|
||||
};
|
||||
|
||||
const NetworkStatus = () => {
|
||||
const {
|
||||
data,
|
||||
send: loadData,
|
||||
error
|
||||
} = useAutoRequest(NetworkApi.readNetworkStatus, { pollingTime: 3000 });
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
useLayoutTitle(LL.STATUS_OF(LL.NETWORK(1)));
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const networkStatus = ({ status }: NetworkStatusType) => {
|
||||
switch (status) {
|
||||
case NetworkConnectionStatus.WIFI_STATUS_NO_SHIELD:
|
||||
return LL.INACTIVE(1);
|
||||
case NetworkConnectionStatus.WIFI_STATUS_IDLE:
|
||||
return LL.IDLE();
|
||||
case NetworkConnectionStatus.WIFI_STATUS_NO_SSID_AVAIL:
|
||||
return 'No SSID Available';
|
||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECTED:
|
||||
return LL.CONNECTED(0) + ' (WiFi)';
|
||||
case NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED:
|
||||
return LL.CONNECTED(0) + ' (Ethernet)';
|
||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECT_FAILED:
|
||||
return LL.CONNECTED(1) + ' ' + LL.FAILED(0);
|
||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECTION_LOST:
|
||||
return LL.CONNECTED(1) + ' ' + LL.LOST();
|
||||
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
|
||||
return LL.DISCONNECTED();
|
||||
default:
|
||||
return LL.UNKNOWN();
|
||||
}
|
||||
};
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: networkStatusHighlight(data, theme) }}>
|
||||
{isWiFi(data) && <WifiIcon />}
|
||||
{isEthernet(data) && <RouterIcon />}
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="Status" secondary={networkStatus(data)} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: networkStatusHighlight(data, theme) }}>
|
||||
<GiteIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={LL.HOSTNAME()} secondary={data.hostname} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
{isWiFi(data) && (
|
||||
<>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: networkQualityHighlight(data, theme) }}>
|
||||
<SettingsInputAntennaIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary="SSID (RSSI)"
|
||||
secondary={data.ssid + ' (' + data.rssi + ' dBm)'}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
</>
|
||||
)}
|
||||
{isConnected(data) && (
|
||||
<>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>IP</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={LL.ADDRESS_OF('IP')} secondary={IPs(data)} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<DeviceHubIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={LL.ADDRESS_OF('MAC')}
|
||||
secondary={data.mac_address}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>#</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={LL.NETWORK_SUBNET()}
|
||||
secondary={data.subnet_mask}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<SettingsInputComponentIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={LL.NETWORK_GATEWAY()}
|
||||
secondary={data.gateway_ip || 'none'}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<DnsIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={LL.NETWORK_DNS()}
|
||||
secondary={dnsServers(data)}
|
||||
/>
|
||||
</ListItem>
|
||||
</>
|
||||
)}
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
return <SectionContent>{content()}</SectionContent>;
|
||||
};
|
||||
|
||||
export default NetworkStatus;
|
||||
81
interface/src/app/status/RestartMonitor.tsx
Normal file
81
interface/src/app/status/RestartMonitor.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import {
|
||||
Box,
|
||||
CircularProgress,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
|
||||
import { readSystemStatus } from 'api/system';
|
||||
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import { useAutoRequest } from 'alova/client';
|
||||
import MessageBox from 'components/MessageBox';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
const RestartMonitor = () => {
|
||||
const [errorMessage, setErrorMessage] = useState<string>();
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
let count = 0;
|
||||
|
||||
const { data } = useAutoRequest(readSystemStatus, {
|
||||
pollingTime: 1000,
|
||||
force: true,
|
||||
initialData: { status: 'Getting ready...' },
|
||||
async middleware(_, next) {
|
||||
if (count++ >= 1) {
|
||||
// skip first request (1 second) to allow AsyncWS to send its response
|
||||
await next();
|
||||
}
|
||||
}
|
||||
})
|
||||
.onSuccess((event) => {
|
||||
if (event.data.status === 'ready' || event.data.status === undefined) {
|
||||
document.location.href = '/';
|
||||
}
|
||||
})
|
||||
.onError((error) => {
|
||||
setErrorMessage(error.message);
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog fullWidth={true} sx={dialogStyle} open={true}>
|
||||
<DialogContent dividers>
|
||||
<Box m={0} py={0} display="flex" alignItems="center" flexDirection="column">
|
||||
<Typography
|
||||
color="secondary"
|
||||
variant="h6"
|
||||
fontWeight={400}
|
||||
textAlign="center"
|
||||
>
|
||||
{data?.status === 'uploading'
|
||||
? LL.WAIT_FIRMWARE()
|
||||
: data?.status === 'restarting'
|
||||
? LL.APPLICATION_RESTARTING()
|
||||
: data?.status === 'ready'
|
||||
? LL.RESTARTING_PRE()
|
||||
: LL.RESTARTING_POST()}
|
||||
…
|
||||
</Typography>
|
||||
<Typography mt={2} variant="h6" fontWeight={400} textAlign="center">
|
||||
{LL.PLEASE_WAIT()}
|
||||
</Typography>
|
||||
|
||||
{errorMessage ? (
|
||||
<MessageBox my={2} level="error" message={errorMessage} />
|
||||
) : (
|
||||
<Box py={2}>
|
||||
<CircularProgress size={32} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default RestartMonitor;
|
||||
346
interface/src/app/status/Status.tsx
Normal file
346
interface/src/app/status/Status.tsx
Normal file
@@ -0,0 +1,346 @@
|
||||
import { useContext, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
|
||||
import DirectionsBusIcon from '@mui/icons-material/DirectionsBus';
|
||||
import LogoDevIcon from '@mui/icons-material/LogoDev';
|
||||
import MemoryIcon from '@mui/icons-material/Memory';
|
||||
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
|
||||
import RouterIcon from '@mui/icons-material/Router';
|
||||
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
|
||||
import TimerIcon from '@mui/icons-material/Timer';
|
||||
import WifiIcon from '@mui/icons-material/Wifi';
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
|
||||
import { API } from 'api/app';
|
||||
import { readSystemStatus } from 'api/system';
|
||||
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import { useAutoRequest, useRequest } from 'alova/client';
|
||||
import { type APIcall, busConnectionStatus } from 'app/main/types';
|
||||
import { FormLoader, SectionContent, useLayoutTitle } from 'components';
|
||||
import ListMenuItem from 'components/layout/ListMenuItem';
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { NTPSyncStatus, NetworkConnectionStatus } from 'types';
|
||||
|
||||
import RestartMonitor from './RestartMonitor';
|
||||
|
||||
const SystemStatus = () => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
useLayoutTitle(LL.STATUS_OF(''));
|
||||
|
||||
const { me } = useContext(AuthenticatedContext);
|
||||
|
||||
const [confirmRestart, setConfirmRestart] = useState<boolean>(false);
|
||||
const [restarting, setRestarting] = useState<boolean>();
|
||||
|
||||
const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
const {
|
||||
data,
|
||||
send: loadData,
|
||||
error
|
||||
} = useAutoRequest(readSystemStatus, {
|
||||
initialData: [],
|
||||
pollingTime: 3000,
|
||||
async middleware(_, next) {
|
||||
if (!restarting) {
|
||||
await next();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const formatDurationSec = (duration_sec: number) => {
|
||||
const days = Math.trunc((duration_sec * 1000) / 86400000);
|
||||
const hours = Math.trunc((duration_sec * 1000) / 3600000) % 24;
|
||||
const minutes = Math.trunc((duration_sec * 1000) / 60000) % 60;
|
||||
const seconds = Math.trunc((duration_sec * 1000) / 1000) % 60;
|
||||
|
||||
let formatted = '';
|
||||
if (days) {
|
||||
formatted += LL.NUM_DAYS({ num: days }) + ' ';
|
||||
}
|
||||
if (hours) {
|
||||
formatted += LL.NUM_HOURS({ num: hours }) + ' ';
|
||||
}
|
||||
if (minutes) {
|
||||
formatted += LL.NUM_MINUTES({ num: minutes }) + ' ';
|
||||
}
|
||||
formatted += LL.NUM_SECONDS({ num: seconds });
|
||||
return formatted;
|
||||
};
|
||||
|
||||
function formatNumber(num: number) {
|
||||
return new Intl.NumberFormat().format(num);
|
||||
}
|
||||
|
||||
const busStatus = () => {
|
||||
if (data) {
|
||||
switch (data.bus_status) {
|
||||
case busConnectionStatus.BUS_STATUS_CONNECTED:
|
||||
return (
|
||||
'EMS ' +
|
||||
LL.CONNECTED(0) +
|
||||
' (' +
|
||||
formatDurationSec(data.bus_uptime) +
|
||||
')'
|
||||
);
|
||||
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
|
||||
return 'EMS ' + LL.TX_ISSUES();
|
||||
case busConnectionStatus.BUS_STATUS_OFFLINE:
|
||||
return 'EMS ' + LL.DISCONNECTED();
|
||||
}
|
||||
}
|
||||
return 'EMS state unknown';
|
||||
};
|
||||
|
||||
const busStatusHighlight = () => {
|
||||
switch (data.bus_status) {
|
||||
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
|
||||
return theme.palette.warning.main;
|
||||
case busConnectionStatus.BUS_STATUS_CONNECTED:
|
||||
return theme.palette.success.main;
|
||||
case busConnectionStatus.BUS_STATUS_OFFLINE:
|
||||
return theme.palette.error.main;
|
||||
default:
|
||||
return theme.palette.warning.main;
|
||||
}
|
||||
};
|
||||
|
||||
const ntpStatus = () => {
|
||||
switch (data.ntp_status) {
|
||||
case NTPSyncStatus.NTP_DISABLED:
|
||||
return LL.NOT_ENABLED();
|
||||
case NTPSyncStatus.NTP_INACTIVE:
|
||||
return LL.INACTIVE(0);
|
||||
case NTPSyncStatus.NTP_ACTIVE:
|
||||
return LL.ACTIVE();
|
||||
default:
|
||||
return LL.UNKNOWN();
|
||||
}
|
||||
};
|
||||
|
||||
const ntpStatusHighlight = () => {
|
||||
switch (data.ntp_status) {
|
||||
case NTPSyncStatus.NTP_DISABLED:
|
||||
return theme.palette.info.main;
|
||||
case NTPSyncStatus.NTP_INACTIVE:
|
||||
return theme.palette.error.main;
|
||||
case NTPSyncStatus.NTP_ACTIVE:
|
||||
return theme.palette.success.main;
|
||||
default:
|
||||
return theme.palette.error.main;
|
||||
}
|
||||
};
|
||||
|
||||
const networkStatusHighlight = () => {
|
||||
switch (data.network_status) {
|
||||
case NetworkConnectionStatus.WIFI_STATUS_IDLE:
|
||||
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
|
||||
case NetworkConnectionStatus.WIFI_STATUS_NO_SHIELD:
|
||||
return theme.palette.info.main;
|
||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECTED:
|
||||
case NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED:
|
||||
return theme.palette.success.main;
|
||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECT_FAILED:
|
||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECTION_LOST:
|
||||
return theme.palette.error.main;
|
||||
default:
|
||||
return theme.palette.warning.main;
|
||||
}
|
||||
};
|
||||
|
||||
const networkStatus = () => {
|
||||
switch (data.network_status) {
|
||||
case NetworkConnectionStatus.WIFI_STATUS_NO_SHIELD:
|
||||
return LL.INACTIVE(1);
|
||||
case NetworkConnectionStatus.WIFI_STATUS_IDLE:
|
||||
return LL.IDLE();
|
||||
case NetworkConnectionStatus.WIFI_STATUS_NO_SSID_AVAIL:
|
||||
return 'No SSID Available';
|
||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECTED:
|
||||
return LL.CONNECTED(0) + ' (WiFi, ' + data.wifi_rssi + ' dBm)';
|
||||
case NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED:
|
||||
return LL.CONNECTED(0) + ' (Ethernet)';
|
||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECT_FAILED:
|
||||
return LL.CONNECTED(1) + ' ' + LL.FAILED(0);
|
||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECTION_LOST:
|
||||
return LL.CONNECTED(1) + ' ' + LL.LOST();
|
||||
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
|
||||
return LL.DISCONNECTED();
|
||||
default:
|
||||
return LL.UNKNOWN();
|
||||
}
|
||||
};
|
||||
|
||||
const activeHighlight = (value: boolean) =>
|
||||
value ? theme.palette.success.main : theme.palette.info.main;
|
||||
|
||||
const doRestart = async () => {
|
||||
setConfirmRestart(false);
|
||||
setRestarting(true);
|
||||
await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch(
|
||||
(error: Error) => {
|
||||
toast.error(error.message);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const renderRestartDialog = () => (
|
||||
<Dialog
|
||||
sx={dialogStyle}
|
||||
open={confirmRestart}
|
||||
onClose={() => setConfirmRestart(false)}
|
||||
>
|
||||
<DialogTitle>{LL.RESTART()}</DialogTitle>
|
||||
<DialogContent dividers>{LL.RESTART_CONFIRM()}</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
onClick={() => setConfirmRestart(false)}
|
||||
color="secondary"
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<PowerSettingsNewIcon />}
|
||||
variant="outlined"
|
||||
onClick={doRestart}
|
||||
color="error"
|
||||
>
|
||||
{LL.RESTART()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
const content = () => {
|
||||
if (!data || !LL) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<List sx={{ borderRadius: 3, border: '2px solid grey' }}>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: '#c5572c', color: 'white' }}>
|
||||
<TimerIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={LL.UPTIME()}
|
||||
secondary={formatDurationSec(data.uptime)}
|
||||
/>
|
||||
{me.admin && (
|
||||
<Button
|
||||
startIcon={<PowerSettingsNewIcon />}
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={() => setConfirmRestart(true)}
|
||||
>
|
||||
{LL.RESTART()}
|
||||
</Button>
|
||||
)}
|
||||
</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.ACTIVE() : 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>
|
||||
|
||||
{renderRestartDialog()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent>{restarting ? <RestartMonitor /> : content()}</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemStatus;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user