mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-06 15:59:52 +03:00
Compare commits
4118 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 | ||
|
|
e00eb8e64f | ||
|
|
ba21293c42 | ||
|
|
8574b44d4e | ||
|
|
76901d97d2 | ||
|
|
1cbfc91912 | ||
|
|
7d6bb6b9c8 | ||
|
|
c81f579464 | ||
|
|
2a6fedc6b3 | ||
|
|
e3a7e9fe33 | ||
|
|
8898ec9419 | ||
|
|
548fdd823b | ||
|
|
b60127c3ba | ||
|
|
095255b9b7 | ||
|
|
2b486ffa36 | ||
|
|
c3f9d9ddd6 | ||
|
|
740f3b4ef7 | ||
|
|
932a496f47 | ||
|
|
d36246d4d1 | ||
|
|
19094d47aa | ||
|
|
f41bb3671c | ||
|
|
22c75e6df3 | ||
|
|
24cca67625 | ||
|
|
41443d4efe | ||
|
|
6e08356ff7 | ||
|
|
751410ca58 | ||
|
|
60beeddb66 | ||
|
|
c61c34f10e | ||
|
|
96a04da1ff | ||
|
|
bd8472b34e | ||
|
|
09228e4637 | ||
|
|
40a79c51ce | ||
|
|
5ab10b7aa6 | ||
|
|
01a15a5c85 | ||
|
|
1e15f65b0d | ||
|
|
0818728c25 | ||
|
|
2edfe0f42c | ||
|
|
a2eb8dfe83 | ||
|
|
4c60545057 | ||
|
|
d06b3285bd | ||
|
|
4dcfe8e0f6 | ||
|
|
ee5fd4d0eb | ||
|
|
46f35bc67c | ||
|
|
42e679d5ba | ||
|
|
4db1c7dfca | ||
|
|
64fb84dd54 | ||
|
|
5ad03facce | ||
|
|
ad3bf294d8 | ||
|
|
6dba452aea | ||
|
|
cff5b8c949 | ||
|
|
aed45968db | ||
|
|
ec85a7ec24 | ||
|
|
2de855e044 | ||
|
|
dfd9fe4d01 | ||
|
|
049f956131 | ||
|
|
b503e2bb90 | ||
|
|
b300181d33 | ||
|
|
02f2389587 | ||
|
|
7f140021aa | ||
|
|
905b52e39d | ||
|
|
8fcfb3d8f7 | ||
|
|
a17a9b71a2 | ||
|
|
64d46fe8f7 | ||
|
|
7962af0872 | ||
|
|
873b75240c | ||
|
|
9e405c5a5a | ||
|
|
6796962c1e | ||
|
|
df6de21cf4 | ||
|
|
e20434da88 | ||
|
|
9c8677acb9 | ||
|
|
1c3bc98bbb | ||
|
|
02616696dc | ||
|
|
00d66c1c4e | ||
|
|
8cfc540670 | ||
|
|
a4062f5d84 | ||
|
|
edee463ade | ||
|
|
64471e4c0e | ||
|
|
28a7ceb6aa | ||
|
|
0a10e78bfd | ||
|
|
e4a899912c | ||
|
|
cc9819b56b | ||
|
|
29f86c9ab9 | ||
|
|
722ca34a18 | ||
|
|
50777bd681 | ||
|
|
2f5558c311 | ||
|
|
21c3fe5d8e | ||
|
|
acb453bd4b | ||
|
|
bc39b738e2 | ||
|
|
1ada18ec9a | ||
|
|
6b9dadc0d9 | ||
|
|
cf89a06437 | ||
|
|
4a1ea99ee7 | ||
|
|
3dbf8a0fd1 | ||
|
|
9c6b9a5359 | ||
|
|
0d07a9e50c | ||
|
|
7b5a37a85d | ||
|
|
0b5a7b9f2a | ||
|
|
0f99415bbf | ||
|
|
c9775c1edd | ||
|
|
1f1b571e91 | ||
|
|
21e28e970c | ||
|
|
f9e1940c7b | ||
|
|
72c0625823 | ||
|
|
4f1ef297c7 | ||
|
|
3b30083e7c | ||
|
|
50590f4924 | ||
|
|
fae876d7d2 | ||
|
|
c1dabddf21 | ||
|
|
6926f6fd0b | ||
|
|
e30c476e5c | ||
|
|
509122bf4b | ||
|
|
84fab951ba | ||
|
|
a8ea6ef4a8 | ||
|
|
196e98b3bd | ||
|
|
a0f1f51cca | ||
|
|
d717b72a9e | ||
|
|
eccece7207 | ||
|
|
499ff0d31e | ||
|
|
ae0599d13d | ||
|
|
1a9cf4ebdc | ||
|
|
386082b747 | ||
|
|
fc02721815 | ||
|
|
529970fb19 | ||
|
|
752ce333ec | ||
|
|
f34be2a884 | ||
|
|
bed1650350 | ||
|
|
1f8a477939 | ||
|
|
8e844550bf | ||
|
|
dfd2a017c2 | ||
|
|
c40a11504c | ||
|
|
fd81299da1 | ||
|
|
9993a7c739 | ||
|
|
ea38011085 | ||
|
|
2079b7e602 | ||
|
|
101159b9f6 | ||
|
|
1442568790 | ||
|
|
6699d9ad80 | ||
|
|
1b27fae844 | ||
|
|
253eb72dbf | ||
|
|
4b6b89f1a0 | ||
|
|
fe772f85bf | ||
|
|
a2422e1f6a | ||
|
|
8ba40003e4 | ||
|
|
ac0fb52ce9 | ||
|
|
031c97a162 | ||
|
|
f7d3939c72 | ||
|
|
20f32db8bc | ||
|
|
0e55caf721 | ||
|
|
4d5f8cc96a | ||
|
|
434ef2b333 | ||
|
|
e0ab208c52 | ||
|
|
5b1f3d266e | ||
|
|
4c83f5fe60 | ||
|
|
981d62dc2c | ||
|
|
ffcf0bf2d7 | ||
|
|
7997544fb8 | ||
|
|
7e1f16c865 | ||
|
|
765e6bcd69 | ||
|
|
81b654cc41 | ||
|
|
3fc36b5e50 | ||
|
|
52077cbd07 | ||
|
|
a45f1badba | ||
|
|
f38f4bc85a | ||
|
|
8c03592157 | ||
|
|
e7254bc7f4 | ||
|
|
5d0242b47c | ||
|
|
5997dd1491 | ||
|
|
31a5216ae8 | ||
|
|
5c0c0675a2 | ||
|
|
ba9f16da00 | ||
|
|
110ee59cd1 | ||
|
|
555bf8cb2f | ||
|
|
5f1dddf7e4 | ||
|
|
2f658a9a14 | ||
|
|
c3f487eced | ||
|
|
a8a12dd1f8 | ||
|
|
7f7e3c47ec | ||
|
|
eafeb5c5d2 | ||
|
|
caca8bf802 | ||
|
|
338091578b | ||
|
|
bf5b40ccf4 | ||
|
|
58cbfbc0df | ||
|
|
06f5e1226f | ||
|
|
af57af46b0 | ||
|
|
3f91377751 | ||
|
|
c4c9ed739f | ||
|
|
2a8d3b8cd6 | ||
|
|
af1292caa0 | ||
|
|
44afa1a30f | ||
|
|
fa184a8f94 | ||
|
|
387c2eebcd | ||
|
|
cd6a0da9f0 | ||
|
|
188bfa4525 | ||
|
|
037a9848bc | ||
|
|
b6543169de | ||
|
|
14b3b058fe | ||
|
|
fa4763309d | ||
|
|
adcc59642c | ||
|
|
d18fd4948c | ||
|
|
c10ecb097e | ||
|
|
4e4258f9dc | ||
|
|
ab6cf78822 | ||
|
|
d105c18bf7 | ||
|
|
6bbf4e4778 | ||
|
|
3101f5e6ae | ||
|
|
207953e8d1 | ||
|
|
a449ebd0ea | ||
|
|
2afe50fc9e | ||
|
|
cd564f0c54 | ||
|
|
8eba09fea6 | ||
|
|
6c17d61baf | ||
|
|
6df1f2850f | ||
|
|
288d9b70b7 | ||
|
|
8307b0920c | ||
|
|
1097b519ae | ||
|
|
e7b7002883 | ||
|
|
c934b9e8d9 | ||
|
|
0838d06ec4 | ||
|
|
41666458d9 | ||
|
|
b02207a0d7 | ||
|
|
faafd51e40 | ||
|
|
94cf81c164 | ||
|
|
1cd97cf1ee | ||
|
|
6f097e4613 | ||
|
|
90af88f36d | ||
|
|
862cc2545b | ||
|
|
74c6e54dc5 | ||
|
|
33ac89e202 | ||
|
|
89d591500c | ||
|
|
9bf56b2e6d | ||
|
|
7c3dac3da7 | ||
|
|
6853651c8b | ||
|
|
16a9bc4fae | ||
|
|
70d4bcf097 | ||
|
|
1991ca4128 | ||
|
|
879df338e3 | ||
|
|
29c1169b33 | ||
|
|
362b6a1394 | ||
|
|
f5f5901ad9 | ||
|
|
757bf58992 | ||
|
|
af8e77029e | ||
|
|
94519179df | ||
|
|
527dd870a2 | ||
|
|
b9e57414ce | ||
|
|
e3a644182e | ||
|
|
caadb506d8 | ||
|
|
597f0b147d | ||
|
|
0b9a2483b2 | ||
|
|
6a221e84cc | ||
|
|
7ea7a4b25a | ||
|
|
c494627867 | ||
|
|
5032c37f63 | ||
|
|
f5cd92d567 | ||
|
|
240b7dad32 | ||
|
|
2bac805ebf | ||
|
|
b1a3d6ea20 | ||
|
|
77cfad9ff0 | ||
|
|
2dc1bd71ee | ||
|
|
fcdc4314e3 | ||
|
|
900112a967 | ||
|
|
114a8af467 | ||
|
|
e2c2a12a2b | ||
|
|
222f853616 | ||
|
|
450f5a465c | ||
|
|
ba106eecc0 | ||
|
|
374bd7c5c2 | ||
|
|
31b2005320 | ||
|
|
8ae5570c70 | ||
|
|
96fe9aeb31 | ||
|
|
4a7d69c797 | ||
|
|
7dca77450c | ||
|
|
9bb157bbe6 | ||
|
|
6c7787b2ae | ||
|
|
ec5b5ef79d | ||
|
|
6abd56c7e2 | ||
|
|
1fdeaf8b24 | ||
|
|
ad51870d2c | ||
|
|
8929e15e00 | ||
|
|
3f31e636ec | ||
|
|
5e7e1c30ca | ||
|
|
5cf0d8d204 | ||
|
|
dbbd475934 | ||
|
|
bf9877b528 | ||
|
|
5e6b0f64bd | ||
|
|
509d213536 | ||
|
|
4c789a656b | ||
|
|
2f6edfd505 | ||
|
|
9c2d861b93 | ||
|
|
2c0d4fdcef | ||
|
|
be93627ec0 | ||
|
|
c6c7754735 | ||
|
|
07bdf28350 | ||
|
|
fd49f0372a | ||
|
|
36ca7e09ca | ||
|
|
7a36c5e8cb | ||
|
|
15b97515bb | ||
|
|
4048f58856 | ||
|
|
b0ea3184a4 | ||
|
|
6ab2cc60e7 | ||
|
|
237d631e2c | ||
|
|
0ed46679e2 | ||
|
|
1fded64f2e | ||
|
|
161f782904 | ||
|
|
3fe9296139 | ||
|
|
9698e787b2 | ||
|
|
eb274a94c3 | ||
|
|
18be921c1b | ||
|
|
fddfa47b51 | ||
|
|
c533e91643 | ||
|
|
de2792ffdd | ||
|
|
97de23f521 | ||
|
|
c212520f4f | ||
|
|
dc739b97ab | ||
|
|
2583da8714 | ||
|
|
ddfc9f9dd0 | ||
|
|
7d855326d0 | ||
|
|
a1bb49359f | ||
|
|
f9a176e09c | ||
|
|
24d5ac7656 | ||
|
|
558fa20283 | ||
|
|
c42ead995e | ||
|
|
8584901229 | ||
|
|
a17f8db035 | ||
|
|
6783d5b3e8 | ||
|
|
9223a00270 | ||
|
|
c150c578a6 | ||
|
|
5be1482c20 | ||
|
|
20e301594c | ||
|
|
f90f4279d7 | ||
|
|
9962f16d62 | ||
|
|
cb10663735 | ||
|
|
2ab247131f | ||
|
|
c0a3d03a09 | ||
|
|
baa180cc79 | ||
|
|
dbc59b7c8c | ||
|
|
c4d1058133 | ||
|
|
3a8495ca54 | ||
|
|
dad62613f6 | ||
|
|
9641adce1c | ||
|
|
67693364cc | ||
|
|
d37a5c5713 | ||
|
|
a881431010 | ||
|
|
2866862730 | ||
|
|
c234e70a87 | ||
|
|
61296896bd | ||
|
|
2acb45db47 | ||
|
|
abe0d793d6 | ||
|
|
654403ca3d | ||
|
|
b111e4762a | ||
|
|
8bd796b772 | ||
|
|
84b6611ffd | ||
|
|
58b3f309bd | ||
|
|
0bd4330881 | ||
|
|
68feb0fa30 | ||
|
|
b4e266f128 | ||
|
|
855794dbe8 | ||
|
|
2a38981c67 | ||
|
|
63b4a62fac | ||
|
|
710fd1bc79 | ||
|
|
f5ab7510b4 | ||
|
|
ecd37223e8 | ||
|
|
99992db9ac | ||
|
|
3ecea985ad | ||
|
|
ae1b6fc67b | ||
|
|
020bb628d6 | ||
|
|
22a4338e7a | ||
|
|
1f089ec6f9 | ||
|
|
0f78d4f34d | ||
|
|
8395983106 | ||
|
|
87ce1a6d9b | ||
|
|
ac4eba5b72 | ||
|
|
5e53689a81 | ||
|
|
895eddd8d2 | ||
|
|
a8c7b2bca4 | ||
|
|
6f46382806 | ||
|
|
b8af3e235f | ||
|
|
531c2e7b5d | ||
|
|
f76e6b56b1 | ||
|
|
c388b31b22 | ||
|
|
cbb6fa9fec | ||
|
|
a3300e94a7 | ||
|
|
3c8a00be09 | ||
|
|
36ae55e543 | ||
|
|
8b5d893977 | ||
|
|
bd851f9005 | ||
|
|
208be85304 | ||
|
|
9dd613394c | ||
|
|
62eeca944b | ||
|
|
17d7487423 | ||
|
|
ae589c745b | ||
|
|
4b41180785 | ||
|
|
7748f67b9a | ||
|
|
df9f75a5c9 | ||
|
|
7bd8710eb6 | ||
|
|
32f2c6d341 | ||
|
|
20d8b6400f | ||
|
|
d3b086a675 | ||
|
|
e1ee83b907 | ||
|
|
763a2eaab1 | ||
|
|
5d78f1c814 | ||
|
|
3f99806ddd | ||
|
|
bc0a90ee41 | ||
|
|
3900b8fc94 | ||
|
|
f9a53cf320 | ||
|
|
99467558b4 | ||
|
|
2e25e46f6f | ||
|
|
3a1b7cae67 | ||
|
|
5bc3bd23e2 | ||
|
|
a04f4d784b | ||
|
|
9c423dc886 | ||
|
|
1b0f840d18 | ||
|
|
21eaf70181 | ||
|
|
14f2cd4bee | ||
|
|
917e268ac9 | ||
|
|
41228e894a | ||
|
|
365c95991b | ||
|
|
ba4ebe221c | ||
|
|
3ab5946e38 | ||
|
|
f427288c06 | ||
|
|
398cbadb64 | ||
|
|
38a1c9e9d7 | ||
|
|
5abfdf1177 | ||
|
|
385d2377db | ||
|
|
6655035561 | ||
|
|
755408e6aa | ||
|
|
df70ed0062 | ||
|
|
b893290b62 | ||
|
|
96ff5ed123 | ||
|
|
dcdd8d9e44 | ||
|
|
5c484d58dc | ||
|
|
96bf1a32ee | ||
|
|
ad81f84c02 | ||
|
|
529b46d776 | ||
|
|
bbc5bc8585 | ||
|
|
9c33b5cb8b | ||
|
|
ba7ceca798 | ||
|
|
4824152ae5 | ||
|
|
af935d8124 | ||
|
|
951e706fee | ||
|
|
9f1002df9a | ||
|
|
6ab2135cea | ||
|
|
e6bcdede73 | ||
|
|
8b136ddefe | ||
|
|
ca9ac86fde | ||
|
|
42a4c792ad | ||
|
|
a6b0c74f5f | ||
|
|
09e2945c15 | ||
|
|
a3f05cb08b | ||
|
|
0ab573225d | ||
|
|
4c8fd45908 | ||
|
|
8bb703fafe | ||
|
|
9cdeb7c07a | ||
|
|
8d4b43e9f6 | ||
|
|
d8c298f6fe | ||
|
|
8a16566f53 | ||
|
|
9d3456cb3a | ||
|
|
7310db6426 | ||
|
|
ac08bb6037 | ||
|
|
776f72650f | ||
|
|
0eb96e764c | ||
|
|
ec939dfc2e | ||
|
|
809b6be79c | ||
|
|
34f87a0a21 | ||
|
|
bc4418755e | ||
|
|
d24742facd | ||
|
|
9c16e2008e | ||
|
|
6a7a9636ac | ||
|
|
5e76bfa210 | ||
|
|
4da45a7240 | ||
|
|
3c84a1ced1 | ||
|
|
86919c1684 | ||
|
|
677f6c5a6e | ||
|
|
3ce67b8e6e | ||
|
|
aad9c12a7e | ||
|
|
877d60f46f | ||
|
|
bf02b0ad1c | ||
|
|
2f5b7cd0aa | ||
|
|
4bec32ea56 | ||
|
|
33ea1c6863 | ||
|
|
09ff892b91 | ||
|
|
f462afb547 | ||
|
|
83211e0ef8 | ||
|
|
b7b3cb177f | ||
|
|
10c8c2b4a7 | ||
|
|
c19345c345 | ||
|
|
04fd04b2ab | ||
|
|
6e3c2b2ec9 | ||
|
|
98e29516b0 | ||
|
|
6db5058e3d | ||
|
|
b3ada4a01f | ||
|
|
f38e0abf73 | ||
|
|
6376ed2361 | ||
|
|
04e43d5d41 | ||
|
|
0ba5d8e2bd | ||
|
|
95b8e79624 | ||
|
|
e9c3f8c6da | ||
|
|
27aa72eda1 | ||
|
|
01f6024776 | ||
|
|
aa5730c683 | ||
|
|
fcc2a48192 | ||
|
|
1c4da53e75 | ||
|
|
09a15727c7 | ||
|
|
f1034f4230 | ||
|
|
06e9b8d303 | ||
|
|
b912779ef9 | ||
|
|
ced63a6eb0 | ||
|
|
a5f5d36871 | ||
|
|
bd92345793 | ||
|
|
4c1b66279d | ||
|
|
fcf16eec4f | ||
|
|
508f18c29b | ||
|
|
b165e1c20c | ||
|
|
9ebcfe38bc | ||
|
|
57a585741d | ||
|
|
6173e94cc0 | ||
|
|
243aec37fd | ||
|
|
fb44e020fc | ||
|
|
81842e544b | ||
|
|
6babdab8de | ||
|
|
93d50bbee1 | ||
|
|
e6f0ecce62 | ||
|
|
aba597aa6a | ||
|
|
7444fdceff | ||
|
|
f60197e9bf | ||
|
|
4778206e3a | ||
|
|
005463c41f | ||
|
|
ae1bf1cbfb | ||
|
|
bde06621bd | ||
|
|
647acf6a68 | ||
|
|
120c0b5ca2 | ||
|
|
7e45c89fcd | ||
|
|
b7611c67bb | ||
|
|
d6ae55218f | ||
|
|
d17582d697 | ||
|
|
c3227a6352 | ||
|
|
d0c368f6a0 | ||
|
|
9aa9cc46e9 | ||
|
|
4ee045b84e | ||
|
|
2fe18c14c7 | ||
|
|
f46b002c5a | ||
|
|
65ea11b48b | ||
|
|
b2113add02 | ||
|
|
152e6ce1b9 | ||
|
|
78a5166e48 | ||
|
|
f6a4da0584 | ||
|
|
1818057c4c | ||
|
|
9c946b9808 | ||
|
|
c703106058 | ||
|
|
1bc70f09c5 | ||
|
|
d049572239 | ||
|
|
0feaed8869 | ||
|
|
7496f482ab | ||
|
|
f6faad7255 | ||
|
|
757757fa3f | ||
|
|
1fd3c11e12 | ||
|
|
86e29515e7 | ||
|
|
b1f2273bb5 | ||
|
|
e9cf3f5ab5 | ||
|
|
46eb4185d7 | ||
|
|
8da6761a48 | ||
|
|
f6c34ee2c9 | ||
|
|
b500076f32 | ||
|
|
e87ea67435 | ||
|
|
86c965b94d | ||
|
|
faec42ac71 | ||
|
|
86737fb38a | ||
|
|
c8e64668e9 | ||
|
|
89b7eaac1c | ||
|
|
ac889d921f | ||
|
|
a652a3a0c3 | ||
|
|
0f88b7cfbc | ||
|
|
8bb2157cd9 | ||
|
|
b96cede131 | ||
|
|
b2590fc9e0 | ||
|
|
8dfff29389 | ||
|
|
0ab081c135 | ||
|
|
7a3aca1220 | ||
|
|
1360a5d35b | ||
|
|
9226e5b303 | ||
|
|
4e4cc93092 | ||
|
|
faa3479f0a | ||
|
|
0924f441cb | ||
|
|
f39deefbb4 | ||
|
|
461f1bb102 | ||
|
|
1d1aaef291 | ||
|
|
3c51e80408 | ||
|
|
ff68d23642 | ||
|
|
643b42469e | ||
|
|
a996d00f70 | ||
|
|
6ccab21791 | ||
|
|
b5b965e339 | ||
|
|
2b54db255e | ||
|
|
45fc13f7a0 | ||
|
|
94407e5192 | ||
|
|
7e791e56ce | ||
|
|
2698116c63 | ||
|
|
e17f709fe1 | ||
|
|
c4ec535a82 | ||
|
|
7efc7616f8 | ||
|
|
9468248995 | ||
|
|
a72cbe9c0c | ||
|
|
171df2c5b7 | ||
|
|
ca2bb4ba6f | ||
|
|
9b92bfa81c | ||
|
|
0cc14215e6 | ||
|
|
2e1e294baf | ||
|
|
6cee81aeb0 | ||
|
|
6e653eea7e | ||
|
|
f788be691f | ||
|
|
c167aa90bc | ||
|
|
657056d774 | ||
|
|
1d979cc894 | ||
|
|
dda2d6316d | ||
|
|
cac7ddd6bb | ||
|
|
f677616b22 | ||
|
|
26daf548ff | ||
|
|
d92974dd88 | ||
|
|
52de833b19 | ||
|
|
bb39e8bd4c | ||
|
|
4295b2d114 | ||
|
|
01060574ab | ||
|
|
2e829b560d | ||
|
|
a2f0cafc5c | ||
|
|
6a675736eb | ||
|
|
c8466df61d | ||
|
|
ded26feeb6 | ||
|
|
b2482b092c | ||
|
|
67397dc345 | ||
|
|
4a11147467 | ||
|
|
962131592f | ||
|
|
168a31d331 | ||
|
|
00f08951db | ||
|
|
96d7069e8d | ||
|
|
a4ddacb29c | ||
|
|
93bbf2570e | ||
|
|
ba409b808d | ||
|
|
207d92be64 | ||
|
|
e3c775d0dd | ||
|
|
2bd7e04d18 | ||
|
|
8b58282bde | ||
|
|
ad6be2d5ef | ||
|
|
c47414dad5 | ||
|
|
d84a555171 | ||
|
|
7a5f13bcad | ||
|
|
1b6e3b89b6 | ||
|
|
ed694f4ec5 | ||
|
|
b659218d13 | ||
|
|
cbecebd80a | ||
|
|
7bb6427ec9 | ||
|
|
37724814be | ||
|
|
98dd25c5bb | ||
|
|
ccd69c2c3a | ||
|
|
6d0bd099e1 | ||
|
|
660a715893 | ||
|
|
981cdaecf1 | ||
|
|
18e4c3aa88 | ||
|
|
116c51443e | ||
|
|
66799d0c6c | ||
|
|
9ad9bc5536 | ||
|
|
d1ebf378dc | ||
|
|
4730389668 | ||
|
|
7881abaca2 | ||
|
|
f3d3a386ee | ||
|
|
1b623014c1 | ||
|
|
af3ed871af | ||
|
|
2a9be94f6e | ||
|
|
08a5437210 | ||
|
|
9ff8d01e93 | ||
|
|
0afda87092 | ||
|
|
18d32de483 | ||
|
|
d9ad835fbc | ||
|
|
afad1d7b95 | ||
|
|
e2f5124db9 | ||
|
|
875507d7a9 | ||
|
|
17b4964b01 | ||
|
|
5af1f932bd | ||
|
|
25ff2bd150 | ||
|
|
3654003577 | ||
|
|
ac96ea5308 | ||
|
|
dde270a84c | ||
|
|
52c3153123 | ||
|
|
8557fec581 | ||
|
|
b8e36eb13b | ||
|
|
39624dcfcc | ||
|
|
156d5a02d3 | ||
|
|
eb7ed0c524 | ||
|
|
0415ade906 | ||
|
|
bf6c522918 | ||
|
|
5f8f13e1a5 | ||
|
|
d92cd4ea2c | ||
|
|
2c19b9369a | ||
|
|
78403adeb4 | ||
|
|
5374127d56 | ||
|
|
87f28e7804 | ||
|
|
1e8adfb34f | ||
|
|
d3175e3d64 | ||
|
|
1b832b541a | ||
|
|
29557d764a | ||
|
|
ae46f40fce | ||
|
|
362d837baa | ||
|
|
2db7174068 | ||
|
|
1360ffaec0 | ||
|
|
91e0a29494 | ||
|
|
99b2da752f | ||
|
|
32884da60d | ||
|
|
d537247ead | ||
|
|
d8364c5df2 | ||
|
|
f62b5548a3 | ||
|
|
443fe640b3 | ||
|
|
365d246a89 | ||
|
|
bea0922ee8 | ||
|
|
eb6e8d0c93 | ||
|
|
1b27ccf70e | ||
|
|
3aa806ca65 | ||
|
|
f99735921c | ||
|
|
c934e6617c | ||
|
|
67ce23337f | ||
|
|
b57bc11070 | ||
|
|
f3d1ed5596 | ||
|
|
5112bf9e08 | ||
|
|
60594ff4b7 | ||
|
|
dc3467e7fc | ||
|
|
bb87d064a5 | ||
|
|
30388da021 | ||
|
|
1f7ddcec55 | ||
|
|
942eba868a | ||
|
|
d6e7b136a8 | ||
|
|
55c33ef51f | ||
|
|
d33a554ab1 | ||
|
|
cbbe6ac589 | ||
|
|
84b8fa8ff5 | ||
|
|
3a7eb634de | ||
|
|
5977b0585d | ||
|
|
7b0d73e275 | ||
|
|
143ce8c80f | ||
|
|
4e736a4ff1 | ||
|
|
d2523fc5d2 | ||
|
|
ab4f8d46ad | ||
|
|
75defd0858 | ||
|
|
e52f0954cd | ||
|
|
98e9b62c85 | ||
|
|
40737ea552 | ||
|
|
523f501bd0 | ||
|
|
905c496fa3 | ||
|
|
2ad375884f | ||
|
|
50b8db0c96 | ||
|
|
36f88c772f | ||
|
|
7086358ebc | ||
|
|
51769011be | ||
|
|
d15de88065 | ||
|
|
f909b32db8 | ||
|
|
e8e3fc5fb6 | ||
|
|
90a1bdd5d9 | ||
|
|
0b6eba294e | ||
|
|
98b2a3a6e1 | ||
|
|
3bd21d4486 | ||
|
|
2094d42c3b | ||
|
|
0156dd4956 | ||
|
|
da3e118444 | ||
|
|
2663bc15f4 | ||
|
|
50939c1077 | ||
|
|
b0fc26b6eb | ||
|
|
476f84be77 | ||
|
|
449bb4055c | ||
|
|
86daa13426 | ||
|
|
09aea280b5 | ||
|
|
34113c3698 | ||
|
|
9b21607da5 | ||
|
|
985da48947 | ||
|
|
32f3c646f8 | ||
|
|
d27ef2530d | ||
|
|
fb41606e43 | ||
|
|
cbb7d46ede | ||
|
|
3efe16c840 | ||
|
|
8b800ded21 | ||
|
|
31220b3fde | ||
|
|
f4cae5027e | ||
|
|
d17705db6c | ||
|
|
d6c5c87412 | ||
|
|
89f14f1dba | ||
|
|
5fddb08330 | ||
|
|
f61c447ea5 | ||
|
|
8b8b023665 | ||
|
|
158617f56b | ||
|
|
27b9aa6ddd | ||
|
|
e615274f83 | ||
|
|
2b24f2585f | ||
|
|
0e52deae7b | ||
|
|
bc6b48bd07 | ||
|
|
ce1b9f22cb | ||
|
|
b1d666d7b9 | ||
|
|
ed55a96b80 | ||
|
|
2ae45ecd6e | ||
|
|
f58dbf6ec1 | ||
|
|
adc4760b5f | ||
|
|
c44903e1b0 | ||
|
|
8e081ce04f | ||
|
|
5c6d704a48 | ||
|
|
e11d5bed9d | ||
|
|
5784bf744e | ||
|
|
85716bc6bc | ||
|
|
81e5ee8364 | ||
|
|
52d4505410 | ||
|
|
b28865a283 | ||
|
|
d2ff44e1cf | ||
|
|
9a34b2dd81 | ||
|
|
43dc177a6d | ||
|
|
34046795b7 | ||
|
|
2c132a86b0 | ||
|
|
759fe69909 | ||
|
|
181b672c1a | ||
|
|
2e63d60273 | ||
|
|
46578dbe69 | ||
|
|
cbc9607b26 | ||
|
|
7865ddc51f | ||
|
|
d9c2fe0fb9 | ||
|
|
02c386d76f | ||
|
|
ee0e6ed29f | ||
|
|
e858596658 | ||
|
|
b4519b40eb | ||
|
|
a0bc5fed35 | ||
|
|
2a7bd7e963 | ||
|
|
26b873da9e | ||
|
|
82a8f0481e | ||
|
|
53c8d9372e | ||
|
|
d314e1d050 | ||
|
|
e4a0799163 | ||
|
|
685f987a8e | ||
|
|
b57fff13eb | ||
|
|
4a3201f194 | ||
|
|
8b948bc521 | ||
|
|
91f0da84d2 | ||
|
|
551b2d900e | ||
|
|
06e3c0a41f | ||
|
|
ac3d16b2b4 | ||
|
|
c8c4a3d450 | ||
|
|
6aaccf4f90 | ||
|
|
2d70f637c9 | ||
|
|
cf042271c3 | ||
|
|
7330ca20f8 | ||
|
|
b5ccf79f70 | ||
|
|
9a2303ab73 | ||
|
|
07102e0279 | ||
|
|
0c1042abc9 | ||
|
|
2571f5e5e6 | ||
|
|
c9a968c1a5 | ||
|
|
b74e56974e | ||
|
|
fabaa230de | ||
|
|
c224abfc61 | ||
|
|
26f5c9debf | ||
|
|
ae9a86e796 | ||
|
|
c57a5c04d6 | ||
|
|
d1df21cb8e | ||
|
|
ff59276387 | ||
|
|
0e0ec7a7c7 | ||
|
|
3ecaeffec0 | ||
|
|
4f8d3d27ba | ||
|
|
6575e1d790 | ||
|
|
b255b65dc4 | ||
|
|
b1b15eef4c | ||
|
|
2f5e5535c8 | ||
|
|
5abe9530d7 | ||
|
|
a5113eb90d | ||
|
|
e6f839b554 | ||
|
|
b2ad600de2 | ||
|
|
ea71b43f78 | ||
|
|
d939795b8c | ||
|
|
db9b9992d6 | ||
|
|
c8fd8c4928 | ||
|
|
a5fae64db6 | ||
|
|
5767d83c85 | ||
|
|
7910893529 | ||
|
|
3c003657b9 | ||
|
|
348ffcd077 | ||
|
|
09ea35e340 | ||
|
|
fcac70a2ca | ||
|
|
b5b890beaa | ||
|
|
ca964c13a7 | ||
|
|
979503256a | ||
|
|
c9ab6b23ea | ||
|
|
e84a3bc99a | ||
|
|
310bb53985 | ||
|
|
5b87e74be8 | ||
|
|
8aa7cd166b | ||
|
|
72f530b969 | ||
|
|
b027ce0d91 | ||
|
|
ff5247eaf5 | ||
|
|
148e35ea53 | ||
|
|
8a01a1e471 | ||
|
|
c34208c3df | ||
|
|
bf5c11156a | ||
|
|
6e3b36c070 | ||
|
|
dc4bd64aff | ||
|
|
0d9cd64619 | ||
|
|
cc10c494c6 | ||
|
|
3ef3f561b9 | ||
|
|
2d9627373c | ||
|
|
d618d09bdf | ||
|
|
8a02f2a27a | ||
|
|
ceb63fa09f | ||
|
|
a256d2573c | ||
|
|
5fe9049537 | ||
|
|
6f681aa451 | ||
|
|
63d105437f | ||
|
|
2254bf9c16 | ||
|
|
90a719561b | ||
|
|
d06dc3e2cf | ||
|
|
ff058b06a1 | ||
|
|
26b0c67d13 | ||
|
|
ab24695371 | ||
|
|
1861365124 | ||
|
|
321558c583 | ||
|
|
dd25900af4 | ||
|
|
28252b987b | ||
|
|
b823366a82 | ||
|
|
90ce8bc8ce | ||
|
|
1362976508 | ||
|
|
cfe8c410ae | ||
|
|
5f87718deb | ||
|
|
f15fb9a5d1 | ||
|
|
57cc39c087 | ||
|
|
b9402d3a01 | ||
|
|
b59c76df3b | ||
|
|
7d526196a0 | ||
|
|
f1b3cb9646 | ||
|
|
67de1c6a47 | ||
|
|
41b2a67f09 | ||
|
|
f2636d42a4 | ||
|
|
0c0ef80025 | ||
|
|
bcd0c5ac52 | ||
|
|
005485188f | ||
|
|
9553161b07 | ||
|
|
71e7887da3 | ||
|
|
300fe721a1 | ||
|
|
047eae2d40 | ||
|
|
c3e5e8d6d1 | ||
|
|
466e9ec6fe | ||
|
|
74c88caa19 | ||
|
|
71d086a8db | ||
|
|
728e15772f | ||
|
|
e1ffd8860d | ||
|
|
4b9bddd565 | ||
|
|
f53921d068 | ||
|
|
445618d232 | ||
|
|
b035e27bc8 | ||
|
|
cecbb25857 | ||
|
|
c2823a5ed6 | ||
|
|
3dba97239d | ||
|
|
209afc87bb | ||
|
|
19e8e4a7a1 | ||
|
|
d8ff088231 | ||
|
|
6f14fcb6e8 | ||
|
|
56e95d1d85 | ||
|
|
e12083e3ba | ||
|
|
fa263918ba | ||
|
|
e81b1b9277 | ||
|
|
84635ac889 | ||
|
|
f80764d72b | ||
|
|
04dd9eef09 | ||
|
|
6bd744f12e | ||
|
|
6f155fab0b | ||
|
|
a54f3f7085 | ||
|
|
d86153a08e | ||
|
|
8eb7793cd0 | ||
|
|
79bff902e1 | ||
|
|
825e33c3ea | ||
|
|
0c8a7d51a8 | ||
|
|
05e1bbf86b | ||
|
|
004ec92d68 | ||
|
|
f00502863f | ||
|
|
fe8aa8f727 | ||
|
|
24eccf1e04 | ||
|
|
5ef494b702 | ||
|
|
1673acd537 | ||
|
|
d993b356f9 | ||
|
|
e2fec03a21 | ||
|
|
5e2d97c0b9 | ||
|
|
594e79be5d | ||
|
|
fa50846a05 | ||
|
|
36d5df65b8 | ||
|
|
486b365f9b | ||
|
|
5c59a20714 | ||
|
|
1c10d8015c | ||
|
|
d2d29afcbe | ||
|
|
982a43b8d4 | ||
|
|
17b108597e | ||
|
|
28e42b7920 | ||
|
|
904403f8fa | ||
|
|
84e8eaf33c | ||
|
|
959e09f6c1 | ||
|
|
c8c4b36ebd | ||
|
|
ddad1bbe09 | ||
|
|
a2fc137ebe | ||
|
|
936d887394 | ||
|
|
a396d0335f | ||
|
|
d575f3b3c2 | ||
|
|
0674426d29 | ||
|
|
80ec86361a | ||
|
|
1ebd38bf03 | ||
|
|
274855a19e | ||
|
|
d26d2ae556 | ||
|
|
0a7e1f629e | ||
|
|
a84e9e0923 | ||
|
|
aea2277e26 | ||
|
|
a5d2beb3e7 | ||
|
|
729832ae76 | ||
|
|
21f746d1fa | ||
|
|
d83de8bd15 | ||
|
|
12da8f9b67 | ||
|
|
70550a4866 | ||
|
|
789a4e1340 | ||
|
|
35c999e2fd | ||
|
|
bfdffc14c5 | ||
|
|
e74d3d4cd5 | ||
|
|
514a96c8e4 | ||
|
|
210181feef | ||
|
|
942de63b83 | ||
|
|
93d68996b4 | ||
|
|
369c7554f2 | ||
|
|
c92d9123a0 | ||
|
|
f0be8cf581 | ||
|
|
bb85255ef1 | ||
|
|
e46c5263d9 | ||
|
|
e7372922d7 | ||
|
|
0c9c15f854 | ||
|
|
216e7e5307 | ||
|
|
a92474bf37 | ||
|
|
432220dc86 | ||
|
|
a921b4c7b4 | ||
|
|
d1348f2be4 | ||
|
|
f191f1c81d | ||
|
|
3dfd34b7bf | ||
|
|
ada142ff29 | ||
|
|
ec8b55c1e8 | ||
|
|
70eb9e6b08 | ||
|
|
7eef95f57c | ||
|
|
bac266eadc | ||
|
|
4ba9f6f330 | ||
|
|
0b0ae2fbc2 | ||
|
|
50bf7932a0 | ||
|
|
d3615d2961 | ||
|
|
2c572f8099 | ||
|
|
944aa861f8 | ||
|
|
62c3c91665 | ||
|
|
ecafda86f1 | ||
|
|
354741a81d | ||
|
|
63e6752bd1 | ||
|
|
1e1baa9781 | ||
|
|
94678a6726 | ||
|
|
64b202225a | ||
|
|
0d81dcde10 | ||
|
|
d0ccbc2963 | ||
|
|
8b29c1973d | ||
|
|
c960db5180 | ||
|
|
9233f0dfcc | ||
|
|
dc3c6cad05 | ||
|
|
77798085ce | ||
|
|
3c03f4b18f | ||
|
|
955c8b4346 | ||
|
|
a3759b1959 | ||
|
|
00e87dc3b0 | ||
|
|
13211249c9 | ||
|
|
c677fed916 | ||
|
|
cc5a28ca14 | ||
|
|
8cbae88dc3 | ||
|
|
ae890dab37 | ||
|
|
45ec315252 | ||
|
|
d27cd392f6 | ||
|
|
751008dcf2 | ||
|
|
c16b9968e1 | ||
|
|
ab798bee12 | ||
|
|
30561411e1 | ||
|
|
e1e8c1064a | ||
|
|
94ff01bf2c | ||
|
|
4b1b0e74df | ||
|
|
78205d77f3 | ||
|
|
53a0a41a02 | ||
|
|
8d437c31ce | ||
|
|
6f396087d2 | ||
|
|
a6cd5d2ddb | ||
|
|
261cfe3911 | ||
|
|
bafe23431e | ||
|
|
5759012914 | ||
|
|
c9dbf282f6 | ||
|
|
db1a335509 | ||
|
|
cdb64a4c15 | ||
|
|
fd7cbdbdc4 | ||
|
|
bcbfc09bf4 | ||
|
|
5f3212afc4 | ||
|
|
af7d072486 | ||
|
|
a93fd314ec | ||
|
|
501f4a856a | ||
|
|
55af79dfeb | ||
|
|
607a725395 | ||
|
|
ef602ca70b | ||
|
|
50d489d3c1 | ||
|
|
3c10d750f0 | ||
|
|
cab93b16b6 | ||
|
|
d3d2270f80 | ||
|
|
9b3b758be7 | ||
|
|
dfdaf596bc | ||
|
|
8ee8998381 | ||
|
|
de59434d95 | ||
|
|
e6f9d9b3c8 | ||
|
|
a41dc71965 | ||
|
|
dd200f6d34 | ||
|
|
b1c42b8d8f | ||
|
|
aba870f0ba | ||
|
|
0380fe1fff | ||
|
|
78c4a646d2 | ||
|
|
44056318fd | ||
|
|
f1bb2c75a6 | ||
|
|
dd0489596e | ||
|
|
8389e06aae | ||
|
|
afbb670c78 | ||
|
|
666da8d4e3 | ||
|
|
8bb42addc0 | ||
|
|
0dfd32412e | ||
|
|
54eee7ca9f | ||
|
|
ad936c7f6c | ||
|
|
ee5ec1b7d1 | ||
|
|
d2c5ae8aa9 | ||
|
|
9e3c4134e1 | ||
|
|
c8ea1d0c8a | ||
|
|
bba5c5472c | ||
|
|
29c3923a52 | ||
|
|
1c20929886 | ||
|
|
0078a2ef8e | ||
|
|
ee1c70f608 | ||
|
|
e29bc4e56a | ||
|
|
e1595daeb9 | ||
|
|
77e85f02de | ||
|
|
557892bbc7 | ||
|
|
f3bfc6f126 | ||
|
|
5937a0e3b2 | ||
|
|
94749e4615 | ||
|
|
ea093844b4 | ||
|
|
4d496de488 | ||
|
|
31bfc5388c | ||
|
|
e9cc86e072 | ||
|
|
9ed32307d3 | ||
|
|
23cfdb95f2 | ||
|
|
71ff695290 | ||
|
|
f48aeb0917 | ||
|
|
84c90dd587 | ||
|
|
4c186606bd | ||
|
|
2ffa5fbe36 | ||
|
|
471d18f7fa | ||
|
|
0985cea3d3 | ||
|
|
f0509ae333 | ||
|
|
127a20b29c | ||
|
|
f179030d6c | ||
|
|
76db7b3d21 | ||
|
|
00a3d67707 | ||
|
|
a165dd51c4 | ||
|
|
05eaa3ce54 | ||
|
|
d652bca32d | ||
|
|
0464030544 | ||
|
|
5927781a6b | ||
|
|
763efaf4af | ||
|
|
d9f4ce7347 | ||
|
|
218cab3935 | ||
|
|
81c6351c93 | ||
|
|
dce29857d3 | ||
|
|
3b9da0ecfd | ||
|
|
a728a4d779 | ||
|
|
9bae82592f | ||
|
|
50400459ee | ||
|
|
2ede730cfb | ||
|
|
45609e30ea | ||
|
|
20f817410f | ||
|
|
e276025d60 | ||
|
|
a89a7b5430 | ||
|
|
044807accb | ||
|
|
3e9f63a913 | ||
|
|
c80470d8f5 | ||
|
|
55d19bbc2d | ||
|
|
a3f4376d8f | ||
|
|
2466df0c3d | ||
|
|
425cf33f74 | ||
|
|
077a9937fe | ||
|
|
e6d41f6697 | ||
|
|
59791cdaa5 | ||
|
|
c7a76f90ea | ||
|
|
605e3c26d7 | ||
|
|
a59fc3f3e7 | ||
|
|
b5667c17d3 | ||
|
|
bc80aaea63 | ||
|
|
6746c9a2d3 | ||
|
|
11ad4d8a0f | ||
|
|
752f0177cf | ||
|
|
2078305ff4 | ||
|
|
924502113e | ||
|
|
c95e804733 | ||
|
|
6a356d4a45 | ||
|
|
2bfcbd8fb1 | ||
|
|
19bfcea412 | ||
|
|
0d7a013bba | ||
|
|
931b153290 | ||
|
|
9f91fffcab | ||
|
|
6aea27e30f | ||
|
|
134f2671fb | ||
|
|
6438d1ee57 | ||
|
|
a6a1d44204 | ||
|
|
cf97c7eb59 | ||
|
|
e8713ff260 | ||
|
|
993724d945 | ||
|
|
8c89fdec95 | ||
|
|
a49bfec7d9 | ||
|
|
050ecd9290 | ||
|
|
395bd604ce | ||
|
|
4f91c78b4f | ||
|
|
20405ae9ab | ||
|
|
c18640eab0 | ||
|
|
854584138b | ||
|
|
2d129f6233 | ||
|
|
4aa3e27880 | ||
|
|
094387335d | ||
|
|
ff1cf12e0c | ||
|
|
2f44beccc2 | ||
|
|
558e48f671 | ||
|
|
e60048f5fb | ||
|
|
6ad1dd1a77 | ||
|
|
4bfbdf31e4 | ||
|
|
71c2b4b7e8 | ||
|
|
292f743b14 | ||
|
|
dd6dfffd57 | ||
|
|
ec705a5307 | ||
|
|
f45f071710 | ||
|
|
4630e4f4a0 | ||
|
|
d872ce0ab6 | ||
|
|
c04b032abf | ||
|
|
b218bf1d2e | ||
|
|
18f787703a | ||
|
|
a3719b12e5 | ||
|
|
8c91311c1e | ||
|
|
096336042e | ||
|
|
4ca25fcfc2 | ||
|
|
12b4bd3795 | ||
|
|
ba96d587ab | ||
|
|
411f3be3aa | ||
|
|
ffacefb792 | ||
|
|
2d1689b79a | ||
|
|
78e0d30687 | ||
|
|
deb0952266 | ||
|
|
9f77cfd0bf | ||
|
|
767c6da325 | ||
|
|
f034d908b6 | ||
|
|
bc391d014e | ||
|
|
d42cd8ebf9 | ||
|
|
ac5635b671 | ||
|
|
c76b250c3b | ||
|
|
4630287924 | ||
|
|
d3cae8ec52 | ||
|
|
149fa72566 | ||
|
|
a0d13df033 | ||
|
|
c9a3fad9eb | ||
|
|
eeb96199f9 | ||
|
|
765b1a1b39 | ||
|
|
78eef7cb9d | ||
|
|
1ad9f8d9cf | ||
|
|
36734a4768 | ||
|
|
d0f91c5f43 | ||
|
|
1cd980f46a | ||
|
|
74162ab4e7 | ||
|
|
0ef5579540 | ||
|
|
9501f02aa2 | ||
|
|
ea8850f8bd | ||
|
|
949842118b | ||
|
|
a4d31711a0 | ||
|
|
94f0d040ea | ||
|
|
bb6e2b9336 | ||
|
|
d0166f7f12 | ||
|
|
3cef6d7a65 | ||
|
|
6a1bc00f04 | ||
|
|
bee4b3970d | ||
|
|
f3858546de | ||
|
|
de40cb8920 | ||
|
|
8ddf1315eb | ||
|
|
00d5b16de7 | ||
|
|
0b02bb417a | ||
|
|
d68260411d | ||
|
|
7227937660 | ||
|
|
2ac3640760 | ||
|
|
71de48fd32 | ||
|
|
834eceab16 | ||
|
|
20de1734d1 | ||
|
|
63e734b0aa | ||
|
|
cf53c56ac7 | ||
|
|
e82433c530 | ||
|
|
3a4e6aa852 | ||
|
|
6c46770330 | ||
|
|
e66e2c40a6 | ||
|
|
39bc0d5fb2 | ||
|
|
50e73e2a04 | ||
|
|
efa9875744 | ||
|
|
8182b0e95a | ||
|
|
6091621858 | ||
|
|
ba568581ff | ||
|
|
c78190c3a0 | ||
|
|
84c968e053 | ||
|
|
6b7da4068c | ||
|
|
6dc63907f1 | ||
|
|
e3e14e7a66 | ||
|
|
997ced3938 | ||
|
|
e2f149caf9 | ||
|
|
da94fc052d | ||
|
|
b18da9064b | ||
|
|
77a6304fc3 | ||
|
|
7c44f22c45 | ||
|
|
d39d62c2f8 | ||
|
|
8f68163f90 | ||
|
|
b3ee5f4d9a | ||
|
|
5398abb074 | ||
|
|
65397d3e1e | ||
|
|
887d53528b | ||
|
|
1603eafbb2 | ||
|
|
33bcb54aaf | ||
|
|
d36f87707a | ||
|
|
226c1fd6c5 | ||
|
|
74b961ab29 | ||
|
|
e4358ba489 | ||
|
|
26522cb061 | ||
|
|
7a2038e124 | ||
|
|
e98d84efdb | ||
|
|
7faffafd8a | ||
|
|
bedfd3ee30 | ||
|
|
94473bdeaa | ||
|
|
be128da9e0 | ||
|
|
381afcbb6d | ||
|
|
1dc008855e | ||
|
|
bbf4431b5f | ||
|
|
c38ad8e382 | ||
|
|
59af1fd62a | ||
|
|
55ae0bec93 | ||
|
|
64cb509d3b | ||
|
|
9d67a56f29 | ||
|
|
b5ee7100f4 | ||
|
|
ee1515d787 | ||
|
|
6f0e98b1a1 | ||
|
|
b8e4430a14 | ||
|
|
65e3187418 | ||
|
|
ea48cf3722 | ||
|
|
2c0ad1a4cc | ||
|
|
7de8fd8b53 | ||
|
|
7eddf4882c | ||
|
|
58e95610e8 | ||
|
|
1416bad9cc | ||
|
|
0e0e9eccf1 | ||
|
|
0dcc953100 | ||
|
|
38fffd4932 | ||
|
|
17c7ae7d7c | ||
|
|
15760117ee | ||
|
|
1cd5e0781d | ||
|
|
f64fbd1040 | ||
|
|
ceb9246cf2 | ||
|
|
032a631b61 | ||
|
|
5d0f6b665b | ||
|
|
4bbf096350 | ||
|
|
e1a950ec21 | ||
|
|
ff14fab492 | ||
|
|
df66d0441c | ||
|
|
ede8e7dfce | ||
|
|
4edfa4d244 | ||
|
|
9d155d6a0e | ||
|
|
1fa92eec57 | ||
|
|
7fc28fd1bb | ||
|
|
bb24da92e4 | ||
|
|
dd338f5f4b | ||
|
|
486bc5ac28 | ||
|
|
68215f37f5 | ||
|
|
4449d3fce8 | ||
|
|
cbb20439ed | ||
|
|
ec357b71f1 | ||
|
|
5acdb4dc31 | ||
|
|
dd959a7316 | ||
|
|
0b6dbe8bfa | ||
|
|
78e6263a2e | ||
|
|
7f5e3d3b4c | ||
|
|
66d5d3db8c | ||
|
|
3b2a4d1eb4 | ||
|
|
485195e035 | ||
|
|
d735f397d8 | ||
|
|
5e41481e27 | ||
|
|
d52c54d6fb | ||
|
|
36c85eed5e | ||
|
|
b09deb1494 | ||
|
|
71d22f6768 | ||
|
|
b68e9dfd4b | ||
|
|
b4362a4539 | ||
|
|
4e783d414c | ||
|
|
9fe6b9d0e5 | ||
|
|
cb4cce119b | ||
|
|
665c33d1a9 | ||
|
|
0ed3bfff4a | ||
|
|
9ba0c6df37 | ||
|
|
312b0d4665 | ||
|
|
7acbda4cda | ||
|
|
247fc6be8e | ||
|
|
1407a61f7a | ||
|
|
a3c302176b | ||
|
|
03130d3de2 | ||
|
|
80d42d92db | ||
|
|
115a596c1e | ||
|
|
ab5815a944 | ||
|
|
7e174a1b7d | ||
|
|
1f08940e47 | ||
|
|
8db5724b77 | ||
|
|
a21352ae4f | ||
|
|
a38f4978fa | ||
|
|
4f05ddab93 | ||
|
|
bbbeb155f0 | ||
|
|
0946254764 | ||
|
|
316dfd02e4 | ||
|
|
c35833fab1 | ||
|
|
81cd6ed764 | ||
|
|
63ff517b30 | ||
|
|
30a2a9aa2a | ||
|
|
cfc8570dca | ||
|
|
7841b44b43 | ||
|
|
e3f51b34b5 | ||
|
|
dcedced8fe | ||
|
|
e437597ef4 | ||
|
|
6d3f768e92 | ||
|
|
c114581400 | ||
|
|
fbfd792416 | ||
|
|
755f1d17d6 | ||
|
|
3e4f7fdc17 | ||
|
|
f6238cd6ab | ||
|
|
754f4e8244 | ||
|
|
a2925011ed | ||
|
|
d658b67e93 | ||
|
|
9b7b5ebfa8 | ||
|
|
b54777bccb | ||
|
|
17b9b8ec3a | ||
|
|
3dc2ab54ac | ||
|
|
97451d2b7a | ||
|
|
e2c675d9a5 | ||
|
|
3e3a600a60 | ||
|
|
1f0d2b147b | ||
|
|
c99aaf1e7d | ||
|
|
7e87dc9de6 | ||
|
|
f556fb61ca | ||
|
|
84dca5e794 | ||
|
|
60d4997985 | ||
|
|
d2900c347a | ||
|
|
285b0dc01d | ||
|
|
172153b840 | ||
|
|
ce71b8b5f6 | ||
|
|
d72d2b33bd | ||
|
|
d15fbe7801 | ||
|
|
3627f884c3 | ||
|
|
e89260b8b0 | ||
|
|
ab6267e122 | ||
|
|
731ec684f0 | ||
|
|
bbf43a0744 | ||
|
|
ae51189953 | ||
|
|
9b54c1aa04 | ||
|
|
caab1cae39 | ||
|
|
da04a0d161 | ||
|
|
9351b6c44a | ||
|
|
5d6231f51c | ||
|
|
945697c538 | ||
|
|
dced355a29 | ||
|
|
f133fdfb1c | ||
|
|
687c76703b | ||
|
|
e5558f7fa9 | ||
|
|
60b424705d | ||
|
|
6ed378086b | ||
|
|
66d7dde79e | ||
|
|
80ea1e95ee | ||
|
|
4098cef279 | ||
|
|
c7628ac07f | ||
|
|
8cd08fa765 | ||
|
|
1f493b208c | ||
|
|
3e8d175242 | ||
|
|
360240ee58 | ||
|
|
95c5fb7391 | ||
|
|
fcd221a9b0 | ||
|
|
397522292c | ||
|
|
d0aac18b88 | ||
|
|
fbf799e4c4 | ||
|
|
7bb35812ff | ||
|
|
ad49267b29 | ||
|
|
88404bcb56 | ||
|
|
33af6ec57e | ||
|
|
4b3a0bf4e2 | ||
|
|
508dd399be | ||
|
|
ad2dbd6fc5 | ||
|
|
a51bc276b5 | ||
|
|
132acaf758 | ||
|
|
cd807cd035 | ||
|
|
df547de5f6 | ||
|
|
e1ed362d7b | ||
|
|
4cbf7e2a2d | ||
|
|
1ec00bceed | ||
|
|
68ebe55ca4 | ||
|
|
be854ec14c | ||
|
|
ca8647578b | ||
|
|
c90a1ca9ea | ||
|
|
45734d52af | ||
|
|
da0850124c | ||
|
|
3eb86fd6c3 | ||
|
|
df2a0f6ec4 | ||
|
|
3b196fc90d | ||
|
|
6370296c53 | ||
|
|
6ffb0d9676 | ||
|
|
0e85b54cba | ||
|
|
d1b948aa56 | ||
|
|
5e85c4f8ec | ||
|
|
8b2466dcde | ||
|
|
c754d19015 | ||
|
|
ad680de897 | ||
|
|
78ae4d7a7d | ||
|
|
74acf2fe0f | ||
|
|
bab9fd8ec4 | ||
|
|
cfce36c212 | ||
|
|
5c6d6da4f9 | ||
|
|
5005d06507 | ||
|
|
9a09062a84 | ||
|
|
6dc993a276 | ||
|
|
c45ceec33b | ||
|
|
c28de99907 | ||
|
|
b587c08768 | ||
|
|
71043963e7 | ||
|
|
04b17a15bb | ||
|
|
4aa9d11574 | ||
|
|
56b5739cfc | ||
|
|
bb1704ed7a | ||
|
|
9163fc74d4 | ||
|
|
49113edeff | ||
|
|
718ede8439 | ||
|
|
5ee49d8dad | ||
|
|
c19885d730 | ||
|
|
d2e64468d5 | ||
|
|
e3b6a3308f | ||
|
|
c3f88ae0c8 | ||
|
|
31a5868864 | ||
|
|
4cb50a1dff | ||
|
|
2062703565 | ||
|
|
5e00f07fd8 | ||
|
|
3bf2110df1 | ||
|
|
c3ff60dced | ||
|
|
7cf3b8f1a9 | ||
|
|
4536d60e3e | ||
|
|
2f1ea4da67 | ||
|
|
34f6b412f6 | ||
|
|
b16a16d100 | ||
|
|
55750844ea | ||
|
|
6c09134552 | ||
|
|
8fa74c63c9 | ||
|
|
275ac351fe | ||
|
|
b728827324 | ||
|
|
2db99a3e32 | ||
|
|
263c011a7b | ||
|
|
8eeebb0cef | ||
|
|
f055e53987 | ||
|
|
6dce5f5931 | ||
|
|
14cfbf78bd | ||
|
|
6de577839b | ||
|
|
bfcdf3ef98 | ||
|
|
ffa7ddebb8 | ||
|
|
29838a433a | ||
|
|
1f1422bedd | ||
|
|
d41e634611 | ||
|
|
22cc890cab | ||
|
|
15e940bd2d | ||
|
|
c788f53bb9 | ||
|
|
10f1c5781e | ||
|
|
75b0869a77 | ||
|
|
c4c341922b | ||
|
|
43b4adc618 | ||
|
|
bad982dbdd | ||
|
|
5d45064c2d | ||
|
|
6390f4aa48 | ||
|
|
bb94d56bd0 | ||
|
|
14f3d9ab12 | ||
|
|
d9ecf0efb8 | ||
|
|
9b66b02e46 | ||
|
|
feca878fdd | ||
|
|
f53fd74873 | ||
|
|
bbaf892523 | ||
|
|
451b3abddf | ||
|
|
ba03add3d3 | ||
|
|
a41de7ed1c | ||
|
|
28de5bb097 | ||
|
|
d08a1224e2 | ||
|
|
327cf7ec75 | ||
|
|
83438129a2 | ||
|
|
9b3b7fc8ff | ||
|
|
8000497302 | ||
|
|
84589a4b40 | ||
|
|
067129f5a9 | ||
|
|
ffcf98b06b | ||
|
|
b2f001e416 | ||
|
|
44c31c9c61 | ||
|
|
8aa659eee2 | ||
|
|
51bf163149 | ||
|
|
d5c02f6b94 | ||
|
|
55fa968afb | ||
|
|
a9aac33a46 | ||
|
|
1f45506b37 | ||
|
|
9d9d88b171 | ||
|
|
291da3f7fe | ||
|
|
70cfbc3715 | ||
|
|
9d80c2cea7 | ||
|
|
a7f7959f91 | ||
|
|
082268d9fc | ||
|
|
945ef2f1b0 | ||
|
|
1f1feed3ae | ||
|
|
290890f6e7 | ||
|
|
b7bfd803eb | ||
|
|
2151905d46 | ||
|
|
fdde118af1 | ||
|
|
5f9ba2e04b | ||
|
|
ca871f654b | ||
|
|
3c91ac27dc | ||
|
|
94b4cb0baf | ||
|
|
1b956c6ad7 | ||
|
|
b9b79bbd9a | ||
|
|
9802581301 | ||
|
|
c1f39fbf57 | ||
|
|
f66e7712c3 | ||
|
|
2bb6d985cc | ||
|
|
d300ed38ea | ||
|
|
9cbb810fe4 | ||
|
|
cfa486d8cc | ||
|
|
ab1924d266 | ||
|
|
d6de0f6fa8 | ||
|
|
d1afea104e | ||
|
|
fae8cf83cd | ||
|
|
5a69ac074f | ||
|
|
15f0560005 | ||
|
|
6f7fa6abd9 | ||
|
|
cbd55b0366 | ||
|
|
f1dbd3018d | ||
|
|
502613b433 | ||
|
|
2bdc0d59cf | ||
|
|
ce0ee49ebf | ||
|
|
bc3bdb1221 | ||
|
|
3bea7576b5 | ||
|
|
d70b5d7dc0 | ||
|
|
eb30c1e5c7 | ||
|
|
17cbd2623f | ||
|
|
dc27a44c0c | ||
|
|
f72b02ab0b | ||
|
|
c0946f1e0c | ||
|
|
575fdcb8cd | ||
|
|
783ea7901c | ||
|
|
44e6bb79a2 | ||
|
|
ab3b9f13b5 | ||
|
|
dc6e7f7b1b | ||
|
|
94b75dda24 | ||
|
|
a529879889 | ||
|
|
4b7bbb3d50 | ||
|
|
88c98efd94 | ||
|
|
147be12583 | ||
|
|
137e047205 | ||
|
|
4bd6db31c0 | ||
|
|
1350638fb3 | ||
|
|
b6de431a56 | ||
|
|
1aef27da33 | ||
|
|
1e78979ed0 | ||
|
|
ba90ebda4c | ||
|
|
7c1bade54d | ||
|
|
a46b394714 | ||
|
|
6dff1136c5 | ||
|
|
ccbb56d403 | ||
|
|
4f3a7e5451 | ||
|
|
e4cda4087e | ||
|
|
6eeb8de02c | ||
|
|
29c4fec90a | ||
|
|
5c9ba8de43 | ||
|
|
2358f6a9c9 | ||
|
|
7097279dca | ||
|
|
c3eb553425 | ||
|
|
24f64fac6b | ||
|
|
4cdd5e9f20 | ||
|
|
de9e261807 | ||
|
|
c26793c68d | ||
|
|
11fd833cdb | ||
|
|
2b21a0c31f | ||
|
|
7e888f6408 | ||
|
|
bba70ce852 | ||
|
|
e96b5af0c8 | ||
|
|
5f5e786c0e | ||
|
|
8d66e43117 | ||
|
|
ccbc809ecf | ||
|
|
e67fde24f1 | ||
|
|
923494fdce | ||
|
|
73bff2cabe | ||
|
|
c20fc5a345 | ||
|
|
f71c62f167 | ||
|
|
bdaf9e6dc6 | ||
|
|
a2730fb17c | ||
|
|
5061ddf38e | ||
|
|
1735c036cc | ||
|
|
fa703db41e | ||
|
|
9665f4136a | ||
|
|
abeef2be4a | ||
|
|
995420354e | ||
|
|
1af1a1863a | ||
|
|
fd04f8be5a | ||
|
|
ccc9e6dcfb | ||
|
|
9e23710c6d | ||
|
|
3878a3ee0b | ||
|
|
0c9d0a4d15 | ||
|
|
739c007c95 | ||
|
|
6f27253441 | ||
|
|
b900932574 | ||
|
|
0903b31fcf | ||
|
|
d080f5db0f | ||
|
|
5029a1625e | ||
|
|
2950f167aa | ||
|
|
afdf9a3dfb | ||
|
|
132ca9d106 | ||
|
|
990d75d42a | ||
|
|
1c48aa8444 | ||
|
|
67e07813cb | ||
|
|
7fbeed88a9 | ||
|
|
9053e7ac88 | ||
|
|
e3c94cc1f7 | ||
|
|
e8d6c4d451 | ||
|
|
ba1813c767 | ||
|
|
bcd8992abc | ||
|
|
557b532f74 | ||
|
|
d450464a1a | ||
|
|
5d10b28433 | ||
|
|
eafe358deb | ||
|
|
048bd877da | ||
|
|
7f18dd942f | ||
|
|
424234ec48 | ||
|
|
4168a08276 | ||
|
|
996063d76d | ||
|
|
085f5ff22f | ||
|
|
5b5dc6a8cc | ||
|
|
4103bad8de | ||
|
|
d6a8563cc7 | ||
|
|
c9ef0bcd7b | ||
|
|
6b978759ca | ||
|
|
bc31d54028 | ||
|
|
7bad0e04b1 | ||
|
|
7144c746c6 | ||
|
|
d5592e5662 | ||
|
|
12b027110e | ||
|
|
644896faff | ||
|
|
f41f7c0769 | ||
|
|
68f1a891ca | ||
|
|
b186311968 | ||
|
|
c21b594cf0 | ||
|
|
21ec46843a | ||
|
|
4da827501a | ||
|
|
5d0c7bfd32 | ||
|
|
d583409af4 | ||
|
|
559e607601 | ||
|
|
276b27d8a6 | ||
|
|
2134f42cfd | ||
|
|
88135d2750 | ||
|
|
1b1a4bcad4 | ||
|
|
af8e82a860 | ||
|
|
c8d5e37b44 | ||
|
|
781fe03b5d | ||
|
|
702103aa66 | ||
|
|
d2503431c6 | ||
|
|
dccdf18226 | ||
|
|
a92f287256 | ||
|
|
a193d67f11 | ||
|
|
6873e113bb | ||
|
|
77860d9d05 | ||
|
|
de97010cfb | ||
|
|
89245c7af7 | ||
|
|
d51745774f | ||
|
|
deda1daa04 | ||
|
|
8594c4a7eb | ||
|
|
d0d73aa5e2 | ||
|
|
1f43bb201c | ||
|
|
6e92d31f5d | ||
|
|
ae4070b7f2 | ||
|
|
ee2ac18b69 | ||
|
|
85ce697dad | ||
|
|
82eb79ce40 | ||
|
|
c76b4b9ede | ||
|
|
9c2e814e16 | ||
|
|
2e9499ea90 | ||
|
|
52296bfed1 | ||
|
|
a53d54a1d6 | ||
|
|
81cf08b723 | ||
|
|
f192d7dffc | ||
|
|
5425896988 | ||
|
|
9e2be00b5c | ||
|
|
06004ce478 | ||
|
|
5bda018d25 | ||
|
|
b12729a874 | ||
|
|
d3a84e02e4 | ||
|
|
9865c84df5 | ||
|
|
33d2cb9a49 | ||
|
|
37c121e8de | ||
|
|
9dd0bf01e2 | ||
|
|
92ac601072 | ||
|
|
dfd7647838 | ||
|
|
058246e2ce | ||
|
|
8ed789892b | ||
|
|
57775af24b | ||
|
|
845681b6dc | ||
|
|
ea2c9cbc9a | ||
|
|
96bb3013a3 | ||
|
|
aeee37fdae | ||
|
|
551497bfeb | ||
|
|
57057cac0e | ||
|
|
dccd9f09e7 | ||
|
|
b91e474343 | ||
|
|
f25262c191 | ||
|
|
723662a6bc | ||
|
|
757fcd3ea4 | ||
|
|
ec3e28436d | ||
|
|
6a291ef1e2 | ||
|
|
1e529a9e19 | ||
|
|
05d393e7fe | ||
|
|
c757ace2b4 | ||
|
|
5ab066de5f | ||
|
|
b0ae22d493 | ||
|
|
03a6abcfc6 | ||
|
|
e58491ed97 | ||
|
|
1e92ae05c0 | ||
|
|
513b6181a4 | ||
|
|
05b54bc6f5 | ||
|
|
1d4634a76c | ||
|
|
342cf12ae7 | ||
|
|
6883dbbce1 | ||
|
|
a3d3706b89 | ||
|
|
da911e374a | ||
|
|
26904f4e0e | ||
|
|
ec5601f3ca | ||
|
|
a2ee2a5e6b | ||
|
|
78d5f8b76d | ||
|
|
dd0cc00004 | ||
|
|
825836c447 | ||
|
|
4996eb9e3c | ||
|
|
0bc3a3c34e | ||
|
|
ca5bc313ee | ||
|
|
069df92dbf | ||
|
|
bdce4ee9f9 | ||
|
|
756a136124 | ||
|
|
4809ef3537 | ||
|
|
2c056d6807 | ||
|
|
90af466a2f | ||
|
|
20cbbcd9f4 | ||
|
|
036953044e | ||
|
|
499456fa77 | ||
|
|
22d9705412 | ||
|
|
663c853aff | ||
|
|
103ffa4761 | ||
|
|
3baedf01d1 | ||
|
|
bfad6d34b5 | ||
|
|
2aa2564078 | ||
|
|
6561bb5a6c | ||
|
|
ac75176292 | ||
|
|
1005079f71 | ||
|
|
e0e07a9deb | ||
|
|
c65005e5a6 | ||
|
|
64f2f82e0c | ||
|
|
98495c8114 | ||
|
|
d0ac0b7804 | ||
|
|
e45c31345e | ||
|
|
23cd677133 | ||
|
|
805c1298fb | ||
|
|
5199edff1e | ||
|
|
f3adc13c6d | ||
|
|
40f5f7026d | ||
|
|
b8b96763cf | ||
|
|
80b94fcd00 | ||
|
|
8d09d3c654 | ||
|
|
03564b3a82 | ||
|
|
a7569256d0 | ||
|
|
b0d4a094c1 | ||
|
|
1b232adc72 | ||
|
|
ed81e095ee | ||
|
|
76734b77f1 | ||
|
|
d8284ec09f | ||
|
|
6e982acde8 | ||
|
|
db6f8eaba1 | ||
|
|
eb7ad7163f | ||
|
|
6a478eec5e | ||
|
|
1dc08d6399 | ||
|
|
c3cfed5ac3 | ||
|
|
aba1dc93d9 | ||
|
|
d9bbf35afc | ||
|
|
dec3abfab9 | ||
|
|
bb98f13b19 | ||
|
|
8b10970e03 | ||
|
|
b859ab9d78 | ||
|
|
26208103fc | ||
|
|
d150b017e3 | ||
|
|
49414adfd2 | ||
|
|
5c675c7ce7 | ||
|
|
f16aaf7874 | ||
|
|
d80831e708 | ||
|
|
3e3e7156ec | ||
|
|
dbb2a365cb | ||
|
|
52a8c7288d | ||
|
|
42f6bf6182 | ||
|
|
9d4d3738ff | ||
|
|
1647a6d0a7 | ||
|
|
e5e058672d | ||
|
|
f5cd8e2523 | ||
|
|
fd2cc8aff1 | ||
|
|
f244fb837b | ||
|
|
b4848ab7f9 | ||
|
|
0fa60f0502 | ||
|
|
8d290317d7 | ||
|
|
cb5c8f3a85 | ||
|
|
5a0f4c1462 | ||
|
|
e4445413fd | ||
|
|
9be13eb0b9 | ||
|
|
2c7eeeca7b | ||
|
|
b59a552288 | ||
|
|
def67899ed | ||
|
|
ea0870c180 | ||
|
|
379d57ca8e | ||
|
|
761df2b4cc | ||
|
|
56e1f02f69 | ||
|
|
f921ef4708 | ||
|
|
f11dc9bc25 | ||
|
|
0ebfcd7238 | ||
|
|
5fb9cd142f | ||
|
|
7d5e112efb | ||
|
|
e82dd816de | ||
|
|
887a245d82 | ||
|
|
2f706b33fa | ||
|
|
dcbdb04009 | ||
|
|
008903cbbd | ||
|
|
b67113fc1f | ||
|
|
3afbe832cc | ||
|
|
9adfa0ecfc | ||
|
|
f081d7fd3c | ||
|
|
394a3253aa | ||
|
|
13890d2835 | ||
|
|
6fd3e567cd | ||
|
|
b6d8e55b00 | ||
|
|
e6d3d347ab | ||
|
|
c159ce7eb9 | ||
|
|
65c9bf22dc | ||
|
|
87dfffeddb | ||
|
|
8d6c676fed | ||
|
|
324d27896b | ||
|
|
fd5fcf356f | ||
|
|
0dde5a9d2b | ||
|
|
73f7603c1d | ||
|
|
5faffc3886 | ||
|
|
d09d5a7dbe | ||
|
|
b906fecdff | ||
|
|
a6e4122e44 | ||
|
|
e663ecb458 | ||
|
|
95876e28bf | ||
|
|
348932d929 | ||
|
|
bd28516324 | ||
|
|
d1fc050bed | ||
|
|
bace01e4f7 | ||
|
|
0a56ee7dbb | ||
|
|
c90be99216 | ||
|
|
a0a431e0e2 | ||
|
|
4489e7149c | ||
|
|
ec81420894 | ||
|
|
619dd0c99d | ||
|
|
9f5a5108fb | ||
|
|
974510b2c7 | ||
|
|
f2f10f0c79 | ||
|
|
39cbcd4d6f | ||
|
|
1accdfcafb | ||
|
|
e37bbe420c | ||
|
|
58c4455076 | ||
|
|
a413ffb9d2 | ||
|
|
1f96622e74 | ||
|
|
9527cf6abf | ||
|
|
13f0bc3296 | ||
|
|
58a0ec9cca | ||
|
|
fe385de342 | ||
|
|
ae5fb26387 | ||
|
|
cd67ab03ff | ||
|
|
94f134f3fe | ||
|
|
ca95e44a81 | ||
|
|
ecc045b2a8 | ||
|
|
9c205a07c5 | ||
|
|
c414354708 | ||
|
|
ddacd2d9d7 | ||
|
|
987fcb4a5a | ||
|
|
4c70da28e6 | ||
|
|
246684f28c | ||
|
|
e450a0e096 | ||
|
|
90d2144588 | ||
|
|
4302d314cf | ||
|
|
22322a55ed | ||
|
|
d09c0436e0 | ||
|
|
9b619216cb | ||
|
|
fe0a855618 | ||
|
|
3bca7c9a13 | ||
|
|
87887494a5 | ||
|
|
614a8cb14b | ||
|
|
da6e64e89f | ||
|
|
87d0db0b5c | ||
|
|
c756df90fc | ||
|
|
d3053d8ce2 | ||
|
|
4c728cf777 | ||
|
|
17b90af972 | ||
|
|
a0339f5ae2 | ||
|
|
732aaffbb6 | ||
|
|
d50606ce13 | ||
|
|
777c9db0f6 | ||
|
|
815397dba6 | ||
|
|
01f361e7cd | ||
|
|
1278776297 | ||
|
|
a3a8e515bf | ||
|
|
96af9afc83 | ||
|
|
c841c8c284 | ||
|
|
d6ee8ccb2d | ||
|
|
a7930d8403 | ||
|
|
eeeb889ba7 | ||
|
|
f1f4147628 | ||
|
|
42118e0169 | ||
|
|
b67d69a5c8 | ||
|
|
d8144c901d | ||
|
|
e9d9cf7e48 | ||
|
|
0867d7fe0e | ||
|
|
b646331adc | ||
|
|
30109a54ce | ||
|
|
e85ca7dcd3 | ||
|
|
faf05ceb72 | ||
|
|
049e37ba82 | ||
|
|
d187ee23ea | ||
|
|
20b0c9653d | ||
|
|
022b667858 | ||
|
|
1b4af09185 | ||
|
|
58ca4efd42 | ||
|
|
f1bb183017 | ||
|
|
081c11c503 | ||
|
|
d54843635a | ||
|
|
12638275fa | ||
|
|
25c50435ca | ||
|
|
e1c61cfadb | ||
|
|
06fd860964 | ||
|
|
ab4e1f63c5 | ||
|
|
fb998a7e6a | ||
|
|
dbbbfc1170 | ||
|
|
eb432d9acb | ||
|
|
23f65b9eb2 | ||
|
|
c11ea4fe0d | ||
|
|
6b1cfffb6f | ||
|
|
918c0315eb | ||
|
|
106517fc8b | ||
|
|
96070d6b42 | ||
|
|
b22473d0d0 | ||
|
|
ef842b6c52 | ||
|
|
e9415c1708 | ||
|
|
0114efc66f | ||
|
|
84e7847c16 | ||
|
|
3f935942ea | ||
|
|
8f45322b7b | ||
|
|
6e095c1085 | ||
|
|
acaceefc89 | ||
|
|
21c3a4bee8 | ||
|
|
a959af80b5 | ||
|
|
e0d7e7698e | ||
|
|
dbf9912c5a | ||
|
|
f9038b9180 | ||
|
|
506aacbd83 | ||
|
|
63e31d672b | ||
|
|
0d5d353e99 | ||
|
|
766ec6a6e4 | ||
|
|
872effb297 | ||
|
|
fb13b79a76 | ||
|
|
706daeb678 | ||
|
|
9d6af82f9c | ||
|
|
2bc37027dd | ||
|
|
e70b6b210e | ||
|
|
df8a36c695 | ||
|
|
702af4b1c8 | ||
|
|
cd15e11ce3 | ||
|
|
ca8b236ccb | ||
|
|
06b057f4a2 | ||
|
|
0158c9b434 | ||
|
|
368728d12b | ||
|
|
c1cab103aa | ||
|
|
d9da85638c | ||
|
|
a25989d213 | ||
|
|
6bfe4fe258 | ||
|
|
e7cbb8cc77 | ||
|
|
06354efa76 | ||
|
|
345f05b23f | ||
|
|
f0489cb918 | ||
|
|
d0b265985b | ||
|
|
82f0902fa4 | ||
|
|
1dcb44f81a | ||
|
|
9bbc421407 | ||
|
|
54f335946e | ||
|
|
d95d8278de | ||
|
|
c813339945 | ||
|
|
cc44bc9d7f | ||
|
|
4cd655fb36 | ||
|
|
5845c37672 | ||
|
|
9ae81779ff | ||
|
|
df4aa64883 | ||
|
|
70f94322ee | ||
|
|
c2ccb4edda | ||
|
|
f42fafb25f | ||
|
|
0bccf9d358 | ||
|
|
d3f105d9b9 | ||
|
|
5c240316ee | ||
|
|
2fde0c6d8c | ||
|
|
76d78f34ee | ||
|
|
3efb3a99e9 | ||
|
|
65dfc3b878 | ||
|
|
b820aad3fc | ||
|
|
09d4cae6f4 | ||
|
|
85308e52a7 | ||
|
|
4f42ae7efc | ||
|
|
7cc2eac79d | ||
|
|
e3487b4845 | ||
|
|
354b2facf7 | ||
|
|
0d381fd5e9 | ||
|
|
65b7d64e92 | ||
|
|
e79115d719 | ||
|
|
be4f49e96d | ||
|
|
6b24a71ad7 | ||
|
|
1ccacdc600 | ||
|
|
3ea71e1dfb | ||
|
|
d8e324a005 | ||
|
|
7122e878a5 | ||
|
|
c8848a9e76 | ||
|
|
84499dab35 | ||
|
|
e9c695f76a | ||
|
|
029ade8dd6 | ||
|
|
0ebb509205 | ||
|
|
9fa59310d2 | ||
|
|
1f8e6cc82b | ||
|
|
1a4ce643fc |
61
.github/ISSUE_TEMPLATE/bug_report.md
vendored
61
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,35 +1,50 @@
|
|||||||
---
|
---
|
||||||
name: Bug report
|
name: Problem Report/Change Request
|
||||||
about: Create a report to help us improve
|
about: Create a Report to help us improve
|
||||||
title: ''
|
|
||||||
labels: bug
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Before creating a new issue please check that you have:*
|
<!-- Thanks for reporting an issue for this project. READ THIS FIRST:
|
||||||
|
|
||||||
* *searched the existing [issues](https://github.com/emsesp/EMS-ESP32/issues) (both open and closed)*
|
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
|
||||||
* *searched the [documentation help section](https://emsesp.github.io/docs)*
|
|
||||||
|
|
||||||
*Completing this template will help developers and contributors to address the issue. Try to be as specific and extensive as possible. If the information provided is not enough the issue will likely be closed.*
|
Please take a few minutes to complete the requested information below.
|
||||||
|
|
||||||
*You can now remove this line and the above ones. Text in italic is meant to be replaced by your own words. If any of the sections below are not relevant to the issue (for instance, the screenshots) then you can delete them.*
|
-->
|
||||||
|
|
||||||
**Bug description**
|
### DESCRIPTION
|
||||||
*A clear and concise description of what the bug is. Mention which EMS-ESP version you're using.*
|
|
||||||
|
|
||||||
**Steps to reproduce**
|
_A clear and concise description of what the problem is or the change requested._
|
||||||
*Steps to reproduce the behavior.*
|
|
||||||
|
|
||||||
**Expected behavior**
|
### REQUESTED INFORMATION
|
||||||
*A clear and concise description of what you expected to happen.*
|
|
||||||
|
|
||||||
**Screenshots**
|
_Make sure your have performed every step and checked the applicable boxes before submitting your issue. Thank you!_
|
||||||
*If applicable, add screenshots to help explain your problem.*
|
|
||||||
|
|
||||||
**Device information**
|
- [ ] Searched the issue in [issues](https://github.com/emsesp/EMS-ESP32/issues)
|
||||||
*Copy-paste here the information as it is outputted by the device. You can get this information by from http://ems-esp.local/api/system*
|
- [ ] 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`
|
||||||
|
|
||||||
**Additional context**
|
```json
|
||||||
*Add any other context about the problem here.*
|
Paste System information here....
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### TO REPRODUCE
|
||||||
|
|
||||||
|
_Steps to reproduce the behavior:_
|
||||||
|
|
||||||
|
### EXPECTED BEHAVIOUR
|
||||||
|
|
||||||
|
_A clear and concise description of what you expected to happen._
|
||||||
|
|
||||||
|
### SCREENSHOTS
|
||||||
|
|
||||||
|
_If applicable, add screenshots to help explain your issue._
|
||||||
|
|
||||||
|
### ADDITIONAL CONTEXT
|
||||||
|
|
||||||
|
_Add any other context about the issue here._
|
||||||
|
|
||||||
|
**(Please remember to close the issue when it has been addressed)**
|
||||||
|
|||||||
11
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
11
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: EMS-ESP 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
|
||||||
|
about: EMS-ESP usage Questions, Feature Requests and Projects.
|
||||||
|
- name: EMS-ESP Users Chat
|
||||||
|
url: https://discord.gg/3J3GgnzpyT
|
||||||
|
about: Chat for feedback, questions and troubleshooting.
|
||||||
26
.github/ISSUE_TEMPLATE/feature_request.md
vendored
26
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,26 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest an idea for this project
|
|
||||||
title: ''
|
|
||||||
labels: enhancement
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Before creating a new feature request please check that you have searched the existing [issues](https://github.com/emsesp/EMS-ESP32/issues) (both open and closed)*
|
|
||||||
|
|
||||||
*Completing this template will help developers and contributors evaluating the feature. If the information provided is not enough the issue will likely be closed.*
|
|
||||||
|
|
||||||
*You can now remove this line and the above ones. Text in italic is meant to be replaced by your own words. If any of the sections below are not relevant to the request then you can delete them.*
|
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
|
||||||
*A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]*
|
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
|
||||||
*A clear and concise description of what you want to happen.*
|
|
||||||
|
|
||||||
**Describe alternatives you've considered**
|
|
||||||
*A clear and concise description of any alternative solutions or features you've considered.*
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
*Add any other context or screenshots about the feature request here.*
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
---
|
|
||||||
name: Questions & Troubleshooting
|
|
||||||
about: Anything not a bug or feature request
|
|
||||||
title: ''
|
|
||||||
labels: question
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Before creating a new issue please check that you have:*
|
|
||||||
|
|
||||||
* *searched the existing [issues](https://github.com/emsesp/EMS-ESP32/issues) (both open and closed)*
|
|
||||||
* *searched the [documentation help section](https://emsesp.github.io/docs)*
|
|
||||||
|
|
||||||
*Completing this template will help developers and contributors help you. Try to be as specific and extensive as possible. If the information provided is not enough the issue will likely be closed.*
|
|
||||||
|
|
||||||
*You can now remove this line and the above ones. Text in italic is meant to be replaced by your own words. If any of the sections below are not relevant to the issue (for instance, the screenshots) then you can delete them.*
|
|
||||||
|
|
||||||
**Question**
|
|
||||||
*A clear and concise description of what the problem/doubt is.*
|
|
||||||
|
|
||||||
**Screenshots**
|
|
||||||
*If applicable, add screenshots to help explain your problem.*
|
|
||||||
|
|
||||||
**Device information**
|
|
||||||
*Copy-paste here the information as it is outputted by the device. You can get this information from http://ems-esp.local/api/system*
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
*Add any other context about the problem here.*
|
|
||||||
25
.github/workflows/github-releases-to-discord.yml
vendored
Normal file
25
.github/workflows/github-releases-to-discord.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
name: 'github-releases-to-discord'
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
github-releases-to-discord:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: GitHub Releases To Discord
|
||||||
|
uses: SethCohen/github-releases-to-discord@v1.13.1
|
||||||
|
with:
|
||||||
|
webhook_url: ${{ secrets.WEBHOOK_URL }}
|
||||||
|
color: '2105893'
|
||||||
|
username: 'Release Changelog'
|
||||||
|
avatar_url: 'https://cdn.discordapp.com/icons/816637840644505620/0b14718532d855c452903851b4f0c9a2.png'
|
||||||
|
content: '||@everyone||'
|
||||||
|
footer_title: 'Changelog'
|
||||||
|
footer_icon_url: 'https://cdn.discordapp.com/icons/816637840644505620/0b14718532d855c452903851b4f0c9a2.png'
|
||||||
|
footer_timestamp: true
|
||||||
55
.github/workflows/pre_release.yml
vendored
55
.github/workflows/pre_release.yml
vendored
@@ -1,57 +1,64 @@
|
|||||||
name: "pre-release"
|
name: 'pre-release'
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- "dev"
|
- 'dev'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
pre-release:
|
pre-release:
|
||||||
|
name: 'Automatic pre-release build'
|
||||||
name: "Automatic pre-release build"
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/checkout@v2
|
- name: Enable Corepack
|
||||||
- uses: actions/setup-python@v2
|
run: corepack enable
|
||||||
- uses: actions/setup-node@v2
|
|
||||||
|
- name: Install python 3.11
|
||||||
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
node-version: '16'
|
python-version: '3.11'
|
||||||
|
|
||||||
- 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
|
id: build_info
|
||||||
run: |
|
run: |
|
||||||
version=`grep -E '^#define EMSESP_APP_VERSION' ./src/version.h | awk -F'"' '{print $2}'`
|
version=`grep -E '^#define EMSESP_APP_VERSION' ./src/version.h | awk -F'"' '{print $2}'`
|
||||||
echo "::set-output name=version::$version"
|
echo "VERSION=$version" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Install PlatformIO
|
- name: Install PlatformIO
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
pip install -U platformio
|
pip install -U platformio
|
||||||
platformio upgrade
|
|
||||||
platformio update
|
|
||||||
|
|
||||||
- name: Build WebUI
|
- name: Build WebUI
|
||||||
run: |
|
run: |
|
||||||
cd interface
|
cd interface
|
||||||
npm ci
|
yarn install
|
||||||
npm run build
|
yarn typesafe-i18n --no-watch
|
||||||
|
sed -i "s/= 'pl'/= 'en'/" ./src/i18n/i18n-util.ts
|
||||||
|
yarn build
|
||||||
|
yarn webUI
|
||||||
|
|
||||||
- name: Build firmware
|
- name: Build all PIO target environments from default_envs
|
||||||
run: |
|
run: |
|
||||||
platformio run -e ci
|
platformio run
|
||||||
|
|
||||||
- name: Create a GH Release
|
- name: Create GitHub Release
|
||||||
id: "automatic_releases"
|
id: 'automatic_releases'
|
||||||
uses: "marvinpinto/action-automatic-releases@latest"
|
uses: emsesp/action-automatic-releases@v1.0.0
|
||||||
with:
|
with:
|
||||||
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
repo_token: '${{ secrets.GITHUB_TOKEN }}'
|
||||||
title: Development Build v${{steps.build_info.outputs.version}}
|
title: Development Build v${{steps.build_info.outputs.VERSION}}
|
||||||
automatic_release_tag: "latest"
|
automatic_release_tag: 'latest'
|
||||||
prerelease: true
|
prerelease: true
|
||||||
files: |
|
files: |
|
||||||
CHANGELOG_LATEST.md
|
CHANGELOG_LATEST.md
|
||||||
./build/firmware/*.*
|
./build/firmware/*.*
|
||||||
|
|
||||||
|
|||||||
59
.github/workflows/sonar_check.yml
vendored
59
.github/workflows/sonar_check.yml
vendored
@@ -1,57 +1,34 @@
|
|||||||
|
# see https://github.com/marketplace/actions/sonarcloud-scan-for-c-and-c#usage
|
||||||
name: Sonar Check
|
name: Sonar Check
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- dev
|
- dev
|
||||||
pull_request:
|
# pull_request:
|
||||||
types: [opened, synchronize, reopened]
|
# types: [opened, synchronize, reopened]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build
|
name: Build and analyze
|
||||||
|
if: github.repository == 'emsesp/EMS-ESP32'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.repository_owner == 'emsesp'
|
|
||||||
# if: github.repository == 'emsesp/EMS-ESP32'
|
|
||||||
env:
|
env:
|
||||||
# https://binaries.sonarsource.com/?prefix=Distribution/sonar-scanner-cli/
|
BUILD_WRAPPER_OUT_DIR: bw-output
|
||||||
SONAR_SCANNER_VERSION: 4.7.0.2747
|
|
||||||
SONAR_SERVER_URL: "https://sonarcloud.io"
|
|
||||||
BUILD_WRAPPER_OUT_DIR: build_wrapper_output_directory
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
fetch-depth: 0
|
||||||
- name: Set up JDK 11
|
|
||||||
uses: actions/setup-java@v1
|
- name: Install sonar-scanner and build-wrapper
|
||||||
with:
|
uses: SonarSource/sonarcloud-github-c-cpp@v2
|
||||||
java-version: 11
|
|
||||||
- name: Cache SonarCloud packages
|
|
||||||
uses: actions/cache@v1
|
|
||||||
with:
|
|
||||||
path: ~/.sonar/cache
|
|
||||||
key: ${{ runner.os }}-sonar
|
|
||||||
restore-keys: ${{ runner.os }}-sonar
|
|
||||||
- name: Download and set up sonar-scanner
|
|
||||||
env:
|
|
||||||
SONAR_SCANNER_DOWNLOAD_URL: https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-${{ env.SONAR_SCANNER_VERSION }}-linux.zip
|
|
||||||
run: |
|
|
||||||
mkdir -p $HOME/.sonar
|
|
||||||
curl -sSLo $HOME/.sonar/sonar-scanner.zip ${{ env.SONAR_SCANNER_DOWNLOAD_URL }}
|
|
||||||
unzip -o $HOME/.sonar/sonar-scanner.zip -d $HOME/.sonar/
|
|
||||||
echo "$HOME/.sonar/sonar-scanner-${{ env.SONAR_SCANNER_VERSION }}-linux/bin" >> $GITHUB_PATH
|
|
||||||
- name: Download and set up build-wrapper
|
|
||||||
env:
|
|
||||||
BUILD_WRAPPER_DOWNLOAD_URL: ${{ env.SONAR_SERVER_URL }}/static/cpp/build-wrapper-linux-x86.zip
|
|
||||||
run: |
|
|
||||||
curl -sSLo $HOME/.sonar/build-wrapper-linux-x86.zip ${{ env.BUILD_WRAPPER_DOWNLOAD_URL }}
|
|
||||||
unzip -o $HOME/.sonar/build-wrapper-linux-x86.zip -d $HOME/.sonar/
|
|
||||||
echo "$HOME/.sonar/build-wrapper-linux-x86" >> $GITHUB_PATH
|
|
||||||
- name: Run build-wrapper
|
- name: Run build-wrapper
|
||||||
run: |
|
run: build-wrapper-linux-x86-64 --out-dir ${{ env.BUILD_WRAPPER_OUT_DIR }} make all
|
||||||
make clean
|
|
||||||
build-wrapper-linux-x86-64 --out-dir ${{ env.BUILD_WRAPPER_OUT_DIR }} make clean all
|
|
||||||
- name: Run sonar-scanner
|
- name: Run sonar-scanner
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||||
run: |
|
run: sonar-scanner --define sonar.cfamily.compile-commands="${{ env.BUILD_WRAPPER_OUT_DIR }}/compile_commands.json"
|
||||||
sonar-scanner
|
|
||||||
|
|||||||
45
.github/workflows/tagged_release.yml
vendored
45
.github/workflows/tagged_release.yml
vendored
@@ -1,45 +1,54 @@
|
|||||||
name: "tagged-release"
|
name: 'tagged-release'
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- "v*"
|
- 'v*'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
tagged-release:
|
tagged-release:
|
||||||
|
name: 'Tagged Release'
|
||||||
name: "Tagged Release"
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/checkout@v2
|
- name: Enable Corepack
|
||||||
- uses: actions/setup-python@v2
|
run: corepack enable
|
||||||
- uses: actions/setup-node@v2
|
|
||||||
|
- name: Install python 3.11
|
||||||
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
node-version: '16'
|
python-version: '3.11'
|
||||||
|
|
||||||
|
- name: Install Node.js 20
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20.x'
|
||||||
|
|
||||||
- name: Install PlatformIO
|
- name: Install PlatformIO
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
pip install -U platformio
|
pip install -U platformio
|
||||||
platformio upgrade
|
|
||||||
platformio update
|
|
||||||
|
|
||||||
- name: Build WebUI
|
- name: Build WebUI
|
||||||
run: |
|
run: |
|
||||||
cd interface
|
cd interface
|
||||||
npm ci
|
yarn install
|
||||||
npm run build
|
yarn typesafe-i18n --no-watch
|
||||||
|
sed -i "s/= 'pl'/= 'en'/" ./src/i18n/i18n-util.ts
|
||||||
|
yarn build
|
||||||
|
yarn webUI
|
||||||
|
|
||||||
- name: Build firmware
|
- name: Build all PIO target environments from default_envs
|
||||||
run: |
|
run: |
|
||||||
platformio run -e ci
|
platformio run
|
||||||
|
|
||||||
- name: Release
|
- name: Create GitHub Release
|
||||||
uses: "marvinpinto/action-automatic-releases@latest"
|
uses: emsesp/action-automatic-releases@v1.0.0
|
||||||
with:
|
with:
|
||||||
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
repo_token: '${{ secrets.GITHUB_TOKEN }}'
|
||||||
prerelease: false
|
prerelease: false
|
||||||
files: |
|
files: |
|
||||||
CHANGELOG.md
|
CHANGELOG.md
|
||||||
|
|||||||
54
.github/workflows/test_release.yml
vendored
Normal file
54
.github/workflows/test_release.yml
vendored
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
name: 'test-release'
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'dev2'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pre-release:
|
||||||
|
name: 'Automatic test-release build'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Enable Corepack
|
||||||
|
run: corepack enable
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.11'
|
||||||
|
- name: Use Node.js 20.x
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
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
|
||||||
|
yarn install
|
||||||
|
yarn typesafe-i18n --no-watch
|
||||||
|
sed -i "s/= 'pl'/= 'en'/" ./src/i18n/i18n-util.ts
|
||||||
|
yarn build
|
||||||
|
yarn webUI
|
||||||
|
- name: Build all target environments from default_envs
|
||||||
|
run: |
|
||||||
|
platformio run
|
||||||
|
- name: Create GitHub Release
|
||||||
|
id: 'automatic_releases'
|
||||||
|
uses: emsesp/action-automatic-releases@v1.0.0
|
||||||
|
with:
|
||||||
|
repo_token: '${{ secrets.GITHUB_TOKEN }}'
|
||||||
|
title: Test Build v${{steps.build_info.outputs.VERSION}}
|
||||||
|
automatic_release_tag: 'test'
|
||||||
|
prerelease: true
|
||||||
|
files: |
|
||||||
|
CHANGELOG_LATEST.md
|
||||||
|
./build/firmware/*.*
|
||||||
61
.gitignore
vendored
61
.gitignore
vendored
@@ -1,37 +1,74 @@
|
|||||||
# vscode
|
# vscode
|
||||||
.vscode
|
.vscode/c_cpp_properties.json
|
||||||
.directory
|
.vscode/extensions.json
|
||||||
workspace.code-workspace
|
.vscode/launch.json
|
||||||
|
.vscode/settings.json
|
||||||
|
|
||||||
# build
|
# c++ compiling
|
||||||
build/
|
|
||||||
.clang_complete
|
.clang_complete
|
||||||
.gcc-flags.json
|
.gcc-flags.json
|
||||||
cppcheck.out.xml
|
cppcheck.out.xml
|
||||||
debug.log
|
|
||||||
|
|
||||||
# platformio
|
# platformio
|
||||||
.pio
|
.pio
|
||||||
pio_local.ini
|
pio_local.ini
|
||||||
/.VSCodeCounter
|
*_old
|
||||||
|
|
||||||
# OS specific
|
# OS specific
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*Thumbs.db
|
*Thumbs.db
|
||||||
|
|
||||||
# project specfic
|
# web specfic
|
||||||
/scripts/stackdmp.txt
|
build/
|
||||||
emsesp
|
dist/
|
||||||
/data/www
|
/data/www
|
||||||
/lib/framework/WWWData.h
|
/lib/framework/WWWData.h
|
||||||
/interface/build
|
/interface/build
|
||||||
node_modules
|
node_modules
|
||||||
/interface/.eslintcache
|
/interface/.eslintcache
|
||||||
|
stats.html
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
.pnp.*
|
||||||
|
*/.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/sdks
|
||||||
|
!.yarn/versions
|
||||||
|
yarn.lock
|
||||||
|
analyse.html
|
||||||
|
interface/vite.config.ts.timestamp*
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# i18n generated files
|
||||||
|
interface/src/i18n/i18n-react.tsx
|
||||||
|
interface/src/i18n/i18n-types.ts
|
||||||
|
interface/src/i18n/i18n-util.ts
|
||||||
|
interface/src/i18n/i18n-util.sync.ts
|
||||||
|
interface/src/i18n/i18n-util.async.ts
|
||||||
|
|
||||||
|
# scripts
|
||||||
test.sh
|
test.sh
|
||||||
|
scripts/run.sh
|
||||||
scripts/__pycache__
|
scripts/__pycache__
|
||||||
.temp
|
scripts/stackdmp.txt
|
||||||
|
|
||||||
# sonar
|
# sonar
|
||||||
.scannerwork/
|
.scannerwork/
|
||||||
sonar/
|
sonar/
|
||||||
build_wrapper_output_directory/
|
bw-output/
|
||||||
|
|
||||||
|
# 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
|
||||||
13
.prettierrc
Normal file
13
.prettierrc
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"plugins": ["@trivago/prettier-plugin-sort-imports"],
|
||||||
|
"trailingComma": "none",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": 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"
|
||||||
|
}
|
||||||
101
.vscode/settings.json
vendored
Normal file
101
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
362
CHANGELOG.md
362
CHANGELOG.md
@@ -5,13 +5,356 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
# [3.4.3]
|
## [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
|
## Fixed
|
||||||
|
|
||||||
- Fix for new installations with filesystem not initializing
|
- 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)
|
||||||
|
|
||||||
# [3.4.2]
|
## 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**
|
||||||
|
|
||||||
|
Writeable Text entities have moved from type `sensor` to `text` in Home Assistant to make them also editable within an HA dashboard. Examples are `datetime`, `holidays`, `switchtime`, `vacations`, `maintenancedate`. You will need to manually remove any old discovery topics from your MQTT broker using an application like MQTT Explorer.
|
||||||
|
|
||||||
|
## Added
|
||||||
|
|
||||||
|
- humidity for ventilation devices
|
||||||
|
- telegrams for RC100H, hc2, etc. (seen on discord, not tested)
|
||||||
|
- names for BC400, GB192i.2, read temperatures for low loss header and heatblock [#1317](https://github.com/emsesp/EMS-ESP32/discussions/1317)
|
||||||
|
- option for `forceheatingoff` [#1262](https://github.com/emsesp/EMS-ESP32/issues/1262)
|
||||||
|
- remote thermostat emulation RC100H for RC3xx [#1278](https://github.com/emsesp/EMS-ESP32/discussions/1278)
|
||||||
|
- shower_data MQTT payload contains the timestamp [#1329](https://github.com/emsesp/EMS-ESP32/issues/1329)
|
||||||
|
- HA discovery for writeable text entities [#1337](https://github.com/emsesp/EMS-ESP32/pull/1377)
|
||||||
|
- autodetect board_profile, store in nvs, add telnet command option, add E32V2
|
||||||
|
- heat pump high res energy counters [#1348, #1349. #1350](https://github.com/emsesp/EMS-ESP32/issues/1348)
|
||||||
|
- optional bssid in network settings
|
||||||
|
- extension module EM100 [#1315](https://github.com/emsesp/EMS-ESP32/discussions/1315)
|
||||||
|
- digital_out with new options for polarity and startup state
|
||||||
|
- added 'system allvalues' command that dumps all the EMS device values, plus sensors and any custom entities
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
|
||||||
|
- remove command `remoteseltemp`, thermostat accept it only from remote thermostat
|
||||||
|
- shower_data MQTT payload contains the timestamp [#1329](https://github.com/emsesp/EMS-ESP32/issues/1329)
|
||||||
|
- fixed helper text in Web Device Entity dialog box for numerical ranges
|
||||||
|
- MQTT base with paths not working in HA [#1393](https://github.com/emsesp/EMS-ESP32/issues/1393)
|
||||||
|
- set/read thermostat mode for RC100-RC300, [#1440](https://github.com/emsesp/EMS-ESP32/issues/1440) [#1442](https://github.com/emsesp/EMS-ESP32/issues/1442)
|
||||||
|
- some setting commands for ems-boiler have used wrong ems+ telegram in 3.6.3
|
||||||
|
|
||||||
|
## Changed
|
||||||
|
|
||||||
|
- update to platform 6.4.0, arduino 2.0.14 / idf 4.4.6
|
||||||
|
- small changes for arduino 3.0.0 / idf 5.1 compatibility (not backward compatible to platform 6.3.2 and before)
|
||||||
|
- AP start after 10 sec, stay until station/eth connected
|
||||||
|
- tested wifi-all-channel-scan (3.6.3-dev4 a-e), removed again because of connect issues
|
||||||
|
- mqtt disconnect stops queue
|
||||||
|
|
||||||
|
## [3.6.2] October 1 2023
|
||||||
|
|
||||||
|
## **IMPORTANT! BREAKING CHANGES**
|
||||||
|
|
||||||
|
## Added
|
||||||
|
|
||||||
|
- Power entities
|
||||||
|
- Optional input of BSSID for AP connection
|
||||||
|
- Return empty json if no entries in scheduler/custom/analogsensor/temperaturesensor
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
|
||||||
|
- Wifi full scan to get strongest AP
|
||||||
|
- Add missing dhw tags
|
||||||
|
- Sending a dash/- to the Reset command doesn't return an error [#1308](https://github.com/emsesp/EMS-ESP32/discussions/1308)
|
||||||
|
|
||||||
|
## Changed
|
||||||
|
|
||||||
|
- MQTT queue max 300 messages, check heap and maxAlloc
|
||||||
|
- API call commands are logged as WARN in the log
|
||||||
|
- Reset Command renamed to 'reset' in lowercase in EN
|
||||||
|
|
||||||
|
## [3.6.1] September 9 2023
|
||||||
|
|
||||||
|
## **IMPORTANT! BREAKING CHANGES**
|
||||||
|
|
||||||
|
- `shower_data` MQTT topic shows duration is seconds (was previously a full english sentence)
|
||||||
|
|
||||||
|
## Added
|
||||||
|
|
||||||
|
- Show WiFi rssi in Network Status Page, show quality as color
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
|
||||||
|
- Issue in espMqttClient causing a memory leak when MQTT broker is disconnected due to network unavailability [#1264](https://github.com/emsesp/EMS-ESP32/issues/1264)
|
||||||
|
- Using MQTT enum values correctly formatted in MQTT Discovery [#1280](https://github.com/emsesp/EMS-ESP32/issues/1280)
|
||||||
|
|
||||||
|
## Changed
|
||||||
|
|
||||||
|
- MQTT free mem check set to 60 kb
|
||||||
|
- Small cosmetic changes to Searching in Customization web page
|
||||||
|
- Updated to espressif32@6.4.0
|
||||||
|
|
||||||
|
# [3.6.0] August 13 2023
|
||||||
|
|
||||||
|
## **IMPORTANT! BREAKING CHANGES**
|
||||||
|
|
||||||
|
There are breaking changes between 3.5.x and earlier versions of 3.6.0. Please read carefully before applying the update.
|
||||||
|
|
||||||
|
- The sensors have been renamed. `dallassensor` is now `temperaturesensor` in the MQTT topic and named `ts` in the Customizations file. Likewise `analogs` is now `analogsensor` in MQTT and called `as` in the Customizations file. If you have previous customizations you will need to manually update by downloading, changing the JSON file and uploading. It's also recommended cleaning up any old MQTT topics from your broker using an application like MQTTExplorer.
|
||||||
|
|
||||||
|
## Added
|
||||||
|
|
||||||
|
- Workaround for better Domoticz MQTT integration? [#904](https://github.com/emsesp/EMS-ESP32/issues/904)
|
||||||
|
- Show MAC address without connecting to network enhancement [#933](https://github.com/emsesp/EMS-ESP32/issues/933)
|
||||||
|
- Warn user in WebUI of unsaved changes [#911](https://github.com/emsesp/EMS-ESP32/issues/911)
|
||||||
|
- Detect old Tado thermostat, device-id 0x19, no entities
|
||||||
|
- Some more HM200 entities [#500](https://github.com/emsesp/EMS-ESP32/issues/500)
|
||||||
|
- Added Scheduler [#701](https://github.com/emsesp/EMS-ESP32/issues/701)
|
||||||
|
- Added Custom Entities read/write from EMS bus
|
||||||
|
- Build S3 binary with github actions
|
||||||
|
- Greenstar HIU [#1158](https://github.com/emsesp/EMS-ESP32/issues/1158)
|
||||||
|
- AM200 code 10 [#1161](https://github.com/emsesp/EMS-ESP32/issues/1161)
|
||||||
|
- Ventilation device (Logavent HRV176) [#1172](https://github.com/emsesp/EMS-ESP32/issues/1172)
|
||||||
|
- Turn ETH off on wifi connect [#1167](https://github.com/emsesp/EMS-ESP32/issues/1167)
|
||||||
|
- Support for multiple EMS-ESPs with HA [#1196](https://github.com/emsesp/EMS-ESP32/issues/1196)
|
||||||
|
- Italian translation [#1199](https://github.com/emsesp/EMS-ESP32/issues/1199)
|
||||||
|
- Turkish language support [#1076](https://github.com/emsesp/EMS-ESP32/issues/1076)
|
||||||
|
- Buderus GB182 - HC1 mode change not work bug [#1193](https://github.com/emsesp/EMS-ESP32/issues/1193)
|
||||||
|
- Minimal flow temperature enhancement [#1192](https://github.com/emsesp/EMS-ESP32/issues/1192)
|
||||||
|
- Roomtemperature Switching Difference enhancement [#1191](https://github.com/emsesp/EMS-ESP32/issues/1191)
|
||||||
|
- Dew Point Temperature Difference enhancement [#1190](https://github.com/emsesp/EMS-ESP32/issues/1190)
|
||||||
|
- Control of heating circuit mode enhancement [#1187](https://github.com/emsesp/EMS-ESP32/issues/1187)
|
||||||
|
- Warn user in WebUI of unsaved changes enhancement [#911](https://github.com/emsesp/EMS-ESP32/issues/911)
|
||||||
|
- Create safebuild app to fit into factory partition to give ESP32 more flash memory enhancement [#608](https://github.com/emsesp/EMS-ESP32/issues/608)
|
||||||
|
- Support ESP32 S2, C3 mini and S3 [#605](https://github.com/emsesp/EMS-ESP32/issues/605)
|
||||||
|
- Support Buderus AM200 [#1161](https://github.com/emsesp/EMS-ESP32/issues/1161)
|
||||||
|
- Custom telegram handler [#1155](https://github.com/emsesp/EMS-ESP32/issues/1155)
|
||||||
|
- Added support for TLS in MQTT (ESP32-S3 only) [#1178](https://github.com/emsesp/EMS-ESP32/issues/1178)
|
||||||
|
- Boardprofile BBQKees Gateway S3
|
||||||
|
- Custom entity type RAW [#1212](https://github.com/emsesp/EMS-ESP32/discussions/1212)
|
||||||
|
- API command response [#1212](https://github.com/emsesp/EMS-ESP32/discussions/1212)
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
|
||||||
|
- HA-discovery for analog sensor commands [#1035](https://github.com/emsesp/EMS-ESP32/issues/1035)
|
||||||
|
- Enum order of RC3x nofrost mode
|
||||||
|
- Heartbeat interval
|
||||||
|
- Exhaust temperature always zero on GB125/MC110/RC310 bug [#1147](https://github.com/emsesp/EMS-ESP32/issues/1147)
|
||||||
|
- thermostat modetype is not changing when mode changes (e.g. to night) bugSomething isn't working [#1098](https://github.com/emsesp/EMS-ESP32/issues/1098)
|
||||||
|
- NTP: cant apply changed timezone [#1182](https://github.com/emsesp/EMS-ESP32/issues/1182)
|
||||||
|
- Missing Status of VS1 for Buderus SM200 enhancement [#1034](https://github.com/emsesp/EMS-ESP32/issues/1034)
|
||||||
|
- Allowed gpios for S3
|
||||||
|
|
||||||
|
## Changed
|
||||||
|
|
||||||
|
- Optional upgrade to platform-espressif32 6.3.0 (after 5.3.0) [#862](https://github.com/emsesp/EMS-ESP32/issues/862)
|
||||||
|
- Use byte 3 for detection RC30 active heatingcircuit [#786](https://github.com/emsesp/EMS-ESP32/issues/786)
|
||||||
|
- Write repeated selflowtemp if tx-queue is empty without verify [#954](https://github.com/emsesp/EMS-ESP32/issues/954)
|
||||||
|
- HA discovery recreate after disconnect by device [#1067](https://github.com/emsesp/EMS-ESP32/issues/1067)
|
||||||
|
- File upload: check flash size (overflow) instead of filesize
|
||||||
|
- Improved HA Discovery so previous configs no longer need to be removed when starting [#1077](https://github.com/emsesp/EMS-ESP32/pull/1077) (thanks @pswid)
|
||||||
|
- Enlarge UART-Stack to 2,5k
|
||||||
|
- Retry timeout for Mqtt-QOS1/2 10seconds
|
||||||
|
- Optimize WebUI rendering when using Dialog Boxes [#1116](https://github.com/emsesp/EMS-ESP32/issues/1116)
|
||||||
|
- Optimize Web libraries to reduce bundle size (3.6.x) [#1112](https://github.com/emsesp/EMS-ESP32/issues/1112)
|
||||||
|
- Use [espMqttClient](https://github.com/bertmelis/espMqttClient) with integrated queue [#1178](https://github.com/emsesp/EMS-ESP32/issues/1178)
|
||||||
|
- Move Sensors from Web dashboard to it's own tab enhancement [#1170](https://github.com/emsesp/EMS-ESP32/issues/1170)
|
||||||
|
- Optimize WebUI dashboard data [#1169](https://github.com/emsesp/EMS-ESP32/issues/1169)
|
||||||
|
- Replace React core library with Preact to save on memory footprint
|
||||||
|
- Response to `system/send` raw reads gives combined data for telegrams with more parts
|
||||||
|
|
||||||
|
# [3.5.0] February 6 2023
|
||||||
|
|
||||||
|
## **IMPORTANT! BREAKING CHANGES**
|
||||||
|
|
||||||
|
- When upgrading to v3.5 for the first time from v3.4 on a BBQKees Gateway board you will need to use the [EMS-EPS Flasher](https://github.com/emsesp/EMS-ESP-Flasher/releases) to correctly re-partition the flash. Make sure you backup the settings and customizations from the WebUI (System->Upload/Download) and restore after the upgrade.
|
||||||
|
|
||||||
|
## Added
|
||||||
|
|
||||||
|
- Translations in Web UI and all device entity names (DE, NL, SV, PL, NO, FR) [#22](https://github.com/emsesp/EMS-ESP32/issues/22)
|
||||||
|
- Add support for Lolin C3 mini [#620](https://github.com/emsesp/EMS-ESP32/pull/620)
|
||||||
|
- Add support for ESP32-S2 [#667](https://github.com/emsesp/EMS-ESP32/pull/667)
|
||||||
|
- Add devices: Greenstar 30Ri boiler, Junkers FW500 thermostat, Buderus BC30 controller
|
||||||
|
- Add program memory info
|
||||||
|
- Add mqtt queue and connection infos
|
||||||
|
- Adapt min/max if ems-value is not in this range
|
||||||
|
- Add heat pump settings for inputs and limits [#600](https://github.com/emsesp/EMS-ESP32/issues/600)
|
||||||
|
- Add hybrid heatpump [#500](https://github.com/emsesp/EMS-ESP32/issues/500)
|
||||||
|
- Add translated tags
|
||||||
|
- Add min/max to customization table [#686](https://github.com/emsesp/EMS-ESP32/issues/686)
|
||||||
|
- Add MD5 check [#637](https://github.com/emsesp/EMS-ESP32/issues/637)
|
||||||
|
- Add more bus-ids [#673](https://github.com/emsesp/EMS-ESP32/issues/673)
|
||||||
|
- Use HA connectivity device class for Status, added boot time [#751](https://github.com/emsesp/EMS-ESP32/issues/751)
|
||||||
|
- Add commands for analog sensors outputs
|
||||||
|
- Support for multiple EMS-ESPs with MQTT and HA [[#759](https://github.com/emsesp/EMS-ESP32/issues/759)]
|
||||||
|
- Settings for heatpump silent mode and additional heater [[#802](https://github.com/emsesp/EMS-ESP32/issues/802)] [[#803](https://github.com/emsesp/EMS-ESP32/issues/803)]
|
||||||
|
- Zone module MZ100 [#826](https://github.com/emsesp/EMS-ESP32/issues/826)
|
||||||
|
- Default MQTT hostname is blank [#829](https://github.com/emsesp/EMS-ESP32/issues/829)
|
||||||
|
- wwCurFlow for ems+ devices [#829](https://github.com/emsesp/EMS-ESP32/issues/829)
|
||||||
|
- Add Rego 3000, TR120RF thermostats [#917](https://github.com/emsesp/EMS-ESP32/issues/917)
|
||||||
|
- Add config for ESP32-S3
|
||||||
|
- Add heatpump silent mode and other entities [#896](https://github.com/emsesp/EMS-ESP32/issues/896)
|
||||||
|
- Allow reboot to other partition (factory or asymetric OTA)
|
||||||
|
- Blacklist entities to remove from memory [#891](https://github.com/emsesp/EMS-ESP32/issues/891)
|
||||||
|
- Add boiler pump operating mode [#944](https://github.com/emsesp/EMS-ESP32/issues/944)
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
|
||||||
|
- Factory Reset not working [#628](https://github.com/emsesp/EMS-ESP32/issues/628)
|
||||||
|
- Valid 4 byte values [#820](https://github.com/emsesp/EMS-ESP32/issues/820)
|
||||||
|
- Commands for multiple thermostats [#826](https://github.com/emsesp/EMS-ESP32/issues/826)
|
||||||
|
- API queries for multiple devices [#865](https://github.com/emsesp/EMS-ESP32/issues/865)
|
||||||
|
- Console crash when using call with command `hcx` only. [#841](https://github.com/emsesp/EMS-ESP32/issues/841)
|
||||||
|
- `heatingPump2Mod` was wrong, changed to absBurnPow [[#908](https://github.com/emsesp/EMS-ESP32/issues/908)
|
||||||
|
- Rounding of web input values
|
||||||
|
- Analog sensor with single gpio number [#915](https://github.com/emsesp/EMS-ESP32/issues/915)
|
||||||
|
- HA dallas and analog configs: remove/rebuild on change [#888](https://github.com/emsesp/EMS-ESP32/issues/888)
|
||||||
|
- Modes and set seltemp for RC30 and RC20 [#932](https://github.com/emsesp/EMS-ESP32/issues/932)
|
||||||
|
|
||||||
|
## Changed
|
||||||
|
|
||||||
|
- Discovery in HomeAssistant don't work with custom base topic. [#596](https://github.com/emsesp/EMS-ESP32/issues/596) Base topic containing `/` are changed to `_`
|
||||||
|
- RF room temperature sensor are shown as thermostat
|
||||||
|
- Render mqtt float json values with trailing zero
|
||||||
|
- Removed flash strings, to increase available heap memory
|
||||||
|
- Reload page after restart button is pressed
|
||||||
|
- Analog/dallas values command as list like ems-devices
|
||||||
|
- Analog/dallas HA-entities based on id
|
||||||
|
- MQTT Base is a mandatory field. Removed MQTT topic length from settings
|
||||||
|
- HA duration class for time entities [[#822](https://github.com/emsesp/EMS-ESP32/issues/822)
|
||||||
|
- AM200 alternative heatsource as class heatsource [[#857](https://github.com/emsesp/EMS-ESP32/issues/857)
|
||||||
|
|
||||||
|
# [3.4.2] September 18 2022
|
||||||
|
|
||||||
## Added
|
## Added
|
||||||
|
|
||||||
@@ -30,12 +373,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
- fix Table resizing in WebUI [#519](https://github.com/emsesp/EMS-ESP32/issues/519)
|
- 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)
|
- 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
|
## Changed
|
||||||
|
|
||||||
- Shorten "friendly names" in Home Assistant [#555](https://github.com/emsesp/EMS-ESP32/issues/555)
|
- Shorten "friendly names" in Home Assistant [#555](https://github.com/emsesp/EMS-ESP32/issues/555)
|
||||||
|
|
||||||
- platformio 2.3.0 (IDF 4, Arduino 2)
|
- platformio 2.3.0 (IDF 4, Arduino 2)
|
||||||
- remove master-thermostat, support multiple thermostats
|
- remove master-thermostat, support multiple thermostats
|
||||||
- merge up- and download in webui [#577](https://github.com/emsesp/EMS-ESP32/issues/577)
|
- merge up- and download in webui [#577](https://github.com/emsesp/EMS-ESP32/issues/577)
|
||||||
@@ -48,13 +390,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## Changed
|
## Changed
|
||||||
|
|
||||||
|
- Controller data in web-ui only for IVT [#522](https://github.com/emsesp/EMS-ESP32/issues/522)
|
||||||
|
- Rename hidden `climate` to a more explaining name [#523](https://github.com/emsesp/EMS-ESP32/issues/523)
|
||||||
|
- Minor changes to the Customizations web page [#527](https://github.com/emsesp/EMS-ESP32/pull/527)
|
||||||
|
|
||||||
# [3.4.0] May 23 2022
|
# [3.4.0] May 23 2022
|
||||||
|
|
||||||
## Added
|
## Added
|
||||||
|
|
||||||
- WebUI optimizations, updated look&feel and better performance [#124](https://github.com/emsesp/EMS-ESP32/issues/124)
|
- WebUI optimizations, updated look&feel and better performance [#124](https://github.com/emsesp/EMS-ESP32/issues/124)
|
||||||
- Auto refresh of WebUI after successful firmware upload [#178](https://github.com/emsesp/EMS-ESP32/issues/178)
|
- Auto refresh of WebUI after successful firmware upload [#178](https://github.com/emsesp/EMS-ESP32/issues/178)
|
||||||
- New Customization Service in WebUI. First feature is the ability to enable/disabled Enitites (device values) from EMS devices [#206](https://github.com/emsesp/EMS-ESP32/issues/206)
|
- New Customization Service in WebUI. First feature is the ability to enable/disabled Entities (device values) from EMS devices [#206](https://github.com/emsesp/EMS-ESP32/issues/206)
|
||||||
- Option to disable Telnet Console [#209](https://github.com/emsesp/EMS-ESP32/issues/209)
|
- Option to disable Telnet Console [#209](https://github.com/emsesp/EMS-ESP32/issues/209)
|
||||||
- Added Hide SSID, Max Clients and Preferred Channel to Access Point
|
- Added Hide SSID, Max Clients and Preferred Channel to Access Point
|
||||||
- Merged in MichaelDvP's changes like Fahrenheit conversion, publish single (for IOBroker) and a few other critical optimizations
|
- Merged in MichaelDvP's changes like Fahrenheit conversion, publish single (for IOBroker) and a few other critical optimizations
|
||||||
@@ -154,7 +500,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Added pool data to telegrams 0x494 & 0x495 [#102](https://github.com/emsesp/EMS-ESP32/issues/102)
|
- Added pool data to telegrams 0x494 & 0x495 [#102](https://github.com/emsesp/EMS-ESP32/issues/102)
|
||||||
- Add RC300 second summermode telegram [#108](https://github.com/emsesp/EMS-ESP32/issues/108)
|
- Add RC300 second summermode telegram [#108](https://github.com/emsesp/EMS-ESP32/issues/108)
|
||||||
- Add support for the RC25 thermostat [#106](https://github.com/emsesp/EMS-ESP32/issues/106)
|
- Add support for the RC25 thermostat [#106](https://github.com/emsesp/EMS-ESP32/issues/106)
|
||||||
- Add new command 'entities' for a device, e.g. http://ems-esp/api/boiler/entities to show the shortname, description and HA Entity name (if HA enabled) [#116](https://github.com/emsesp/EMS-ESP32/issues/116)
|
- Add new command 'entities' for a device, e.g. <http://ems-esp/api/boiler/entities> to show the shortname, description and HA Entity name (if HA enabled) [#116](https://github.com/emsesp/EMS-ESP32/issues/116)
|
||||||
- Support for Junkers program and remote (fb10/fb110) temperature
|
- Support for Junkers program and remote (fb10/fb110) temperature
|
||||||
- Home Assistant `state_class` attribute for Wh, kWh, W and KW [#129](https://github.com/emsesp/EMS-ESP32/issues/129)
|
- Home Assistant `state_class` attribute for Wh, kWh, W and KW [#129](https://github.com/emsesp/EMS-ESP32/issues/129)
|
||||||
- Add current room influence for RC300 [#136](https://github.com/emsesp/EMS-ESP32/issues/136)
|
- Add current room influence for RC300 [#136](https://github.com/emsesp/EMS-ESP32/issues/136)
|
||||||
@@ -376,4 +722,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- some names of mqtt-tags like in v2.2.1
|
- some names of mqtt-tags like in v2.2.1
|
||||||
- new ESP32 partition side to allow for smoother OTA and fallback
|
- new ESP32 partition side to allow for smoother OTA and fallback
|
||||||
- Network Gateway IP is optional (#682)emsesp/EMS-ESP
|
- Network Gateway IP is optional (#682)emsesp/EMS-ESP
|
||||||
- moved to a new GitHub repo https://github.com/emsesp/EMS-ESP32
|
- moved to a new GitHub repo <https://github.com/emsesp/EMS-ESP32>
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
# Changelog
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Everybody is welcome and invited to contribute to the EMS-ESP Project by:
|
|||||||
|
|
||||||
- providing Pull Requests (Features, Fixes, suggestions)
|
- providing Pull Requests (Features, Fixes, suggestions)
|
||||||
- testing new released features and report issues on your EMS equipment
|
- testing new released features and report issues on your EMS equipment
|
||||||
- contributing to missing [documentation](https://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.
|
This document describes rules that are in effect for this repository, meant for handling issues by contributors in the issue tracker and PRs.
|
||||||
|
|
||||||
|
|||||||
36
Makefile
36
Makefile
@@ -1,9 +1,11 @@
|
|||||||
#
|
#
|
||||||
# GNUMakefile for EMS-ESP
|
# GNUMakefile for EMS-ESP
|
||||||
# (c) 2020 Paul Derbyshire
|
# This is mainly used to generate the .o files for SonarQube analysis
|
||||||
#
|
#
|
||||||
NUMJOBS=${NUMJOBS:-" -j4 "}
|
|
||||||
MAKEFLAGS+="j "
|
# NUMJOBS=${NUMJOBS:-" -j10 "}
|
||||||
|
# MAKEFLAGS+="j "
|
||||||
|
|
||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
# Project Structure
|
# Project Structure
|
||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
@@ -17,23 +19,31 @@ MAKEFLAGS+="j "
|
|||||||
#TARGET := $(notdir $(CURDIR))
|
#TARGET := $(notdir $(CURDIR))
|
||||||
TARGET := emsesp
|
TARGET := emsesp
|
||||||
BUILD := build
|
BUILD := build
|
||||||
SOURCES := src src/* lib_standalone lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src src/devices lib/ArduinoJson/src lib/PButton
|
SOURCES := src src/* lib_standalone lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src src/devices lib/ArduinoJson/src lib/PButton lib/semver lib/espMqttClient/src lib/espMqttClient/src/*
|
||||||
INCLUDES := src lib_standalone lib/ArduinoJson/src lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src lib/uuid-telnet/src lib/uuid-syslog/src lib/* src/devices
|
INCLUDES := src lib_standalone lib/espMqttClient/src lib/espMqttClient/src/Transport lib/ArduinoJson/src lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src lib/uuid-telnet/src lib/uuid-syslog/src lib/semver lib/* src/devices
|
||||||
LIBRARIES :=
|
LIBRARIES :=
|
||||||
|
|
||||||
CPPCHECK = cppcheck
|
CPPCHECK = cppcheck
|
||||||
|
# CHECKFLAGS = -q --force --std=c++17
|
||||||
CHECKFLAGS = -q --force --std=c++11
|
CHECKFLAGS = -q --force --std=c++11
|
||||||
|
|
||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
# Languages Standard
|
# Languages Standard
|
||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
C_STANDARD := -std=c11
|
C_STANDARD := -std=c17
|
||||||
CXX_STANDARD := -std=c++11
|
CXX_STANDARD := -std=gnu++14
|
||||||
|
|
||||||
|
# C_STANDARD := -std=c11
|
||||||
|
# CXX_STANDARD := -std=c++11
|
||||||
|
|
||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
# Defined Symbols
|
# Defined Symbols
|
||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
DEFINES += -DFACTORY_WIFI_HOSTNAME=\"ems-esp\" -DARDUINOJSON_ENABLE_STD_STRING=1 -DARDUINOJSON_ENABLE_PROGMEM=1 -DARDUINOJSON_ENABLE_ARDUINO_STRING -DARDUINOJSON_USE_DOUBLE=0 -DEMSESP_DEBUG -DEMSESP_STANDALONE -DEMSESP_USE_SERIAL -DEMSESP_DEFAULT_BOARD_PROFILE=\"LOLIN\"
|
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.7.0-dev\" -DEMSESP_DEFAULT_BOARD_PROFILE=\"S32\"
|
||||||
|
|
||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
# Sources & Files
|
# Sources & Files
|
||||||
@@ -66,16 +76,14 @@ CXX := /usr/bin/g++
|
|||||||
# CXXFLAGS C++ Compiler Flags
|
# CXXFLAGS C++ Compiler Flags
|
||||||
# LDFLAGS Linker Flags
|
# LDFLAGS Linker Flags
|
||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
CPPFLAGS += $(DEFINES) $(INCLUDE)
|
CPPFLAGS += $(DEFINES) $(DEFAULTS) $(INCLUDE)
|
||||||
CPPFLAGS += -ggdb
|
CPPFLAGS += -ggdb
|
||||||
CPPFLAGS += -g3
|
CPPFLAGS += -g3
|
||||||
CPPFLAGS += -Os
|
CPPFLAGS += -Os
|
||||||
|
|
||||||
CFLAGS += $(CPPFLAGS)
|
CFLAGS += $(CPPFLAGS)
|
||||||
CFLAGS += -Wall
|
CFLAGS += -Wall -Wextra -Werror -Wswitch-enum
|
||||||
CFLAGS += -Wextra
|
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
|
||||||
CFLAGS += -Wno-unused-parameter
|
|
||||||
|
|
||||||
CXXFLAGS += $(CFLAGS) -MMD
|
CXXFLAGS += $(CFLAGS) -MMD
|
||||||
|
|
||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
@@ -114,6 +122,8 @@ COMPILE.cpp = $(CXX) $(CXX_STANDARD) $(CXXFLAGS) $(DEPFLAGS) -c $< -o $@
|
|||||||
# Targets
|
# Targets
|
||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
.PHONY: all
|
.PHONY: all
|
||||||
|
.SILENT: $(OUTPUT)
|
||||||
|
|
||||||
all: $(OUTPUT)
|
all: $(OUTPUT)
|
||||||
|
|
||||||
$(OUTPUT): $(OBJS)
|
$(OUTPUT): $(OBJS)
|
||||||
|
|||||||
164
README.md
164
README.md
@@ -1,129 +1,91 @@
|
|||||||
# 
|
# 
|
||||||
|
|
||||||
**EMS-ESP** is an open-source firmware for the Espressif ESP8266 and ESP32 microcontroller that communicates with **EMS** (Energy Management System) based equipment from manufacturers like Bosch, Buderus, Nefit, Junkers, Worcester and Sieger.
|
|
||||||
|
|
||||||
This project is the specifically for the ESP32. Compared with the previous ESP8266 (version 2) release it has the following enhancements:
|
|
||||||
|
|
||||||
- Ethernet Support
|
|
||||||
- Pre-configured circuit board layouts
|
|
||||||
- Supports writing EMS values directly from within Web UI
|
|
||||||
- Mock API server for faster offline development and testing
|
|
||||||
- Improved API and MQTT commands
|
|
||||||
- Improvements to Dallas temperature sensors
|
|
||||||
- Embedded log tracing in the Web UI
|
|
||||||
|
|
||||||
[](https://github.com/emsesp/EMS-ESP32/blob/main/CHANGELOG.md)
|
[](https://github.com/emsesp/EMS-ESP32/blob/main/CHANGELOG.md)
|
||||||
[](https://github.com/emsesp/EMS-ESP32/commits/main)
|
[](https://github.com/emsesp/EMS-ESP32/commits/main)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
[](https://sonarcloud.io/summary/new_code?id=emsesp_EMS-ESP32)
|
[](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://github.com/emsesp/EMS-ESP32/releases)
|
||||||
[](https://discord.gg/3J3GgnzpyT)
|
[](https://discord.gg/3J3GgnzpyT)
|
||||||
|
|
||||||
If you like **EMS-ESP**, please give it a star, or fork it and contribute!
|
|
||||||
|
|
||||||
[](https://github.com/emsesp/EMS-ESP32/stargazers)
|
[](https://github.com/emsesp/EMS-ESP32/stargazers)
|
||||||
[](https://github.com/emsesp/EMS-ES32P/network)
|
[](https://github.com/emsesp/EMS-ES32P/network)
|
||||||
[](https://www.paypal.com/paypalme/prderbyshire/2)
|
[](https://www.paypal.com/paypalme/prderbyshire/2)
|
||||||
|
|
||||||
Note, EMS-ESP requires a small hardware circuit that can convert the EMS bus data to be read by the microcontroller. These can be ordered at <https://bbqkees-electronics.nl> or contact the contributors that can provide the schematic and designs.
|
**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.
|
||||||
|
|
||||||
<img src="media/gateway-integration.jpg" width=40%>
|
It requires a small circuit to interface with the EMS bus which can be purchased from <https://bbqkees-electronics.nl> or custom built.
|
||||||
|
|
||||||
---
|
## **Key Features**
|
||||||
|
|
||||||
# **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
|
||||||
|
|
||||||
- A multi-user secure web interface to change settings and monitor incoming data
|
## **Installing**
|
||||||
- A console, accessible via Serial and Telnet for more advanced monitoring
|
|
||||||
- Native support for Home Assistant and Domoticz 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 [100 EMS devices](https://emsesp.github.io/docs/#/Supported-EMS-Devices) (boilers, thermostats, solar modules, mixer modules, heat pumps, gateways)
|
|
||||||
|
|
||||||
## **Demo**
|
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.
|
||||||
|
|
||||||
See a demo [here](https://ems-esp.derbyshire.nl). Log in with any username/password.
|
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).
|
||||||
|
|
||||||
# **Screenshots**
|
## **Documentation**
|
||||||
|
|
||||||
## Web Interface
|
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.
|
||||||
|
|
||||||
|
## **Getting Support**
|
||||||
|
|
||||||
|
To chat with the community reach out on our [Discord Server](https://discord.gg/3J3GgnzpyT).
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## **Live Demo**
|
||||||
|
|
||||||
|
For a live demo go to [demo.emsesp.org](https://demo.emsesp.org). Pick a language from the sign on page and log in with any username or password. Note not all features are operational as it's based on static data.
|
||||||
|
|
||||||
|
## **Contributors**
|
||||||
|
|
||||||
|
EMS-ESP is a project created by [proddy](https://github.com/proddy) and owned and maintained by both [proddy](https://github.com/proddy) and [MichaelDvP](https://github.com/MichaelDvP) with support from [BBQKees Electronics](https://bbqkees-electronics.nl).
|
||||||
|
|
||||||
|
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 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**
|
||||||
|
|
||||||
|
This program is licensed under GPL-3.0
|
||||||
|
|
||||||
|
## **Screenshots**
|
||||||
|
|
||||||
|
### Web Interface
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
| ---------------------------------- | -------------------------------- |
|
| ---------------------------------- | -------------------------------- |
|
||||||
| <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
|
### Telnet Console
|
||||||
|
|
||||||
<img src="media/console.png" width=80% height=80%>
|

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

|
||||||
|
|
||||||
# **Installing**
|
|
||||||
|
|
||||||
Refer to the [official documentation](https://emsesp.github.io/docs) to how to install the firmware and configure it. The documentation is being constantly updated as new features and settings are added.
|
|
||||||
|
|
||||||
You can choose to use an pre-built firmware image or compile the code yourself:
|
|
||||||
|
|
||||||
- [Uploading a pre-built firmware build](https://emsesp.github.io/docs/#/Uploading-firmware)
|
|
||||||
- [Building the firmware from source code and flashing manually](https://emsesp.github.io/docs/#/Building-firmware)
|
|
||||||
|
|
||||||
# **Support Information**
|
|
||||||
|
|
||||||
If you're looking for support on **EMS-ESP** there are some options available:
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
- [Official EMS-ESP Documentation](https://emsesp.github.io/docs): For information on how to build and upload the firmware
|
|
||||||
- [FAQ and Troubleshooting](https://emsesp.github.io/docs/#/Troubleshooting): For information on common problems and solutions. See also [BBQKees's wiki](https://bbqkees-electronics.nl/wiki/gateway/troubleshooting.html)
|
|
||||||
|
|
||||||
## Support Community
|
|
||||||
|
|
||||||
- [Discord Server](https://discord.gg/3J3GgnzpyT): For support, troubleshooting and general questions. You have better chances to get fast answers from members of the community
|
|
||||||
- [Search in Issues](https://github.com/emsesp/EMS-ESP32/issues): You might find an answer to your question by searching current or closed issues
|
|
||||||
|
|
||||||
## Developer's Community
|
|
||||||
|
|
||||||
- [Bug Report](https://github.com/emsesp/EMS-ESP32/issues/new?template=bug_report.md): For reporting Bugs
|
|
||||||
- [Feature Request](https://github.com/emsesp/EMS-ESP32/issues/new?template=feature_request.md): For requesting features/functions
|
|
||||||
- [Troubleshooting](https://github.com/emsesp/EMS-ESP32/issues/new?template=questions---troubleshooting.md): As a last resort, you can open new _Troubleshooting & Question_ issue on GitHub if the solution could not be found using the other channels. Just remember: the more info you provide the more chances you'll have to get an accurate answer
|
|
||||||
|
|
||||||
# **Contributors ✨**
|
|
||||||
|
|
||||||
EMS-ESP is a project originally created and owned by [proddy](https://github.com/proddy). Key contributors are:
|
|
||||||
|
|
||||||
<!-- prettier-ignore-start -->
|
|
||||||
<!-- markdownlint-disable -->
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<td align="center">
|
|
||||||
<a href="https://github.com/MichaelDvP"><img src="https://avatars.githubusercontent.com/u/59284019?v=3?s=100" width="100px;" alt=""/><br /><sub><b>MichaelDvP</b></sub></a><br /></a> <a href="https://github.com/emsesp/EMS-ESP/commits?author=MichaelDvP" title="v2 Commits">v2</a>
|
|
||||||
<a href="https://github.com/emsesp/EMS-ESP32/commits?author=MichaelDvP" title="v3 Commits">v3</a>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
<!-- markdownlint-restore -->
|
|
||||||
<!-- prettier-ignore-end -->
|
|
||||||
|
|
||||||
You can also contribute to EMS-ESP 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)
|
|
||||||
|
|
||||||
# **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 JSON
|
|
||||||
- [AsyncMqttClient](https://github.com/marvinroger/async-mqtt-client) for the MQTT client, with custom modifications from @bertmelis and @proddy
|
|
||||||
- ESPAsyncWebServer and AsyncTCP for the Web server and TCP backends, with custom modifications for performance
|
|
||||||
|
|
||||||
# **License**
|
|
||||||
|
|
||||||
This program is licensed under GPL-3.0
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
# 
|
|
||||||
|
|
||||||
# Firmware Installation
|
|
||||||
|
|
||||||
Follow the instructions in the [documentation](https://emsesp.github.io/docs) on how to install the firmware binaries in the Assets below.
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
# 
|
|
||||||
|
|
||||||
This is a snapshot of the current "beta" development code and firmware binaries for the ESP32. It has all the latest features and fixes but please be aware that this is still experimental firmware used for testing and thus may contain the odd bug. Use at your own risk and remember to report an issue if you find something unusual.
|
|
||||||
|
|
||||||
# Firmware Installation
|
|
||||||
|
|
||||||
Follow the instructions in the [documentation](https://emsesp.github.io/docs) on how to install the firmware binaries in the Assets below.
|
|
||||||
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
5102
dump_entities.csv
Normal file
5102
dump_entities.csv
Normal file
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,
|
||||||
|
6
esp32_asym_partition_4M.csv
Normal file
6
esp32_asym_partition_4M.csv
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# Name, Type, SubType, Offset, Size, Flags
|
||||||
|
nvs, data, nvs, 0x9000, 0x5000,
|
||||||
|
otadata, data, ota, , 0x2000,
|
||||||
|
app0, app, ota_0, , 0x2A0000,
|
||||||
|
app1, app, ota_1, , 0x140000,
|
||||||
|
spiffs, data, spiffs, , 64K,
|
||||||
|
9
esp32_partition_16M.csv
Normal file
9
esp32_partition_16M.csv
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Name, Type, SubType, Offset, Size, Flags
|
||||||
|
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,
|
||||||
|
6
esp32_partition_4M.csv
Normal file
6
esp32_partition_4M.csv
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# Name, Type, SubType, Offset, Size, Flags
|
||||||
|
nvs, data, nvs, 0x9000, 0x5000,
|
||||||
|
otadata, data, ota, , 0x2000,
|
||||||
|
app0, app, ota_0, , 0x1F0000,
|
||||||
|
app1, app, ota_1, , 0x1F0000,
|
||||||
|
spiffs, data, spiffs, , 64K,
|
||||||
|
@@ -1,6 +0,0 @@
|
|||||||
# Name, Type, SubType, Offset, Size, Flags
|
|
||||||
nvs, data, nvs, 0x9000, 0x5000,
|
|
||||||
otadata, data, ota, 0xE000, 0x2000,
|
|
||||||
app0, app, ota_0, 0x10000, 0x1F0000,
|
|
||||||
app1, app, ota_1, 0x200000, 0x1F0000,
|
|
||||||
spiffs, data, spiffs, 0x3F0000, 0x10000,
|
|
||||||
|
@@ -7,8 +7,8 @@ build_flags =
|
|||||||
|
|
||||||
; Access point settings
|
; Access point settings
|
||||||
-D FACTORY_AP_PROVISION_MODE=AP_MODE_DISCONNECTED
|
-D FACTORY_AP_PROVISION_MODE=AP_MODE_DISCONNECTED
|
||||||
-D FACTORY_AP_SSID=\"ems-esp\" ; 1-64 characters
|
-D FACTORY_AP_SSID=\"ems-esp\"
|
||||||
-D FACTORY_AP_PASSWORD=\"ems-esp-neo\" ; 8-64 characters
|
-D FACTORY_AP_PASSWORD=\"ems-esp-neo\"
|
||||||
-D FACTORY_AP_LOCAL_IP=\"192.168.4.1\"
|
-D FACTORY_AP_LOCAL_IP=\"192.168.4.1\"
|
||||||
-D FACTORY_AP_GATEWAY_IP=\"192.168.4.1\"
|
-D FACTORY_AP_GATEWAY_IP=\"192.168.4.1\"
|
||||||
-D FACTORY_AP_SUBNET_MASK=\"255.255.255.0\"
|
-D FACTORY_AP_SUBNET_MASK=\"255.255.255.0\"
|
||||||
@@ -25,18 +25,12 @@ build_flags =
|
|||||||
-D FACTORY_NTP_TIME_ZONE_FORMAT=\"CET-1CEST,M3.5.0,M10.5.0/3\"
|
-D FACTORY_NTP_TIME_ZONE_FORMAT=\"CET-1CEST,M3.5.0,M10.5.0/3\"
|
||||||
-D FACTORY_NTP_SERVER=\"time.google.com\"
|
-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=true
|
|
||||||
|
|
||||||
; MQTT settings
|
; MQTT settings
|
||||||
-D FACTORY_MQTT_ENABLED=false
|
-D FACTORY_MQTT_ENABLED=false
|
||||||
-D FACTORY_MQTT_HOST=\"test.mosquitto.org\"
|
-D FACTORY_MQTT_HOST=\"\"
|
||||||
-D FACTORY_MQTT_PORT=1883
|
-D FACTORY_MQTT_PORT=1883
|
||||||
-D FACTORY_MQTT_USERNAME=\"\"
|
-D FACTORY_MQTT_USERNAME=\"\"
|
||||||
-D FACTORY_MQTT_PASSWORD=\"\"
|
-D FACTORY_MQTT_PASSWORD=\"\"
|
||||||
-D FACTORY_MQTT_CLIENT_ID=\"ems-esp\"
|
|
||||||
-D FACTORY_MQTT_KEEP_ALIVE=60
|
-D FACTORY_MQTT_KEEP_ALIVE=60
|
||||||
-D FACTORY_MQTT_CLEAN_SESSION=false
|
-D FACTORY_MQTT_CLEAN_SESSION=false
|
||||||
-D FACTORY_MQTT_MAX_TOPIC_LENGTH=128
|
-D FACTORY_MQTT_MAX_TOPIC_LENGTH=128
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
# This enables lint extensions
|
|
||||||
EXTEND_ESLINT=true
|
|
||||||
|
|
||||||
# This is the name of your project. It appears on the sign-in page and in the menu bar.
|
|
||||||
REACT_APP_PROJECT_NAME=EMS-ESP
|
|
||||||
|
|
||||||
# This is the url path your project will be exposed under.
|
|
||||||
REACT_APP_PROJECT_PATH=ems-esp
|
|
||||||
2
interface/.env.development
Normal file
2
interface/.env.development
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
VITE_ALOVA_TIPS=0
|
||||||
|
REACT_APP_ALOVA_TIPS=0
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
GENERATE_SOURCEMAP=false
|
|
||||||
|
|
||||||
REACT_APP_HOSTED=true
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
GENERATE_SOURCEMAP=false
|
|
||||||
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
|
||||||
8
interface/.prettierignore
Normal file
8
interface/.prettierignore
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
node_modules/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
src/i18n/*
|
||||||
|
|
||||||
|
.prettierrc
|
||||||
|
.yarn/
|
||||||
|
.typesafe-i18n.json
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"singleQuote": true,
|
|
||||||
"semi": true,
|
|
||||||
"trailingComma": "none",
|
|
||||||
"printWidth": 120
|
|
||||||
}
|
|
||||||
5
interface/.typesafe-i18n.json
Normal file
5
interface/.typesafe-i18n.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"adapter": "react",
|
||||||
|
"baseLocale": "pl",
|
||||||
|
"$schema": "https://unpkg.com/typesafe-i18n@5.26.2/schema/typesafe-i18n.json"
|
||||||
|
}
|
||||||
1
interface/.yarnrc.yml
Normal file
1
interface/.yarnrc.yml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
nodeLinker: node-modules
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
const { WebpackManifestPlugin } = require('webpack-manifest-plugin');
|
|
||||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
|
||||||
const ProgmemGenerator = require('./progmem-generator.js');
|
|
||||||
const TerserPlugin = require('terser-webpack-plugin');
|
|
||||||
|
|
||||||
module.exports = function override(config, env) {
|
|
||||||
const hosted = process.env.REACT_APP_HOSTED;
|
|
||||||
|
|
||||||
if (env === 'production' && !hosted) {
|
|
||||||
// rename the ouput file, we need it's path to be short, for embedded FS
|
|
||||||
config.output.filename = 'js/[id].[chunkhash:4].js';
|
|
||||||
config.output.chunkFilename = 'js/[id].[chunkhash:4].js';
|
|
||||||
|
|
||||||
// take out the manifest plugin
|
|
||||||
config.plugins = config.plugins.filter((plugin) => !(plugin instanceof WebpackManifestPlugin));
|
|
||||||
|
|
||||||
// shorten css filenames
|
|
||||||
const miniCssExtractPlugin = config.plugins.find((plugin) => plugin instanceof MiniCssExtractPlugin);
|
|
||||||
miniCssExtractPlugin.options.filename = 'css/[id].[contenthash:4].css';
|
|
||||||
miniCssExtractPlugin.options.chunkFilename = 'css/[id].[contenthash:4].c.css';
|
|
||||||
|
|
||||||
// don't emit license file
|
|
||||||
const terserPlugin = config.optimization.minimizer.find((plugin) => plugin instanceof TerserPlugin);
|
|
||||||
terserPlugin.options.extractComments = false;
|
|
||||||
|
|
||||||
// build progmem data files
|
|
||||||
config.plugins.push(new ProgmemGenerator({ outputPath: '../lib/framework/WWWData.h', bytesPerLine: 20 }));
|
|
||||||
}
|
|
||||||
return config;
|
|
||||||
};
|
|
||||||
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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
14
interface/index.html
Normal file
14
interface/index.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<link rel="stylesheet" href="/css/roboto.css" />
|
||||||
|
<link rel="manifest" href="/app/manifest.json" />
|
||||||
|
<title>EMS-ESP</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/index.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
29916
interface/package-lock.json
generated
29916
interface/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,103 +1,67 @@
|
|||||||
{
|
{
|
||||||
"name": "EMS-ESP",
|
"name": "EMS-ESP",
|
||||||
"version": "3.4.0",
|
"version": "3.7.0",
|
||||||
|
"description": "EMS-ESP WebUI",
|
||||||
|
"homepage": "https://emsesp.org",
|
||||||
|
"author": "proddy, emsesp.org",
|
||||||
|
"license": "MIT",
|
||||||
"private": true,
|
"private": true,
|
||||||
"proxy": "http://localhost:3080",
|
"type": "module",
|
||||||
"dependencies": {
|
|
||||||
"@emotion/react": "^11.10.4",
|
|
||||||
"@emotion/styled": "^11.10.4",
|
|
||||||
"@msgpack/msgpack": "^2.8.0",
|
|
||||||
"@mui/icons-material": "^5.10.3",
|
|
||||||
"@mui/material": "^5.10.5",
|
|
||||||
"@table-library/react-table-library": "4.0.18",
|
|
||||||
"@types/lodash": "^4.14.185",
|
|
||||||
"@types/node": "^18.7.18",
|
|
||||||
"@types/react": "^18.0.20",
|
|
||||||
"@types/react-dom": "^18.0.6",
|
|
||||||
"@types/react-router-dom": "^5.3.3",
|
|
||||||
"async-validator": "^4.2.5",
|
|
||||||
"axios": "^0.27.2",
|
|
||||||
"http-proxy-middleware": "^2.0.6",
|
|
||||||
"jwt-decode": "^3.1.2",
|
|
||||||
"lodash": "^4.17.21",
|
|
||||||
"notistack": "^2.0.5",
|
|
||||||
"parse-ms": "^3.0.0",
|
|
||||||
"react": "^18.2.0",
|
|
||||||
"react-app-rewired": "^2.2.1",
|
|
||||||
"react-dom": "^18.2.0",
|
|
||||||
"react-dropzone": "^14.2.2",
|
|
||||||
"react-icons": "^4.4.0",
|
|
||||||
"react-router-dom": "^6.4.0",
|
|
||||||
"react-scripts": "5.0.1",
|
|
||||||
"sockette": "^2.0.6",
|
|
||||||
"typescript": "^4.8.3"
|
|
||||||
},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-app-rewired start",
|
"dev": "vite dev",
|
||||||
"build": "react-app-rewired build",
|
"build": "vite build",
|
||||||
"test": "react-app-rewired test",
|
"preview": "vite preview",
|
||||||
"eject": "react-scripts eject",
|
"build-hosted": "typesafe-i18n --no-watch && vite build --mode hosted",
|
||||||
"format": "prettier --write '**/*.{ts,tsx,js,css,json,md}'",
|
"preview-standalone": "typesafe-i18n --no-watch && vite build && concurrently -c \"auto\" \"npm:mock-rest\" \"vite preview\"",
|
||||||
"build-hosted": "env-cmd -f .env.hosted npm run build",
|
"mock-rest": "bun --watch ../mock-api/rest_server.ts",
|
||||||
"build-localhost": "PUBLIC_URL=/ react-app-rewired build",
|
"standalone": "concurrently -c \"auto\" \"typesafe-i18n\" \"npm:mock-rest\" \"vite\"",
|
||||||
"mock-api": "nodemon --watch ../mock-api ../mock-api/server.js",
|
"typesafe-i18n": "typesafe-i18n --no-watch",
|
||||||
"standalone": "npm-run-all -p start mock-api",
|
"webUI": "node progmem-generator.js",
|
||||||
"lint": "eslint . --ext .ts,.tsx"
|
"format": "prettier -l -w '**/*.{ts,tsx,js,css,json,md}'",
|
||||||
|
"lint": "eslint . --fix"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"dependencies": {
|
||||||
"extends": [
|
"@alova/adapter-xhr": "2.0.9",
|
||||||
"react-app",
|
"@emotion/react": "^11.13.3",
|
||||||
"react-app/jest"
|
"@emotion/styled": "^11.13.0",
|
||||||
],
|
"@mui/icons-material": "^6.1.5",
|
||||||
"rules": {
|
"@mui/material": "^6.1.5",
|
||||||
"eol-last": 1,
|
"@table-library/react-table-library": "4.1.7",
|
||||||
"react/jsx-closing-bracket-location": 1,
|
"alova": "3.1.1",
|
||||||
"react/jsx-closing-tag-location": 1,
|
"async-validator": "^4.2.5",
|
||||||
"react/jsx-wrap-multilines": 1,
|
"jwt-decode": "^4.0.0",
|
||||||
"react/jsx-curly-newline": 1,
|
"mime-types": "^2.1.35",
|
||||||
"no-multiple-empty-lines": [
|
"preact": "^10.24.3",
|
||||||
1,
|
"react": "^18.3.1",
|
||||||
{
|
"react-dom": "^18.3.1",
|
||||||
"max": 1
|
"react-icons": "^5.3.0",
|
||||||
}
|
"react-router-dom": "^6.27.0",
|
||||||
],
|
"react-toastify": "^10.0.6",
|
||||||
"no-trailing-spaces": 1,
|
"typesafe-i18n": "^5.26.2",
|
||||||
"semi": 1,
|
"typescript": "^5.6.3"
|
||||||
"no-extra-semi": 1,
|
|
||||||
"react/jsx-max-props-per-line": [
|
|
||||||
1,
|
|
||||||
{
|
|
||||||
"when": "multiline"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"react/jsx-first-prop-new-line": [
|
|
||||||
1,
|
|
||||||
"multiline"
|
|
||||||
],
|
|
||||||
"@typescript-eslint/no-shadow": 1,
|
|
||||||
"max-len": [
|
|
||||||
1,
|
|
||||||
{
|
|
||||||
"code": 200
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"arrow-parens": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"browserslist": {
|
|
||||||
"production": [
|
|
||||||
">0.2%",
|
|
||||||
"not dead",
|
|
||||||
"not op_mini all"
|
|
||||||
],
|
|
||||||
"development": [
|
|
||||||
"last 1 chrome version",
|
|
||||||
"last 1 firefox version",
|
|
||||||
"last 1 safari version"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^2.0.20",
|
"@babel/core": "^7.26.0",
|
||||||
"npm-run-all": "^4.1.5"
|
"@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": "^5.0.1"
|
||||||
|
},
|
||||||
|
"packageManager": "yarn@4.5.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,33 @@
|
|||||||
const { resolve, relative, sep } = require('path');
|
import crypto from 'crypto';
|
||||||
const { readdirSync, existsSync, unlinkSync, readFileSync, createWriteStream } = require('fs');
|
import {
|
||||||
var zlib = require('zlib');
|
createWriteStream,
|
||||||
var mime = require('mime-types');
|
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 ARDUINO_INCLUDES = '#include <Arduino.h>\n\n';
|
||||||
|
const INDENT = ' ';
|
||||||
|
const outputPath = '../lib/framework/WWWData.h';
|
||||||
|
const sourcePath = './dist';
|
||||||
|
const bytesPerLine = 20;
|
||||||
|
var totalSize = 0;
|
||||||
|
|
||||||
|
const generateWWWClass = () =>
|
||||||
|
`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}, "${file.hash}");`).join('\n')}
|
||||||
|
${indent.repeat(2)}}
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
|
||||||
function getFilesSync(dir, files = []) {
|
function getFilesSync(dir, files = []) {
|
||||||
readdirSync(dir, { withFileTypes: true }).forEach((entry) => {
|
readdirSync(dir, { withFileTypes: true }).forEach((entry) => {
|
||||||
@@ -17,10 +41,6 @@ function getFilesSync(dir, files = []) {
|
|||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
function coherseToBuffer(input) {
|
|
||||||
return Buffer.isBuffer(input) ? input : Buffer.from(input);
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanAndOpen(path) {
|
function cleanAndOpen(path) {
|
||||||
if (existsSync(path)) {
|
if (existsSync(path)) {
|
||||||
unlinkSync(path);
|
unlinkSync(path);
|
||||||
@@ -28,94 +48,68 @@ function cleanAndOpen(path) {
|
|||||||
return createWriteStream(path, { flags: 'w+' });
|
return createWriteStream(path, { flags: 'w+' });
|
||||||
}
|
}
|
||||||
|
|
||||||
class ProgmemGenerator {
|
const writeFile = (relativeFilePath, buffer) => {
|
||||||
constructor(options = {}) {
|
const variable = 'ESP_REACT_DATA_' + fileInfo.length;
|
||||||
const { outputPath, bytesPerLine = 20, indent = ' ', includes = ARDUINO_INCLUDES } = options;
|
const mimeType = mime.lookup(relativeFilePath);
|
||||||
this.options = { outputPath, bytesPerLine, indent, includes };
|
var size = 0;
|
||||||
|
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');
|
||||||
|
writeStream.write(indent);
|
||||||
|
}
|
||||||
|
writeStream.write('0x' + ('00' + b.toString(16).toUpperCase()).slice(-2) + ',');
|
||||||
|
size++;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (size % bytesPerLine) {
|
||||||
|
writeStream.write('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
apply(compiler) {
|
writeStream.write('};\n\n');
|
||||||
compiler.hooks.emit.tapAsync({ name: 'ProgmemGenerator' }, (compilation, callback) => {
|
|
||||||
const { outputPath, bytesPerLine, indent, includes } = this.options;
|
|
||||||
const fileInfo = [];
|
|
||||||
const writeStream = cleanAndOpen(resolve(compilation.options.context, outputPath));
|
|
||||||
try {
|
|
||||||
const writeIncludes = () => {
|
|
||||||
writeStream.write(includes);
|
|
||||||
};
|
|
||||||
|
|
||||||
const writeFile = (relativeFilePath, buffer) => {
|
fileInfo.push({
|
||||||
const variable = 'ESP_REACT_DATA_' + fileInfo.length;
|
uri: '/' + relativeFilePath.replace(sep, '/'),
|
||||||
const mimeType = mime.lookup(relativeFilePath);
|
mimeType,
|
||||||
var size = 0;
|
variable,
|
||||||
writeStream.write('const uint8_t ' + variable + '[] PROGMEM = {');
|
size,
|
||||||
const zipBuffer = zlib.gzipSync(buffer);
|
hash
|
||||||
zipBuffer.forEach((b) => {
|
});
|
||||||
if (!(size % bytesPerLine)) {
|
|
||||||
writeStream.write('\n');
|
|
||||||
writeStream.write(indent);
|
|
||||||
}
|
|
||||||
writeStream.write('0x' + ('00' + b.toString(16).toUpperCase()).substr(-2) + ',');
|
|
||||||
size++;
|
|
||||||
});
|
|
||||||
if (size % bytesPerLine) {
|
|
||||||
writeStream.write('\n');
|
|
||||||
}
|
|
||||||
writeStream.write('};\n\n');
|
|
||||||
fileInfo.push({
|
|
||||||
uri: '/' + relativeFilePath.replace(sep, '/'),
|
|
||||||
mimeType,
|
|
||||||
variable,
|
|
||||||
size
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const writeFiles = () => {
|
// console.log(relativeFilePath + ' (size ' + size + ' bytes)');
|
||||||
// process static files
|
totalSize += size;
|
||||||
const buildPath = compilation.options.output.path;
|
|
||||||
for (const filePath of getFilesSync(buildPath)) {
|
|
||||||
const readStream = readFileSync(filePath);
|
|
||||||
const relativeFilePath = relative(buildPath, filePath);
|
|
||||||
writeFile(relativeFilePath, readStream);
|
|
||||||
}
|
|
||||||
// process assets
|
|
||||||
const { assets } = compilation;
|
|
||||||
Object.keys(assets).forEach((relativeFilePath) => {
|
|
||||||
writeFile(relativeFilePath, coherseToBuffer(assets[relativeFilePath].source()));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const generateWWWClass = () => {
|
|
||||||
// eslint-disable-next-line max-len
|
|
||||||
return `typedef std::function<void(const String& uri, const String& contentType, const uint8_t * content, size_t len)> RouteRegistrationHandler;
|
|
||||||
|
|
||||||
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')}
|
|
||||||
${indent.repeat(2)}}
|
|
||||||
};
|
};
|
||||||
`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const writeWWWClass = () => {
|
// start
|
||||||
writeStream.write(generateWWWClass());
|
console.log('Generating ' + outputPath + ' from ' + sourcePath);
|
||||||
};
|
const includes = ARDUINO_INCLUDES;
|
||||||
|
const indent = INDENT;
|
||||||
|
const fileInfo = [];
|
||||||
|
const writeStream = cleanAndOpen(resolve(outputPath));
|
||||||
|
|
||||||
writeIncludes();
|
// includes
|
||||||
writeFiles();
|
writeStream.write(includes);
|
||||||
writeWWWClass();
|
|
||||||
|
|
||||||
writeStream.on('finish', () => {
|
// process static files
|
||||||
callback();
|
const buildPath = resolve(sourcePath);
|
||||||
});
|
for (const filePath of getFilesSync(buildPath)) {
|
||||||
} finally {
|
const readStream = readFileSync(filePath);
|
||||||
writeStream.end();
|
const relativeFilePath = relative(buildPath, filePath);
|
||||||
}
|
writeFile(relativeFilePath, readStream);
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = ProgmemGenerator;
|
// add class
|
||||||
|
writeStream.write(generateWWWClass());
|
||||||
|
|
||||||
|
// end
|
||||||
|
writeStream.end();
|
||||||
|
|
||||||
|
console.log('Total size: ' + totalSize / 1000 + ' KB');
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 5.6 KiB |
@@ -1,24 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
* Just supporting latin due to size constrains on the esp chip
|
* 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
|
||||||
* The framework only makes use of 400 (regular) + 500 (medium) weight fonts.
|
* 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
|
||||||
* If using light or strong typography variants you will need to add additional fonts.
|
|
||||||
*/
|
*/
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Roboto';
|
font-family: 'Roboto';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: local('Roboto'), local('Roboto-Regular'), url(../fonts/re.woff2) format('woff2');
|
/* src: url(https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu4mxK.woff2) format('woff2'); */
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC,
|
src:
|
||||||
U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
local('Roboto'),
|
||||||
}
|
local('Roboto-Regular'),
|
||||||
|
url(../fonts/re.woff2) format('woff2');
|
||||||
@font-face {
|
unicode-range: U+0000-00FF, U+0104-0107, U+0118-0119, U+011E-011F, U+0130-0131,
|
||||||
font-family: 'Roboto';
|
U+0141-0144, U+0152-0153, U+015A-015B, U+015E-015F, U+0179-017C, U+02BB-02BC,
|
||||||
font-style: normal;
|
U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193,
|
||||||
font-weight: 500;
|
U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
src: local('Roboto Medium'), local('Roboto-Medium'), url(../fonts/md.woff2) format('woff2');
|
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, 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.
Binary file not shown.
@@ -1,17 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta
|
|
||||||
name="viewport"
|
|
||||||
content="width=device-width, initial-scale=1, user-scalable=0, maximum-scale=1, minimum-scale=1"
|
|
||||||
/>
|
|
||||||
<link rel="stylesheet" href="%PUBLIC_URL%/css/roboto.css" />
|
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/app/manifest.json" />
|
|
||||||
<title>EMS-ESP</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<noscript> You need to enable JavaScript to run this app. </noscript>
|
|
||||||
<div id="root"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,44 +1,45 @@
|
|||||||
import { FC, createRef, createContext, useContext, RefObject } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { SnackbarProvider } from 'notistack';
|
import { Slide, ToastContainer } from 'react-toastify';
|
||||||
|
import 'react-toastify/dist/ReactToastify.min.css';
|
||||||
|
|
||||||
import { IconButton } from '@mui/material';
|
import AppRouting from 'AppRouting';
|
||||||
import CloseIcon from '@mui/icons-material/Close';
|
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';
|
||||||
|
|
||||||
import { FeaturesLoader } from './contexts/features';
|
const detectedLocale = detectLocale(localStorageDetector);
|
||||||
|
|
||||||
import CustomTheme from './CustomTheme';
|
const App = () => {
|
||||||
import AppRouting from './AppRouting';
|
const [wasLoaded, setWasLoaded] = useState(false);
|
||||||
|
|
||||||
const App: FC = () => {
|
useEffect(() => {
|
||||||
const notistackRef: RefObject<any> = createRef();
|
void loadLocaleAsync(detectedLocale).then(() => setWasLoaded(true));
|
||||||
|
}, []);
|
||||||
|
|
||||||
const onClickDismiss = (key: string | number | undefined) => () => {
|
if (!wasLoaded) return null;
|
||||||
notistackRef.current.closeSnackbar(key);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ColorModeContext = createContext({ toggleColorMode: () => {} });
|
|
||||||
|
|
||||||
const colorMode = useContext(ColorModeContext);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ColorModeContext.Provider value={colorMode}>
|
<TypesafeI18n locale={detectedLocale}>
|
||||||
<CustomTheme>
|
<CustomTheme>
|
||||||
<SnackbarProvider
|
<AppRouting />
|
||||||
maxSnack={3}
|
<ToastContainer
|
||||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
position="bottom-left"
|
||||||
ref={notistackRef}
|
autoClose={3000}
|
||||||
action={(key) => (
|
hideProgressBar={false}
|
||||||
<IconButton onClick={onClickDismiss(key)} size="small">
|
newestOnTop={false}
|
||||||
<CloseIcon />
|
closeOnClick={true}
|
||||||
</IconButton>
|
rtl={false}
|
||||||
)}
|
pauseOnFocusLoss={false}
|
||||||
>
|
draggable={false}
|
||||||
<FeaturesLoader>
|
pauseOnHover={false}
|
||||||
<AppRouting />
|
transition={Slide}
|
||||||
</FeaturesLoader>
|
closeButton={false}
|
||||||
</SnackbarProvider>
|
theme="light"
|
||||||
|
/>
|
||||||
</CustomTheme>
|
</CustomTheme>
|
||||||
</ColorModeContext.Provider>
|
</TypesafeI18n>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,63 +1,49 @@
|
|||||||
import { FC, useContext, useEffect } from 'react';
|
import { useContext, useEffect } from 'react';
|
||||||
import { Navigate, Routes, Route, useLocation } from 'react-router-dom';
|
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||||
import { useSnackbar, VariantType } from 'notistack';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import { Authentication, AuthenticationContext } from './contexts/authentication';
|
import AuthenticatedRouting from 'AuthenticatedRouting';
|
||||||
import { FeaturesContext } from './contexts/features';
|
import SignIn from 'SignIn';
|
||||||
import { RequireAuthenticated, RequireUnauthenticated } from './components';
|
import { RequireAuthenticated, RequireUnauthenticated } from 'components';
|
||||||
|
import { Authentication, AuthenticationContext } from 'contexts/authentication';
|
||||||
import SignIn from './SignIn';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
import AuthenticatedRouting from './AuthenticatedRouting';
|
|
||||||
|
|
||||||
interface SecurityRedirectProps {
|
interface SecurityRedirectProps {
|
||||||
message: string;
|
message: string;
|
||||||
variant?: VariantType;
|
|
||||||
signOut?: boolean;
|
signOut?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RootRedirect: FC<SecurityRedirectProps> = ({ message, variant, signOut }) => {
|
const RootRedirect = ({ message, signOut }: SecurityRedirectProps) => {
|
||||||
const authenticationContext = useContext(AuthenticationContext);
|
const authenticationContext = useContext(AuthenticationContext);
|
||||||
const { enqueueSnackbar } = useSnackbar();
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
signOut && authenticationContext.signOut(false);
|
signOut && authenticationContext.signOut(false);
|
||||||
enqueueSnackbar(message, { variant });
|
toast.success(message);
|
||||||
}, [message, variant, signOut, authenticationContext, enqueueSnackbar]);
|
}, [message, signOut, authenticationContext]);
|
||||||
return <Navigate to="/" />;
|
return <Navigate to="/" />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RemoveTrailingSlashes = () => {
|
const AppRouting = () => {
|
||||||
const location = useLocation();
|
const { LL } = useI18nContext();
|
||||||
return (
|
|
||||||
location.pathname.match('/.*/$') && (
|
|
||||||
<Navigate
|
|
||||||
to={{
|
|
||||||
pathname: location.pathname.replace(/\/+$/, ''),
|
|
||||||
search: location.search
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const AppRouting: FC = () => {
|
|
||||||
const { features } = useContext(FeaturesContext);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Authentication>
|
<Authentication>
|
||||||
<RemoveTrailingSlashes />
|
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/unauthorized" element={<RootRedirect message="Please sign in to continue" signOut />} />
|
<Route
|
||||||
<Route path="/fileUpdated" element={<RootRedirect message="Upload successful" variant="success" />} />
|
path="/unauthorized"
|
||||||
{features.security && (
|
element={<RootRedirect message={LL.PLEASE_SIGNIN()} signOut />}
|
||||||
<Route
|
/>
|
||||||
path="/"
|
<Route
|
||||||
element={
|
path="/fileUpdated"
|
||||||
<RequireUnauthenticated>
|
element={<RootRedirect message={LL.UPLOAD_SUCCESSFUL()} />}
|
||||||
<SignIn />
|
/>
|
||||||
</RequireUnauthenticated>
|
<Route
|
||||||
}
|
path="/"
|
||||||
/>
|
element={
|
||||||
)}
|
<RequireUnauthenticated>
|
||||||
|
<SignIn />
|
||||||
|
</RequireUnauthenticated>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/*"
|
path="/*"
|
||||||
element={
|
element={
|
||||||
|
|||||||
@@ -1,63 +1,73 @@
|
|||||||
import { FC, useCallback, useContext, useEffect } from 'react';
|
import { useContext } from 'react';
|
||||||
import { Navigate, Routes, Route, useNavigate, useLocation } from 'react-router-dom';
|
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
|
|
||||||
import { FeaturesContext } from './contexts/features';
|
import CustomEntities from 'app/main/CustomEntities';
|
||||||
import * as AuthenticationApi from './api/authentication';
|
import Customizations from 'app/main/Customizations';
|
||||||
import { PROJECT_PATH } from './api/env';
|
import Dashboard from 'app/main/Dashboard';
|
||||||
import { AXIOS } from './api/endpoints';
|
import Devices from 'app/main/Devices';
|
||||||
import { Layout, RequireAdmin } from './components';
|
import Help from 'app/main/Help';
|
||||||
|
import Modules from 'app/main/Modules';
|
||||||
import ProjectRouting from './project/ProjectRouting';
|
import Scheduler from 'app/main/Scheduler';
|
||||||
|
import Sensors from 'app/main/Sensors';
|
||||||
import NetworkConnection from './framework/network/NetworkConnection';
|
import APSettings from 'app/settings/APSettings';
|
||||||
import AccessPoint from './framework/ap/AccessPoint';
|
import ApplicationSettings from 'app/settings/ApplicationSettings';
|
||||||
import NetworkTime from './framework/ntp/NetworkTime';
|
import DownloadUpload from 'app/settings/DownloadUpload';
|
||||||
import Mqtt from './framework/mqtt/Mqtt';
|
import MqttSettings from 'app/settings/MqttSettings';
|
||||||
import System from './framework/system/System';
|
import NTPSettings from 'app/settings/NTPSettings';
|
||||||
import Security from './framework/security/Security';
|
import Settings from 'app/settings/Settings';
|
||||||
|
import Version from 'app/settings/Version';
|
||||||
const AuthenticatedRouting: FC = () => {
|
import Network from 'app/settings/network/Network';
|
||||||
const { features } = useContext(FeaturesContext);
|
import Security from 'app/settings/security/Security';
|
||||||
const location = useLocation();
|
import APStatus from 'app/status/APStatus';
|
||||||
const navigate = useNavigate();
|
import Activity from 'app/status/Activity';
|
||||||
|
import HardwareStatus from 'app/status/HardwareStatus';
|
||||||
const handleApiResponseError = useCallback(
|
import MqttStatus from 'app/status/MqttStatus';
|
||||||
(error: AxiosError) => {
|
import NTPStatus from 'app/status/NTPStatus';
|
||||||
if (error.response && error.response.status === 401) {
|
import NetworkStatus from 'app/status/NetworkStatus';
|
||||||
AuthenticationApi.storeLoginRedirect(location);
|
import Status from 'app/status/Status';
|
||||||
navigate('/unauthorized');
|
import SystemLog from 'app/status/SystemLog';
|
||||||
}
|
import { Layout } from 'components';
|
||||||
return Promise.reject(error);
|
import { AuthenticatedContext } from 'contexts/authentication';
|
||||||
},
|
|
||||||
[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 (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<Routes>
|
<Routes>
|
||||||
{features.project && <Route path={`/${PROJECT_PATH}/*`} element={<ProjectRouting />} />}
|
<Route path="/dashboard/*" element={<Dashboard />} />
|
||||||
<Route path="/network/*" element={<NetworkConnection />} />
|
<Route path="/devices/*" element={<Devices />} />
|
||||||
<Route path="/ap/*" element={<AccessPoint />} />
|
<Route path="/sensors/*" element={<Sensors />} />
|
||||||
{features.ntp && <Route path="/ntp/*" element={<NetworkTime />} />}
|
<Route path="/status/*" element={<Status />} />
|
||||||
{features.mqtt && <Route path="/mqtt/*" element={<Mqtt />} />}
|
<Route path="/help/*" element={<Help />} />
|
||||||
{features.security && (
|
<Route path="/*" element={<Navigate to="/" />} />
|
||||||
<Route
|
|
||||||
path="/security/*"
|
<Route path="/status/hardwarestatus/*" element={<HardwareStatus />} />
|
||||||
element={
|
<Route path="/status/activity" element={<Activity />} />
|
||||||
<RequireAdmin>
|
<Route path="/status/log" element={<SystemLog />} />
|
||||||
<Security />
|
<Route path="/status/mqtt" element={<MqttStatus />} />
|
||||||
</RequireAdmin>
|
<Route path="/status/ntp" element={<NTPStatus />} />
|
||||||
}
|
<Route path="/status/ap" element={<APStatus />} />
|
||||||
/>
|
<Route path="/status/network" element={<NetworkStatus />} />
|
||||||
|
|
||||||
|
{me.admin && (
|
||||||
|
<>
|
||||||
|
<Route path="/settings" element={<Settings />} />
|
||||||
|
<Route path="/settings/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 />} />
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<Route path="/system/*" element={<System />} />
|
|
||||||
<Route path="/*" element={<Navigate to={AuthenticationApi.getDefaultRoute(features)} />} />
|
|
||||||
</Routes>
|
</Routes>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,10 +1,22 @@
|
|||||||
import { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
|
|
||||||
import { CssBaseline } from '@mui/material';
|
import { CssBaseline } from '@mui/material';
|
||||||
import { createTheme, responsiveFontSizes, ThemeProvider } from '@mui/material/styles';
|
import {
|
||||||
import { blueGrey, blue } from '@mui/material/colors';
|
ThemeProvider,
|
||||||
|
createTheme,
|
||||||
|
responsiveFontSizes
|
||||||
|
} from '@mui/material/styles';
|
||||||
|
|
||||||
import { RequiredChildrenProps } from './utils';
|
import type { RequiredChildrenProps } from 'utils';
|
||||||
|
|
||||||
|
export const dialogStyle = {
|
||||||
|
'& .MuiDialog-paper': {
|
||||||
|
borderRadius: '8px',
|
||||||
|
borderColor: '#565656',
|
||||||
|
borderStyle: 'solid',
|
||||||
|
borderWidth: '1px'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const theme = responsiveFontSizes(
|
const theme = responsiveFontSizes(
|
||||||
createTheme({
|
createTheme({
|
||||||
@@ -14,10 +26,13 @@ const theme = responsiveFontSizes(
|
|||||||
palette: {
|
palette: {
|
||||||
mode: 'dark',
|
mode: 'dark',
|
||||||
secondary: {
|
secondary: {
|
||||||
main: blue[500]
|
main: '#2196f3' // blue[500]
|
||||||
},
|
},
|
||||||
info: {
|
info: {
|
||||||
main: blueGrey[500]
|
main: '#607d8b' // blueGrey[500]
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
disabled: '#eee' // white
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,24 +1,28 @@
|
|||||||
import { FC, useContext, useState } from 'react';
|
import { useContext, useState } from 'react';
|
||||||
import { ValidateFieldsError } from 'async-validator';
|
import { toast } from 'react-toastify';
|
||||||
import { useSnackbar } from 'notistack';
|
|
||||||
|
|
||||||
import { Box, Fab, Paper, Typography } from '@mui/material';
|
|
||||||
import ForwardIcon from '@mui/icons-material/Forward';
|
import ForwardIcon from '@mui/icons-material/Forward';
|
||||||
|
import { Box, Button, Paper, Typography } from '@mui/material';
|
||||||
|
|
||||||
import * as AuthenticationApi from './api/authentication';
|
import * as AuthenticationApi from 'components/routing/authentication';
|
||||||
import { PROJECT_NAME } from './api/env';
|
import { useRequest } from 'alova/client';
|
||||||
import { AuthenticationContext } from './contexts/authentication';
|
import type { ValidateFieldsError } from 'async-validator';
|
||||||
|
import {
|
||||||
|
LanguageSelector,
|
||||||
|
ValidatedPasswordField,
|
||||||
|
ValidatedTextField
|
||||||
|
} from 'components';
|
||||||
|
import { AuthenticationContext } from 'contexts/authentication';
|
||||||
|
import { PROJECT_NAME } from 'env';
|
||||||
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
import type { SignInRequest } from 'types';
|
||||||
|
import { onEnterCallback, updateValue } from 'utils';
|
||||||
|
import { SIGN_IN_REQUEST_VALIDATOR, validate } from 'validators';
|
||||||
|
|
||||||
import { AxiosError } from 'axios';
|
const SignIn = () => {
|
||||||
|
|
||||||
import { extractErrorMessage, onEnterCallback, updateValue } from './utils';
|
|
||||||
import { SignInRequest } from './types';
|
|
||||||
import { ValidatedTextField } from './components';
|
|
||||||
import { SIGN_IN_REQUEST_VALIDATOR, validate } from './validators';
|
|
||||||
|
|
||||||
const SignIn: FC = () => {
|
|
||||||
const authenticationContext = useContext(AuthenticationContext);
|
const authenticationContext = useContext(AuthenticationContext);
|
||||||
const { enqueueSnackbar } = useSnackbar();
|
|
||||||
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
const [signInRequest, setSignInRequest] = useState<SignInRequest>({
|
const [signInRequest, setSignInRequest] = useState<SignInRequest>({
|
||||||
username: '',
|
username: '',
|
||||||
@@ -27,31 +31,40 @@ const SignIn: FC = () => {
|
|||||||
const [processing, setProcessing] = useState<boolean>(false);
|
const [processing, setProcessing] = useState<boolean>(false);
|
||||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
|
|
||||||
|
const { send: callSignIn } = useRequest(
|
||||||
|
(request: SignInRequest) => AuthenticationApi.signIn(request),
|
||||||
|
{
|
||||||
|
immediate: false
|
||||||
|
}
|
||||||
|
).onSuccess((response) => {
|
||||||
|
if (response.data) {
|
||||||
|
authenticationContext.signIn(response.data.access_token);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const updateLoginRequestValue = updateValue(setSignInRequest);
|
const updateLoginRequestValue = updateValue(setSignInRequest);
|
||||||
|
|
||||||
|
const signIn = async () => {
|
||||||
|
await callSignIn(signInRequest).catch((event: Error) => {
|
||||||
|
if (event.message === 'Unauthorized') {
|
||||||
|
toast.warning(LL.INVALID_LOGIN());
|
||||||
|
} else {
|
||||||
|
toast.error(LL.ERROR() + ' ' + event.message);
|
||||||
|
}
|
||||||
|
setProcessing(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const validateAndSignIn = async () => {
|
const validateAndSignIn = async () => {
|
||||||
setProcessing(true);
|
setProcessing(true);
|
||||||
|
SIGN_IN_REQUEST_VALIDATOR.messages({
|
||||||
|
required: LL.IS_REQUIRED('%s')
|
||||||
|
});
|
||||||
try {
|
try {
|
||||||
await validate(SIGN_IN_REQUEST_VALIDATOR, signInRequest);
|
await validate(SIGN_IN_REQUEST_VALIDATOR, signInRequest);
|
||||||
signIn();
|
await signIn();
|
||||||
} catch (errors: any) {
|
} catch (error) {
|
||||||
setFieldErrors(errors);
|
setFieldErrors(error as ValidateFieldsError);
|
||||||
setProcessing(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const signIn = async () => {
|
|
||||||
try {
|
|
||||||
const { data: loginResponse } = await AuthenticationApi.signIn(signInRequest);
|
|
||||||
authenticationContext.signIn(loginResponse.access_token);
|
|
||||||
} catch (error: unknown) {
|
|
||||||
if (error instanceof AxiosError) {
|
|
||||||
if (error.response?.status === 401) {
|
|
||||||
enqueueSnackbar('Invalid login details', { variant: 'warning' });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
enqueueSnackbar(extractErrorMessage(error, 'Unexpected error, please try again'), { variant: 'error' });
|
|
||||||
}
|
|
||||||
setProcessing(false);
|
setProcessing(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -72,43 +85,62 @@ const SignIn: FC = () => {
|
|||||||
sx={(theme) => ({
|
sx={(theme) => ({
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
padding: theme.spacing(2),
|
padding: theme.spacing(2),
|
||||||
paddingTop: '200px',
|
paddingTop: '172px',
|
||||||
backgroundImage: 'url("/app/icon.png")',
|
backgroundImage: 'url("/app/icon.png")',
|
||||||
backgroundRepeat: 'no-repeat',
|
backgroundRepeat: 'no-repeat',
|
||||||
backgroundPosition: '50% ' + theme.spacing(2),
|
backgroundPosition: '50% ' + theme.spacing(2),
|
||||||
backgroundSize: 'auto 150px',
|
|
||||||
width: '100%'
|
width: '100%'
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Typography variant="h4">{PROJECT_NAME}</Typography>
|
<Typography variant="h4">{PROJECT_NAME}</Typography>
|
||||||
<ValidatedTextField
|
|
||||||
fieldErrors={fieldErrors}
|
<LanguageSelector />
|
||||||
|
|
||||||
|
<Box display="flex" flexDirection="column" alignItems="center">
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
|
disabled={processing}
|
||||||
|
sx={{
|
||||||
|
width: 240
|
||||||
|
}}
|
||||||
|
name="username"
|
||||||
|
label={LL.USERNAME(0)}
|
||||||
|
value={signInRequest.username}
|
||||||
|
onChange={updateLoginRequestValue}
|
||||||
|
margin="normal"
|
||||||
|
variant="outlined"
|
||||||
|
slotProps={{
|
||||||
|
input: {
|
||||||
|
autoCapitalize: 'none',
|
||||||
|
autoCorrect: 'off'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ValidatedPasswordField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
|
disabled={processing}
|
||||||
|
sx={{
|
||||||
|
width: 240
|
||||||
|
}}
|
||||||
|
name="password"
|
||||||
|
label={LL.PASSWORD()}
|
||||||
|
value={signInRequest.password}
|
||||||
|
onChange={updateLoginRequestValue}
|
||||||
|
onKeyDown={submitOnEnter}
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
sx={{ mt: 2 }}
|
||||||
|
onClick={validateAndSignIn}
|
||||||
disabled={processing}
|
disabled={processing}
|
||||||
name="username"
|
>
|
||||||
label="Username"
|
|
||||||
value={signInRequest.username}
|
|
||||||
onChange={updateLoginRequestValue}
|
|
||||||
margin="normal"
|
|
||||||
variant="outlined"
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
<ValidatedTextField
|
|
||||||
fieldErrors={fieldErrors}
|
|
||||||
disabled={processing}
|
|
||||||
type="password"
|
|
||||||
name="password"
|
|
||||||
label="Password"
|
|
||||||
value={signInRequest.password}
|
|
||||||
onChange={updateLoginRequestValue}
|
|
||||||
onKeyDown={submitOnEnter}
|
|
||||||
margin="normal"
|
|
||||||
variant="outlined"
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
<Fab variant="extended" color="primary" sx={{ mt: 2 }} onClick={validateAndSignIn} disabled={processing}>
|
|
||||||
<ForwardIcon sx={{ mr: 1 }} />
|
<ForwardIcon sx={{ mr: 1 }} />
|
||||||
Sign In
|
{LL.SIGN_IN()}
|
||||||
</Fab>
|
</Button>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,16 +1,9 @@
|
|||||||
import { AxiosPromise } from 'axios';
|
import type { APSettingsType, APStatusType } from 'types';
|
||||||
|
|
||||||
import { APSettings, APStatus } from '../types';
|
import { alovaInstance } from './endpoints';
|
||||||
import { AXIOS } from './endpoints';
|
|
||||||
|
|
||||||
export function readAPStatus(): AxiosPromise<APStatus> {
|
export const readAPStatus = () => alovaInstance.Get<APStatusType>('/rest/apStatus');
|
||||||
return AXIOS.get('/apStatus');
|
export const readAPSettings = () =>
|
||||||
}
|
alovaInstance.Get<APSettingsType>('/rest/apSettings');
|
||||||
|
export const updateAPSettings = (data: APSettingsType) =>
|
||||||
export function readAPSettings(): AxiosPromise<APSettings> {
|
alovaInstance.Post<APSettingsType>('/rest/apSettings', data);
|
||||||
return AXIOS.get('/apSettings');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateAPSettings(apSettings: APSettings): AxiosPromise<APSettings> {
|
|
||||||
return AXIOS.post('/apSettings', apSettings);
|
|
||||||
}
|
|
||||||
|
|||||||
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,105 +1,63 @@
|
|||||||
import axios, { AxiosPromise, CancelToken } from 'axios';
|
import { type AlovaXHRResponse, xhrRequestAdapter } from '@alova/adapter-xhr';
|
||||||
|
import { createAlova } from 'alova';
|
||||||
|
import ReactHook from 'alova/react';
|
||||||
|
|
||||||
import { decode } from '@msgpack/msgpack';
|
import { unpack } from './unpack';
|
||||||
|
|
||||||
export const WS_BASE_URL = '/ws/';
|
|
||||||
export const API_BASE_URL = '/rest/';
|
|
||||||
export const ES_BASE_URL = '/es/';
|
|
||||||
export const EMSESP_API_BASE_URL = '/api/';
|
|
||||||
export const ACCESS_TOKEN = 'access_token';
|
export const ACCESS_TOKEN = 'access_token';
|
||||||
export const WEB_SOCKET_ROOT = calculateWebSocketRoot(WS_BASE_URL);
|
|
||||||
export const EVENT_SOURCE_ROOT = calculateEventSourceRoot(ES_BASE_URL);
|
|
||||||
|
|
||||||
export const AXIOS = axios.create({
|
export const alovaInstance = createAlova({
|
||||||
baseURL: API_BASE_URL,
|
statesHook: ReactHook,
|
||||||
headers: {
|
// timeout: 3000, // 3 seconds before throwing a timeout error, default is 0 = none
|
||||||
'Content-Type': 'application/json'
|
cacheFor: null, // disable cache
|
||||||
},
|
// cacheFor: {
|
||||||
transformRequest: [
|
// GET: {
|
||||||
(data, headers) => {
|
// mode: 'memory',
|
||||||
if (headers) {
|
// expire: 60 * 10 * 1000 // 60 seconds in cache
|
||||||
if (localStorage.getItem(ACCESS_TOKEN)) {
|
// }
|
||||||
headers.Authorization = 'Bearer ' + localStorage.getItem(ACCESS_TOKEN);
|
// },
|
||||||
}
|
requestAdapter: xhrRequestAdapter(),
|
||||||
if (headers['Content-Type'] !== 'application/json') {
|
beforeRequest(method) {
|
||||||
return data;
|
if (localStorage.getItem(ACCESS_TOKEN)) {
|
||||||
}
|
method.config.headers.Authorization =
|
||||||
}
|
'Bearer ' + localStorage.getItem(ACCESS_TOKEN);
|
||||||
return JSON.stringify(data);
|
|
||||||
}
|
}
|
||||||
]
|
// for simulating vrey slow networks
|
||||||
|
// return new Promise((resolve) => {
|
||||||
|
// const random = 3000 + Math.random() * 2000;
|
||||||
|
// setTimeout(resolve, Math.floor(random));
|
||||||
|
// });
|
||||||
|
},
|
||||||
|
|
||||||
|
responded: {
|
||||||
|
onSuccess: async (response: AlovaXHRResponse) => {
|
||||||
|
// if (response.status === 202) {
|
||||||
|
// throw new Error('Wait'); // wifi scan in progress
|
||||||
|
// } else
|
||||||
|
if (response.status === 205) {
|
||||||
|
throw new Error('Reboot required');
|
||||||
|
} else if (response.status === 400) {
|
||||||
|
throw new Error('Request Failed');
|
||||||
|
} else if (response.status >= 400) {
|
||||||
|
throw new Error(response.statusText);
|
||||||
|
}
|
||||||
|
const data: ArrayBuffer = (await response.data) as ArrayBuffer;
|
||||||
|
if (response.data instanceof ArrayBuffer) {
|
||||||
|
return unpack(data) as ArrayBuffer;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interceptor for request failure. This interceptor will be entered when the request is wrong.
|
||||||
|
// http errors like 401 (unauthorized) are handled either in the methods or AuthenticatedRouting()
|
||||||
|
// onError: (error, method) => {
|
||||||
|
// alert(error.message);
|
||||||
|
// }
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const AXIOS_API = axios.create({
|
export const alovaInstanceGH = createAlova({
|
||||||
baseURL: EMSESP_API_BASE_URL,
|
baseURL: 'https://api.github.com/repos/emsesp/EMS-ESP32/releases',
|
||||||
headers: {
|
statesHook: ReactHook,
|
||||||
'Content-Type': 'application/json'
|
requestAdapter: xhrRequestAdapter()
|
||||||
},
|
|
||||||
transformRequest: [
|
|
||||||
(data, headers) => {
|
|
||||||
if (headers) {
|
|
||||||
if (localStorage.getItem(ACCESS_TOKEN)) {
|
|
||||||
headers.Authorization = 'Bearer ' + localStorage.getItem(ACCESS_TOKEN);
|
|
||||||
}
|
|
||||||
if (headers['Content-Type'] !== 'application/json') {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return JSON.stringify(data);
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const AXIOS_BIN = axios.create({
|
|
||||||
baseURL: API_BASE_URL,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
responseType: 'arraybuffer',
|
|
||||||
transformRequest: [
|
|
||||||
(data, headers) => {
|
|
||||||
if (headers) {
|
|
||||||
if (localStorage.getItem(ACCESS_TOKEN)) {
|
|
||||||
headers.Authorization = 'Bearer ' + localStorage.getItem(ACCESS_TOKEN);
|
|
||||||
}
|
|
||||||
if (headers['Content-Type'] !== 'application/json') {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return JSON.stringify(data);
|
|
||||||
}
|
|
||||||
],
|
|
||||||
transformResponse: [
|
|
||||||
(data) => {
|
|
||||||
return decode(data);
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
function calculateWebSocketRoot(webSocketPath: string) {
|
|
||||||
const location = window.location;
|
|
||||||
const webProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
||||||
return webProtocol + '//' + location.host + webSocketPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculateEventSourceRoot(endpointPath: string) {
|
|
||||||
const location = window.location;
|
|
||||||
return location.protocol + '//' + location.host + endpointPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FileUploadConfig {
|
|
||||||
cancelToken?: CancelToken;
|
|
||||||
onUploadProgress?: (progressEvent: ProgressEvent) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const startUploadFile = (url: string, file: File, config?: FileUploadConfig): AxiosPromise<void> => {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('file', file);
|
|
||||||
|
|
||||||
return AXIOS.post(url, formData, {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'multipart/form-data'
|
|
||||||
},
|
|
||||||
...(config || {})
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
export const PROJECT_NAME = process.env.REACT_APP_PROJECT_NAME || 'EMS-ESP';
|
|
||||||
export const PROJECT_PATH = process.env.REACT_APP_PROJECT_PATH || 'project';
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { AxiosPromise } from 'axios';
|
|
||||||
|
|
||||||
import { Features } from '../types';
|
|
||||||
|
|
||||||
import { AXIOS } from './endpoints';
|
|
||||||
|
|
||||||
export function readFeatures(): AxiosPromise<Features> {
|
|
||||||
return AXIOS.get('/features');
|
|
||||||
}
|
|
||||||
@@ -1,16 +1,10 @@
|
|||||||
import { AxiosPromise } from 'axios';
|
import type { MqttSettingsType, MqttStatusType } from 'types';
|
||||||
import { MqttSettings, MqttStatus } from '../types';
|
|
||||||
|
|
||||||
import { AXIOS } from './endpoints';
|
import { alovaInstance } from './endpoints';
|
||||||
|
|
||||||
export function readMqttStatus(): AxiosPromise<MqttStatus> {
|
export const readMqttStatus = () =>
|
||||||
return AXIOS.get('/mqttStatus');
|
alovaInstance.Get<MqttStatusType>('/rest/mqttStatus');
|
||||||
}
|
export const readMqttSettings = () =>
|
||||||
|
alovaInstance.Get<MqttSettingsType>('/rest/mqttSettings');
|
||||||
export function readMqttSettings(): AxiosPromise<MqttSettings> {
|
export const updateMqttSettings = (data: MqttSettingsType) =>
|
||||||
return AXIOS.get('/mqttSettings');
|
alovaInstance.Post<MqttSettingsType>('/rest/mqttSettings', data);
|
||||||
}
|
|
||||||
|
|
||||||
export function updateMqttSettings(ntpSettings: MqttSettings): AxiosPromise<MqttSettings> {
|
|
||||||
return AXIOS.post('/mqttSettings', ntpSettings);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,25 +1,15 @@
|
|||||||
import { AxiosPromise } from 'axios';
|
import type { NetworkSettingsType, NetworkStatusType, WiFiNetworkList } from 'types';
|
||||||
|
|
||||||
import { WiFiNetworkList, NetworkSettings, NetworkStatus } from '../types';
|
import { alovaInstance } from './endpoints';
|
||||||
|
|
||||||
import { AXIOS } from './endpoints';
|
export const readNetworkStatus = () =>
|
||||||
|
alovaInstance.Get<NetworkStatusType>('/rest/networkStatus');
|
||||||
export function readNetworkStatus(): AxiosPromise<NetworkStatus> {
|
export const scanNetworks = () => alovaInstance.Get('/rest/scanNetworks');
|
||||||
return AXIOS.get('/networkStatus');
|
export const listNetworks = () =>
|
||||||
}
|
alovaInstance.Get<WiFiNetworkList>('/rest/listNetworks', {
|
||||||
|
timeout: 20000 // 20 seconds
|
||||||
export function scanNetworks(): AxiosPromise<void> {
|
});
|
||||||
return AXIOS.get('/scanNetworks');
|
export const readNetworkSettings = () =>
|
||||||
}
|
alovaInstance.Get<NetworkSettingsType>('/rest/networkSettings');
|
||||||
|
export const updateNetworkSettings = (wifiSettings: NetworkSettingsType) =>
|
||||||
export function listNetworks(): AxiosPromise<WiFiNetworkList> {
|
alovaInstance.Post<NetworkSettingsType>('/rest/networkSettings', wifiSettings);
|
||||||
return AXIOS.get('/listNetworks');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function readNetworkSettings(): AxiosPromise<NetworkSettings> {
|
|
||||||
return AXIOS.get('/networkSettings');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateNetworkSettings(wifiSettings: NetworkSettings): AxiosPromise<NetworkSettings> {
|
|
||||||
return AXIOS.post('/networkSettings', wifiSettings);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,20 +1,14 @@
|
|||||||
import { AxiosPromise } from 'axios';
|
import type { NTPSettingsType, NTPStatusType, Time } from 'types';
|
||||||
import { NTPSettings, NTPStatus, Time } from '../types';
|
|
||||||
|
|
||||||
import { AXIOS } from './endpoints';
|
import { alovaInstance } from './endpoints';
|
||||||
|
|
||||||
export function readNTPStatus(): AxiosPromise<NTPStatus> {
|
export const readNTPStatus = () =>
|
||||||
return AXIOS.get('/ntpStatus');
|
alovaInstance.Get<NTPStatusType>('/rest/ntpStatus');
|
||||||
}
|
|
||||||
|
|
||||||
export function readNTPSettings(): AxiosPromise<NTPSettings> {
|
export const readNTPSettings = () =>
|
||||||
return AXIOS.get('/ntpSettings');
|
alovaInstance.Get<NTPSettingsType>('/rest/ntpSettings', {});
|
||||||
}
|
export const updateNTPSettings = (data: NTPSettingsType) =>
|
||||||
|
alovaInstance.Post<NTPSettingsType>('/rest/ntpSettings', data);
|
||||||
|
|
||||||
export function updateNTPSettings(ntpSettings: NTPSettings): AxiosPromise<NTPSettings> {
|
export const updateTime = (data: Time) =>
|
||||||
return AXIOS.post('/ntpSettings', ntpSettings);
|
alovaInstance.Post<Time>('/rest/time', data);
|
||||||
}
|
|
||||||
|
|
||||||
export function updateTime(time: Time): AxiosPromise<Time> {
|
|
||||||
return AXIOS.post('/time', time);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
import { AxiosPromise } from 'axios';
|
import type { SecuritySettingsType, Token } from 'types';
|
||||||
|
|
||||||
import { SecuritySettings, Token } from '../types';
|
import { alovaInstance } from './endpoints';
|
||||||
|
|
||||||
import { AXIOS } from './endpoints';
|
export const readSecuritySettings = () =>
|
||||||
|
alovaInstance.Get<SecuritySettingsType>('/rest/securitySettings');
|
||||||
|
|
||||||
export function readSecuritySettings(): AxiosPromise<SecuritySettings> {
|
export const updateSecuritySettings = (securitySettings: SecuritySettingsType) =>
|
||||||
return AXIOS.get('/securitySettings');
|
alovaInstance.Post('/rest/securitySettings', securitySettings);
|
||||||
}
|
|
||||||
|
|
||||||
export function updateSecuritySettings(securitySettings: SecuritySettings): AxiosPromise<SecuritySettings> {
|
export const generateToken = (username?: string) =>
|
||||||
return AXIOS.post('/securitySettings', securitySettings);
|
alovaInstance.Get<Token>('/rest/generateToken', {
|
||||||
}
|
params: { username }
|
||||||
|
});
|
||||||
export function generateToken(username?: string): AxiosPromise<Token> {
|
|
||||||
return AXIOS.get('/generateToken', { params: { username } });
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,41 +1,36 @@
|
|||||||
import { AxiosPromise } from 'axios';
|
import type { LogSettings, SystemStatus } from 'types';
|
||||||
|
|
||||||
import { OTASettings, SystemStatus, LogSettings, LogEntries } from '../types';
|
import { alovaInstance, alovaInstanceGH } from './endpoints';
|
||||||
|
|
||||||
import { AXIOS, AXIOS_BIN, FileUploadConfig, startUploadFile } from './endpoints';
|
// systemStatus - also used to ping in Restart monitor for pinging
|
||||||
|
export const readSystemStatus = () =>
|
||||||
|
alovaInstance.Get<SystemStatus>('/rest/systemStatus');
|
||||||
|
|
||||||
export function readSystemStatus(timeout?: number): AxiosPromise<SystemStatus> {
|
// SystemLog
|
||||||
return AXIOS.get('/systemStatus', { timeout });
|
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');
|
||||||
|
|
||||||
export function restart(): AxiosPromise<void> {
|
// Get versions from GitHub
|
||||||
return AXIOS.post('/restart');
|
export const getStableVersion = () =>
|
||||||
}
|
alovaInstanceGH.Get('latest', {
|
||||||
|
transform(response: { data: { name: string } }) {
|
||||||
export function factoryReset(): AxiosPromise<void> {
|
return response.data.name.substring(1);
|
||||||
return AXIOS.post('/factoryReset');
|
}
|
||||||
}
|
});
|
||||||
|
export const getDevVersion = () =>
|
||||||
export function readOTASettings(): AxiosPromise<OTASettings> {
|
alovaInstanceGH.Get('tags/latest', {
|
||||||
return AXIOS.get('/otaSettings');
|
transform(response: { data: { name: string } }) {
|
||||||
}
|
return response.data.name.split(/\s+/).splice(-1)[0].substring(1);
|
||||||
|
}
|
||||||
export function updateOTASettings(otaSettings: OTASettings): AxiosPromise<OTASettings> {
|
});
|
||||||
return AXIOS.post('/otaSettings', otaSettings);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const uploadFile = (file: File, config?: FileUploadConfig): AxiosPromise<void> =>
|
|
||||||
startUploadFile('/uploadFile', file, config);
|
|
||||||
|
|
||||||
export function readLogSettings(): AxiosPromise<LogSettings> {
|
|
||||||
return AXIOS.get('/logSettings');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateLogSettings(logSettings: LogSettings): AxiosPromise<LogSettings> {
|
|
||||||
return AXIOS.post('/logSettings', logSettings);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function readLogEntries(): AxiosPromise<LogEntries> {
|
|
||||||
return AXIOS_BIN.get('/fetchLog');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
1220
interface/src/api/unpack.ts
Normal file
1220
interface/src/api/unpack.ts
Normal file
File diff suppressed because it is too large
Load Diff
342
interface/src/app/main/CustomEntities.tsx
Normal file
342
interface/src/app/main/CustomEntities.tsx
Normal file
@@ -0,0 +1,342 @@
|
|||||||
|
import { useCallback, 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 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';
|
||||||
|
|
||||||
|
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);
|
||||||
|
const [selectedEntityItem, setSelectedEntityItem] = useState<EntityItem>();
|
||||||
|
const [creating, setCreating] = useState<boolean>(false);
|
||||||
|
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useLayoutTitle(LL.CUSTOM_ENTITIES(0));
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: entities,
|
||||||
|
send: fetchEntities,
|
||||||
|
error
|
||||||
|
} = useRequest(readCustomEntities, {
|
||||||
|
initialData: []
|
||||||
|
});
|
||||||
|
|
||||||
|
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 ||
|
||||||
|
ei.offset !== ei.o_offset ||
|
||||||
|
ei.uom !== ei.o_uom ||
|
||||||
|
ei.factor !== ei.o_factor ||
|
||||||
|
ei.value_type !== ei.o_value_type ||
|
||||||
|
ei.writeable !== ei.o_writeable ||
|
||||||
|
ei.deleted !== ei.o_deleted ||
|
||||||
|
(ei.value || '') !== (ei.o_value || '')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const entity_theme = useTheme({
|
||||||
|
Table: `
|
||||||
|
--data-table-library_grid-template-columns: repeat(1, minmax(60px, 1fr)) minmax(80px, auto) 80px 80px 80px 90px;
|
||||||
|
`,
|
||||||
|
BaseRow: `
|
||||||
|
font-size: 14px;
|
||||||
|
.td {
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
BaseCell: `
|
||||||
|
&:nth-of-type(1) {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
&:nth-of-type(2) {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
&:nth-of-type(3) {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
&:nth-of-type(4) {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
&:nth-of-type(5) {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
&:nth-of-type(6) {
|
||||||
|
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-bottom: 1px solid #565656;
|
||||||
|
}
|
||||||
|
&:hover .td {
|
||||||
|
background-color: #177ac9;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
});
|
||||||
|
|
||||||
|
const saveEntities = async () => {
|
||||||
|
await writeEntities({
|
||||||
|
entities: entities
|
||||||
|
.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,
|
||||||
|
offset: condensed_ei.offset,
|
||||||
|
factor: condensed_ei.factor,
|
||||||
|
uom: condensed_ei.uom,
|
||||||
|
writeable: condensed_ei.writeable,
|
||||||
|
value_type: condensed_ei.value_type,
|
||||||
|
value: condensed_ei.value
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success(LL.ENTITIES_UPDATED());
|
||||||
|
})
|
||||||
|
.catch((error: Error) => {
|
||||||
|
toast.error(error.message);
|
||||||
|
})
|
||||||
|
.finally(async () => {
|
||||||
|
await fetchEntities();
|
||||||
|
setNumChanges(0);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const editEntityItem = useCallback((ei: EntityItem) => {
|
||||||
|
setCreating(false);
|
||||||
|
setSelectedEntityItem(ei);
|
||||||
|
setDialogOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onDialogClose = () => {
|
||||||
|
setDialogOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDialogCancel = async () => {
|
||||||
|
await fetchEntities().then(() => {
|
||||||
|
setNumChanges(0);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDialogSave = (updatedItem: EntityItem) => {
|
||||||
|
setDialogOpen(false);
|
||||||
|
void updateState(readCustomEntities(), (data: EntityItem[]) => {
|
||||||
|
const new_data = creating
|
||||||
|
? [
|
||||||
|
...data.filter((ei) => creating || ei.o_id !== updatedItem.o_id),
|
||||||
|
updatedItem
|
||||||
|
]
|
||||||
|
: data.map((ei) =>
|
||||||
|
ei.id === updatedItem.id ? { ...ei, ...updatedItem } : ei
|
||||||
|
);
|
||||||
|
setNumChanges(new_data.filter((ei) => hasEntityChanged(ei)).length);
|
||||||
|
return new_data;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const addEntityItem = () => {
|
||||||
|
setCreating(true);
|
||||||
|
setSelectedEntityItem({
|
||||||
|
id: Math.floor(Math.random() * (Math.floor(200) - 100) + 100),
|
||||||
|
name: '',
|
||||||
|
ram: 0,
|
||||||
|
device_id: '0',
|
||||||
|
type_id: '0',
|
||||||
|
offset: 0,
|
||||||
|
factor: 1,
|
||||||
|
uom: 0,
|
||||||
|
value_type: 0,
|
||||||
|
writeable: false,
|
||||||
|
deleted: false,
|
||||||
|
value: ''
|
||||||
|
});
|
||||||
|
setDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
function formatValue(value: unknown, uom: number) {
|
||||||
|
return value === undefined
|
||||||
|
? ''
|
||||||
|
: typeof value === 'number'
|
||||||
|
? new Intl.NumberFormat().format(value) +
|
||||||
|
(uom === 0 ? '' : ' ' + DeviceValueUOM_s[uom])
|
||||||
|
: (value as string);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showHex(value: number, digit: number) {
|
||||||
|
return '0x' + value.toString(16).toUpperCase().padStart(digit, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderEntity = () => {
|
||||||
|
if (!entities) {
|
||||||
|
return <FormLoader onRetry={fetchEntities} errorMessage={error?.message} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
<HeaderCell>{LL.NAME(0)}</HeaderCell>
|
||||||
|
<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.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}
|
||||||
|
{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>
|
||||||
|
))}
|
||||||
|
</Body>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionContent>
|
||||||
|
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||||
|
<Box mb={2} color="warning.main">
|
||||||
|
<Typography variant="body1">{LL.ENTITIES_HELP_1()}.</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{renderEntity()}
|
||||||
|
|
||||||
|
{selectedEntityItem && (
|
||||||
|
<SettingsCustomEntitiesDialog
|
||||||
|
open={dialogOpen}
|
||||||
|
creating={creating}
|
||||||
|
onClose={onDialogClose}
|
||||||
|
onSave={onDialogSave}
|
||||||
|
selectedItem={selectedEntityItem}
|
||||||
|
validator={entityItemValidation(entities, selectedEntityItem)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box mt={1} display="flex" flexWrap="wrap">
|
||||||
|
<Box flexGrow={1}>
|
||||||
|
{numChanges > 0 && (
|
||||||
|
<ButtonRow>
|
||||||
|
<Button
|
||||||
|
startIcon={<CancelIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={onDialogCancel}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
|
{LL.CANCEL()}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
startIcon={<WarningIcon color="warning" />}
|
||||||
|
variant="contained"
|
||||||
|
color="info"
|
||||||
|
onClick={saveEntities}
|
||||||
|
>
|
||||||
|
{LL.APPLY_CHANGES(numChanges)}
|
||||||
|
</Button>
|
||||||
|
</ButtonRow>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<Box flexWrap="nowrap" whiteSpace="nowrap">
|
||||||
|
<Button
|
||||||
|
startIcon={<AddIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
onClick={addEntityItem}
|
||||||
|
>
|
||||||
|
{LL.ADD(0)}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</SectionContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
753
interface/src/app/main/Customizations.tsx
Normal file
753
interface/src/app/main/Customizations.tsx
Normal file
@@ -0,0 +1,753 @@
|
|||||||
|
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 {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
InputAdornment,
|
||||||
|
Link,
|
||||||
|
MenuItem,
|
||||||
|
TextField,
|
||||||
|
ToggleButton,
|
||||||
|
ToggleButtonGroup,
|
||||||
|
Typography
|
||||||
|
} from '@mui/material';
|
||||||
|
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 { DeviceEntityMask } from './types';
|
||||||
|
import type { APIcall, Device, DeviceEntity } from './types';
|
||||||
|
|
||||||
|
export const APIURL = window.location.origin + '/api/';
|
||||||
|
|
||||||
|
const Customizations = () => {
|
||||||
|
const { LL } = useI18nContext();
|
||||||
|
const [numChanges, setNumChanges] = useState<number>(0);
|
||||||
|
const blocker = useBlocker(numChanges !== 0);
|
||||||
|
|
||||||
|
const [restarting, setRestarting] = useState<boolean>(false);
|
||||||
|
const [restartNeeded, setRestartNeeded] = useState<boolean>(false);
|
||||||
|
const [deviceEntities, setDeviceEntities] = useState<DeviceEntity[]>([]);
|
||||||
|
const [confirmReset, setConfirmReset] = useState<boolean>(false);
|
||||||
|
const [selectedFilters, setSelectedFilters] = useState<number>(0);
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
const [selectedDeviceEntity, setSelectedDeviceEntity] = useState<DeviceEntity>();
|
||||||
|
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||||
|
const [rename, setRename] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useLayoutTitle(LL.CUSTOMIZATIONS());
|
||||||
|
|
||||||
|
// 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: sendResetCustomizations } = useRequest(resetCustomizations(), {
|
||||||
|
immediate: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const { send: sendDeviceName } = useRequest(
|
||||||
|
(data: { id: number; name: string }) => writeDeviceName(data),
|
||||||
|
{
|
||||||
|
immediate: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const { send: sendCustomizationEntities } = useRequest(
|
||||||
|
(data: { id: number; entity_ids: string[] }) => writeCustomizationEntities(data),
|
||||||
|
{
|
||||||
|
immediate: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const { send: sendDeviceEntities } = useRequest(
|
||||||
|
(data: number) => readDeviceEntities(data),
|
||||||
|
{
|
||||||
|
initialData: [],
|
||||||
|
immediate: false
|
||||||
|
}
|
||||||
|
).onSuccess((event) => {
|
||||||
|
setOriginalSettings(event.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
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: `
|
||||||
|
--data-table-library_grid-template-columns: 156px repeat(1, minmax(80px, 1fr)) 45px minmax(45px, auto) minmax(120px, auto);
|
||||||
|
`,
|
||||||
|
BaseRow: `
|
||||||
|
font-size: 14px;
|
||||||
|
.td {
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
BaseCell: `
|
||||||
|
&:nth-of-type(3) {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
&:nth-of-type(4) {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
&:last-of-type {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
HeaderRow: `
|
||||||
|
text-transform: uppercase;
|
||||||
|
background-color: black;
|
||||||
|
color: #90CAF9;
|
||||||
|
.th {
|
||||||
|
border-bottom: 1px solid #565656;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
&:nth-of-type(1) .th {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
Row: `
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
.td {
|
||||||
|
border-top: 1px solid #565656;
|
||||||
|
border-bottom: 1px solid #565656;
|
||||||
|
}
|
||||||
|
&.tr.tr-body.row-select.row-select-single-selected {
|
||||||
|
background-color: #3d4752;
|
||||||
|
}
|
||||||
|
&:hover .td {
|
||||||
|
border-top: 1px solid #177ac9;
|
||||||
|
background-color: #177ac9;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
Cell: `
|
||||||
|
&:nth-of-type(2) {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
&:nth-of-type(3) {
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
&:nth-of-type(4) {
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
&:last-of-type {
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
});
|
||||||
|
|
||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (deviceEntities.length) {
|
||||||
|
setNumChanges(
|
||||||
|
deviceEntities
|
||||||
|
.filter((de) => hasEntityChanged(de))
|
||||||
|
.map(
|
||||||
|
(new_de) =>
|
||||||
|
new_de.m.toString(16).padStart(2, '0') +
|
||||||
|
new_de.id +
|
||||||
|
(new_de.cn || new_de.mi || new_de.ma ? '|' : '') +
|
||||||
|
(new_de.cn ? new_de.cn : '') +
|
||||||
|
(new_de.mi ? '>' + new_de.mi : '') +
|
||||||
|
(new_de.ma ? '<' + new_de.ma : '')
|
||||||
|
).length
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [deviceEntities]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (devices && selectedDevice !== -1) {
|
||||||
|
void sendDeviceEntities(selectedDevice);
|
||||||
|
const index = devices.devices.findIndex((d) => d.id === selectedDevice);
|
||||||
|
if (index === -1) {
|
||||||
|
setSelectedDevice(-1);
|
||||||
|
setSelectedDeviceTypeNameURL('');
|
||||||
|
} else {
|
||||||
|
setSelectedDeviceTypeNameURL(devices.devices[index].url || '');
|
||||||
|
setSelectedDeviceName(devices.devices[index].n);
|
||||||
|
setNumChanges(0);
|
||||||
|
setRestartNeeded(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [devices, selectedDevice]);
|
||||||
|
|
||||||
|
function formatValue(value: unknown) {
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
return new Intl.NumberFormat().format(value);
|
||||||
|
} else if (value === undefined) {
|
||||||
|
return '';
|
||||||
|
} else if (typeof value === 'boolean') {
|
||||||
|
return value ? 'true' : 'false';
|
||||||
|
}
|
||||||
|
return value as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatName = (de: DeviceEntity, withShortname: boolean) =>
|
||||||
|
(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;
|
||||||
|
for (const entry of newMask) {
|
||||||
|
new_mask |= Number(entry);
|
||||||
|
}
|
||||||
|
return new_mask;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMaskString = (m: number) => {
|
||||||
|
const new_masks: string[] = [];
|
||||||
|
if ((m & 1) === 1) {
|
||||||
|
new_masks.push('1');
|
||||||
|
}
|
||||||
|
if ((m & 2) === 2) {
|
||||||
|
new_masks.push('2');
|
||||||
|
}
|
||||||
|
if ((m & 4) === 4) {
|
||||||
|
new_masks.push('4');
|
||||||
|
}
|
||||||
|
if ((m & 8) === 8) {
|
||||||
|
new_masks.push('8');
|
||||||
|
}
|
||||||
|
if ((m & 128) === 128) {
|
||||||
|
new_masks.push('128');
|
||||||
|
}
|
||||||
|
return new_masks;
|
||||||
|
};
|
||||||
|
|
||||||
|
const filter_entity = (de: DeviceEntity) =>
|
||||||
|
(de.m & selectedFilters || !selectedFilters) &&
|
||||||
|
formatName(de, true).includes(search);
|
||||||
|
|
||||||
|
const maskDisabled = (set: boolean) => {
|
||||||
|
setDeviceEntities(
|
||||||
|
deviceEntities.map(function (de) {
|
||||||
|
if (filter_entity(de)) {
|
||||||
|
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
|
||||||
|
)
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return de;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetCustomization = async () => {
|
||||||
|
try {
|
||||||
|
await sendResetCustomizations();
|
||||||
|
toast.info(LL.CUSTOMIZATIONS_RESTART());
|
||||||
|
} catch (error) {
|
||||||
|
toast.error((error as Error).message);
|
||||||
|
} finally {
|
||||||
|
setConfirmReset(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDialogClose = () => {
|
||||||
|
setDialogOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateDeviceEntity = (updatedItem: DeviceEntity) => {
|
||||||
|
setDeviceEntities(
|
||||||
|
deviceEntities?.map((de) =>
|
||||||
|
de.id === updatedItem.id ? { ...de, ...updatedItem } : de
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDialogSave = (updatedItem: DeviceEntity) => {
|
||||||
|
setDialogOpen(false);
|
||||||
|
updateDeviceEntity(updatedItem);
|
||||||
|
};
|
||||||
|
|
||||||
|
const editDeviceEntity = useCallback((de: DeviceEntity) => {
|
||||||
|
if (de.n === undefined || (de.n && de.n[0] === '!')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (de.cn === undefined) {
|
||||||
|
de.cn = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedDeviceEntity(de);
|
||||||
|
setDialogOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const saveCustomization = async () => {
|
||||||
|
if (devices && deviceEntities && selectedDevice !== -1) {
|
||||||
|
const masked_entities = deviceEntities
|
||||||
|
.filter((de: DeviceEntity) => hasEntityChanged(de))
|
||||||
|
.map(
|
||||||
|
(new_de) =>
|
||||||
|
new_de.m.toString(16).padStart(2, '0') +
|
||||||
|
new_de.id +
|
||||||
|
(new_de.cn || new_de.mi || new_de.ma ? '|' : '') +
|
||||||
|
(new_de.cn ? new_de.cn : '') +
|
||||||
|
(new_de.mi ? '>' + new_de.mi : '') +
|
||||||
|
(new_de.ma ? '<' + new_de.ma : '')
|
||||||
|
);
|
||||||
|
|
||||||
|
// check size in bytes to match buffer in CPP, which is 2048
|
||||||
|
const bytes = new TextEncoder().encode(JSON.stringify(masked_entities)).length;
|
||||||
|
if (bytes > 2000) {
|
||||||
|
toast.warning(LL.CUSTOMIZATIONS_FULL());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendCustomizationEntities({
|
||||||
|
id: selectedDevice,
|
||||||
|
entity_ids: masked_entities
|
||||||
|
}).catch((error: Error) => {
|
||||||
|
if (error.message === 'Reboot required') {
|
||||||
|
setRestartNeeded(true);
|
||||||
|
} else {
|
||||||
|
toast.error(error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setOriginalSettings(deviceEntities);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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="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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderDeviceData = () => {
|
||||||
|
const shown_data = deviceEntities.filter((de) => filter_entity(de));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<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="deleted" isSet={true} />={LL.CUSTOMIZATIONS_HELP_6()}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
mb={1}
|
||||||
|
mt={0}
|
||||||
|
spacing={2}
|
||||||
|
direction="row"
|
||||||
|
justifyContent="flex-start"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Grid>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
placeholder={LL.SEARCH()}
|
||||||
|
onChange={(event) => {
|
||||||
|
setSearch(event.target.value);
|
||||||
|
}}
|
||||||
|
slotProps={{
|
||||||
|
input: {
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">
|
||||||
|
<SearchIcon color="primary" sx={{ fontSize: 16 }} />
|
||||||
|
</InputAdornment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<ToggleButtonGroup
|
||||||
|
size="small"
|
||||||
|
color="secondary"
|
||||||
|
value={getMaskString(selectedFilters)}
|
||||||
|
onChange={(event, mask: string[]) => {
|
||||||
|
setSelectedFilters(getMaskNumber(mask));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ToggleButton value="8">
|
||||||
|
<OptionIcon type="favorite" isSet={true} />
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton value="4">
|
||||||
|
<OptionIcon type="readonly" isSet={true} />
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton value="2">
|
||||||
|
<OptionIcon type="api_mqtt_exclude" isSet={true} />
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton value="1">
|
||||||
|
<OptionIcon type="web_exclude" isSet={true} />
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton value="128">
|
||||||
|
<OptionIcon type="deleted" isSet={true} />
|
||||||
|
</ToggleButton>
|
||||||
|
</ToggleButtonGroup>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
sx={{ fontSize: 10 }}
|
||||||
|
variant="outlined"
|
||||||
|
color="inherit"
|
||||||
|
onClick={() => maskDisabled(false)}
|
||||||
|
>
|
||||||
|
{LL.SET_ALL()}
|
||||||
|
<OptionIcon type="api_mqtt_exclude" isSet={false} />
|
||||||
|
<OptionIcon type="web_exclude" isSet={false} />
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
sx={{ fontSize: 10 }}
|
||||||
|
variant="outlined"
|
||||||
|
color="inherit"
|
||||||
|
onClick={() => maskDisabled(true)}
|
||||||
|
>
|
||||||
|
{LL.SET_ALL()}
|
||||||
|
<OptionIcon type="api_mqtt_exclude" isSet={true} />
|
||||||
|
<OptionIcon type="web_exclude" isSet={true} />
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<Typography variant="subtitle2" color="primary">
|
||||||
|
{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: DeviceEntity[]) => (
|
||||||
|
<>
|
||||||
|
<Header>
|
||||||
|
<HeaderRow>
|
||||||
|
<HeaderCell stiff>{LL.OPTIONS()}</HeaderCell>
|
||||||
|
<HeaderCell resize>{LL.NAME(1)}</HeaderCell>
|
||||||
|
<HeaderCell stiff>{LL.MIN()}</HeaderCell>
|
||||||
|
<HeaderCell stiff>{LL.MAX()}</HeaderCell>
|
||||||
|
<HeaderCell resize>{LL.VALUE(0)}</HeaderCell>
|
||||||
|
</HeaderRow>
|
||||||
|
</Header>
|
||||||
|
<Body>
|
||||||
|
{tableList.map((de: DeviceEntity) => (
|
||||||
|
<Row key={de.id} item={de} onClick={() => editDeviceEntity(de)}>
|
||||||
|
<Cell stiff>
|
||||||
|
<EntityMaskToggle onUpdate={updateDeviceEntity} de={de} />
|
||||||
|
</Cell>
|
||||||
|
<Cell>
|
||||||
|
{formatName(de, false)} (
|
||||||
|
<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>{formatValue(de.v)}</Cell>
|
||||||
|
</Row>
|
||||||
|
))}
|
||||||
|
</Body>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Table>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderResetDialog = () => (
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{LL.CANCEL()}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
startIcon={<SettingsBackupRestoreIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={resetCustomization}
|
||||||
|
color="error"
|
||||||
|
>
|
||||||
|
{LL.RESET(0)}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderContent = () => (
|
||||||
|
<>
|
||||||
|
{devices && renderDeviceList()}
|
||||||
|
{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>
|
||||||
|
) : (
|
||||||
|
<Box display="flex" flexWrap="wrap">
|
||||||
|
<Box flexGrow={1}>
|
||||||
|
{numChanges !== 0 && (
|
||||||
|
<ButtonRow>
|
||||||
|
<Button
|
||||||
|
startIcon={<CancelIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
onClick={() => devices && sendDeviceEntities(selectedDevice)}
|
||||||
|
>
|
||||||
|
{LL.CANCEL()}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
startIcon={<WarningIcon color="warning" />}
|
||||||
|
variant="contained"
|
||||||
|
color="info"
|
||||||
|
onClick={saveCustomization}
|
||||||
|
>
|
||||||
|
{LL.APPLY_CHANGES(numChanges)}
|
||||||
|
</Button>
|
||||||
|
</ButtonRow>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
{!rename && (
|
||||||
|
<ButtonRow mt={1}>
|
||||||
|
<Button
|
||||||
|
startIcon={<SettingsBackupRestoreIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
color="error"
|
||||||
|
onClick={() => setConfirmReset(true)}
|
||||||
|
>
|
||||||
|
{LL.RESET(0)}
|
||||||
|
</Button>
|
||||||
|
</ButtonRow>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{renderResetDialog()}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionContent>
|
||||||
|
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||||
|
{restarting ? <RestartMonitor /> : renderContent()}
|
||||||
|
{selectedDeviceEntity && (
|
||||||
|
<SettingsCustomizationsDialog
|
||||||
|
open={dialogOpen}
|
||||||
|
onClose={onDialogClose}
|
||||||
|
onSave={onDialogSave}
|
||||||
|
selectedItem={selectedDeviceEntity}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</SectionContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Customizations;
|
||||||
177
interface/src/app/main/CustomizationsDialog.tsx
Normal file
177
interface/src/app/main/CustomizationsDialog.tsx
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
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,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
TextField,
|
||||||
|
Typography
|
||||||
|
} from '@mui/material';
|
||||||
|
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';
|
||||||
|
|
||||||
|
interface SettingsCustomizationsDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSave: (di: DeviceEntity) => void;
|
||||||
|
selectedItem: DeviceEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CustomizationsDialog = ({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
onSave,
|
||||||
|
selectedItem
|
||||||
|
}: SettingsCustomizationsDialogProps) => {
|
||||||
|
const { LL } = useI18nContext();
|
||||||
|
const [editItem, setEditItem] = useState<DeviceEntity>(selectedItem);
|
||||||
|
const [error, setError] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const updateFormValue = updateValue(setEditItem);
|
||||||
|
|
||||||
|
const isWriteableNumber =
|
||||||
|
typeof editItem.v === 'number' &&
|
||||||
|
editItem.w &&
|
||||||
|
!(editItem.m & DeviceEntityMask.DV_READONLY);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
setError(false);
|
||||||
|
setEditItem(selectedItem);
|
||||||
|
}
|
||||||
|
}, [open, selectedItem]);
|
||||||
|
|
||||||
|
const handleClose = (_event, reason: 'backdropClick' | 'escapeKeyDown') => {
|
||||||
|
if (reason !== 'backdropClick') {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const save = () => {
|
||||||
|
if (
|
||||||
|
isWriteableNumber &&
|
||||||
|
editItem.mi &&
|
||||||
|
editItem.ma &&
|
||||||
|
editItem.mi > editItem?.ma
|
||||||
|
) {
|
||||||
|
setError(true);
|
||||||
|
} else {
|
||||||
|
onSave(editItem);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateDeviceEntity = (updatedItem: DeviceEntity) => {
|
||||||
|
setEditItem({ ...editItem, m: updatedItem.m });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog sx={dialogStyle} open={open} onClose={handleClose}>
|
||||||
|
<DialogTitle>{LL.EDIT() + ' ' + LL.ENTITY()}</DialogTitle>
|
||||||
|
<DialogContent dividers>
|
||||||
|
<Grid container>
|
||||||
|
<Typography variant="body2" color="warning.main">
|
||||||
|
{LL.ID_OF(LL.ENTITY())}:
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2">{editItem.id}</Typography>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid container direction="row">
|
||||||
|
<Typography variant="body2" color="warning.main">
|
||||||
|
{LL.DEFAULT(1) + ' ' + LL.ENTITY_NAME(1)}:
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2">{editItem.n}</Typography>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid container direction="row">
|
||||||
|
<Typography variant="body2" color="warning.main">
|
||||||
|
{LL.WRITEABLE()}:
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2">
|
||||||
|
{editItem.w ? (
|
||||||
|
<DoneIcon color="success" sx={{ fontSize: 16 }} />
|
||||||
|
) : (
|
||||||
|
<CloseIcon color="error" sx={{ fontSize: 16 }} />
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Box mt={1} mb={2}>
|
||||||
|
<EntityMaskToggle onUpdate={updateDeviceEntity} de={editItem} />
|
||||||
|
</Box>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid>
|
||||||
|
<TextField
|
||||||
|
name="cn"
|
||||||
|
label={LL.NEW_NAME_OF(LL.ENTITY())}
|
||||||
|
value={editItem.cn}
|
||||||
|
sx={{ width: '30ch' }}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
{isWriteableNumber && (
|
||||||
|
<>
|
||||||
|
<Grid>
|
||||||
|
<TextField
|
||||||
|
name="mi"
|
||||||
|
label={LL.MIN()}
|
||||||
|
value={numberValue(editItem.mi)}
|
||||||
|
sx={{ width: '11ch' }}
|
||||||
|
type="number"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<TextField
|
||||||
|
name="ma"
|
||||||
|
label={LL.MAX()}
|
||||||
|
value={numberValue(editItem.ma)}
|
||||||
|
sx={{ width: '11ch' }}
|
||||||
|
type="number"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
{error && (
|
||||||
|
<Typography variant="body2" color="error" mt={2}>
|
||||||
|
Error: Check min and max values
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button
|
||||||
|
startIcon={<CancelIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={onClose}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
|
{LL.CANCEL()}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
startIcon={<DoneIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={save}
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
{LL.UPDATE()}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
229
interface/src/app/main/DevicesDialog.tsx
Normal file
229
interface/src/app/main/DevicesDialog.tsx
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
|
import WarningIcon from '@mui/icons-material/Warning';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
CircularProgress,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
FormHelperText,
|
||||||
|
InputAdornment,
|
||||||
|
MenuItem,
|
||||||
|
TextField,
|
||||||
|
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 { 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';
|
||||||
|
|
||||||
|
interface DevicesDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSave: (as: DeviceValue) => void;
|
||||||
|
selectedItem: DeviceValue;
|
||||||
|
writeable: boolean;
|
||||||
|
validator: Schema;
|
||||||
|
progress: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DevicesDialog = ({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
onSave,
|
||||||
|
selectedItem,
|
||||||
|
writeable,
|
||||||
|
validator,
|
||||||
|
progress
|
||||||
|
}: DevicesDialogProps) => {
|
||||||
|
const { LL } = useI18nContext();
|
||||||
|
const [editItem, setEditItem] = useState<DeviceValue>(selectedItem);
|
||||||
|
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
|
|
||||||
|
const updateFormValue = updateValue(setEditItem);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
setFieldErrors(undefined);
|
||||||
|
setEditItem(selectedItem);
|
||||||
|
}
|
||||||
|
}, [open, selectedItem]);
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const save = async () => {
|
||||||
|
try {
|
||||||
|
setFieldErrors(undefined);
|
||||||
|
await validate(validator, editItem);
|
||||||
|
onSave(editItem);
|
||||||
|
} catch (error) {
|
||||||
|
setFieldErrors(error as ValidateFieldsError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setUom = (uom?: DeviceValueUOM) => {
|
||||||
|
if (uom === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (uom) {
|
||||||
|
case DeviceValueUOM.HOURS:
|
||||||
|
return LL.HOURS();
|
||||||
|
case DeviceValueUOM.MINUTES:
|
||||||
|
return LL.MINUTES();
|
||||||
|
case DeviceValueUOM.SECONDS:
|
||||||
|
return LL.SECONDS();
|
||||||
|
default:
|
||||||
|
return DeviceValueUOM_s[uom];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const showHelperText = (dv: DeviceValue) =>
|
||||||
|
dv.h ? (
|
||||||
|
dv.h
|
||||||
|
) : dv.l ? (
|
||||||
|
dv.l.join(' | ')
|
||||||
|
) : dv.m !== undefined && dv.x !== undefined ? (
|
||||||
|
<>
|
||||||
|
{dv.m} → {dv.x}
|
||||||
|
</>
|
||||||
|
) : undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog sx={dialogStyle} open={open} onClose={close}>
|
||||||
|
<DialogTitle>
|
||||||
|
{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 container>
|
||||||
|
<Grid size={12}>
|
||||||
|
{editItem.l ? (
|
||||||
|
<TextField
|
||||||
|
name="v"
|
||||||
|
label={LL.VALUE(0)}
|
||||||
|
value={editItem.v}
|
||||||
|
disabled={!writeable}
|
||||||
|
sx={{ width: '30ch' }}
|
||||||
|
select
|
||||||
|
onChange={updateFormValue}
|
||||||
|
>
|
||||||
|
{editItem.l.map((val) => (
|
||||||
|
<MenuItem value={val} key={val}>
|
||||||
|
{val}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
) : editItem.s || editItem.u !== DeviceValueUOM.NONE ? (
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
|
name="v"
|
||||||
|
label={LL.VALUE(0)}
|
||||||
|
value={numberValue(Math.round((editItem.v as number) * 10) / 10)}
|
||||||
|
autoFocus
|
||||||
|
disabled={!writeable}
|
||||||
|
type="number"
|
||||||
|
sx={{ width: '30ch' }}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
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(0)}
|
||||||
|
value={editItem.v}
|
||||||
|
disabled={!writeable}
|
||||||
|
sx={{ width: '30ch' }}
|
||||||
|
multiline={!editItem.u}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
{writeable && (
|
||||||
|
<Grid>
|
||||||
|
<FormHelperText>{showHelperText(editItem)}</FormHelperText>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</DialogContent>
|
||||||
|
|
||||||
|
<DialogActions>
|
||||||
|
{writeable ? (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
'& button, & a, & .MuiCard-root': {
|
||||||
|
mx: 0.6
|
||||||
|
},
|
||||||
|
position: 'relative'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
startIcon={<CancelIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={close}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
|
{LL.CANCEL()}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
startIcon={<WarningIcon color="warning" />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={save}
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
{selectedItem.v === '' && selectedItem.c ? LL.EXECUTE() : LL.UPDATE()}
|
||||||
|
</Button>
|
||||||
|
{progress && (
|
||||||
|
<CircularProgress
|
||||||
|
size={24}
|
||||||
|
sx={{
|
||||||
|
color: '#4caf50',
|
||||||
|
position: 'absolute',
|
||||||
|
right: '20%',
|
||||||
|
marginTop: '6px'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Button variant="outlined" onClick={close} color="secondary">
|
||||||
|
{LL.CLOSE()}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DevicesDialog;
|
||||||
103
interface/src/app/main/EntityMaskToggle.tsx
Normal file
103
interface/src/app/main/EntityMaskToggle.tsx
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import { ToggleButton, ToggleButtonGroup } from '@mui/material';
|
||||||
|
|
||||||
|
import OptionIcon from './OptionIcon';
|
||||||
|
import { DeviceEntityMask } from './types';
|
||||||
|
import type { DeviceEntity } from './types';
|
||||||
|
|
||||||
|
interface EntityMaskToggleProps {
|
||||||
|
onUpdate: (de: DeviceEntity) => void;
|
||||||
|
de: DeviceEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EntityMaskToggle = ({ onUpdate, de }: EntityMaskToggleProps) => {
|
||||||
|
const getMaskNumber = (newMask: string[]) => {
|
||||||
|
let new_mask = 0;
|
||||||
|
for (const entry of newMask) {
|
||||||
|
new_mask |= Number(entry);
|
||||||
|
}
|
||||||
|
return new_mask;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMaskString = (m: number) => {
|
||||||
|
const new_masks: string[] = [];
|
||||||
|
if ((m & 1) === 1) {
|
||||||
|
new_masks.push('1');
|
||||||
|
}
|
||||||
|
if ((m & 2) === 2) {
|
||||||
|
new_masks.push('2');
|
||||||
|
}
|
||||||
|
if ((m & 4) === 4) {
|
||||||
|
new_masks.push('4');
|
||||||
|
}
|
||||||
|
if ((m & 8) === 8) {
|
||||||
|
new_masks.push('8');
|
||||||
|
}
|
||||||
|
if ((m & 128) === 128) {
|
||||||
|
new_masks.push('128');
|
||||||
|
}
|
||||||
|
return new_masks;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToggleButtonGroup
|
||||||
|
size="small"
|
||||||
|
color="secondary"
|
||||||
|
value={getMaskString(de.m)}
|
||||||
|
onChange={(event, mask: string[]) => {
|
||||||
|
de.m = getMaskNumber(mask);
|
||||||
|
if (de.n === '' && de.m & DeviceEntityMask.DV_READONLY) {
|
||||||
|
de.m = de.m | DeviceEntityMask.DV_WEB_EXCLUDE;
|
||||||
|
}
|
||||||
|
if (de.m & DeviceEntityMask.DV_WEB_EXCLUDE) {
|
||||||
|
de.m = de.m & ~DeviceEntityMask.DV_FAVORITE;
|
||||||
|
}
|
||||||
|
onUpdate(de);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ToggleButton value="8" disabled={(de.m & 0x81) !== 0 || de.n === undefined}>
|
||||||
|
<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
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</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
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</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
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton value="128">
|
||||||
|
<OptionIcon
|
||||||
|
type="deleted"
|
||||||
|
isSet={
|
||||||
|
(de.m & DeviceEntityMask.DV_DELETED) === DeviceEntityMask.DV_DELETED
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ToggleButton>
|
||||||
|
</ToggleButtonGroup>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EntityMaskToggle;
|
||||||
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;
|
||||||
@@ -1,38 +1,41 @@
|
|||||||
import { FC } from 'react';
|
import CommentsDisabledOutlinedIcon from '@mui/icons-material/CommentsDisabledOutlined';
|
||||||
import { SvgIconProps } from '@mui/material';
|
import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
|
||||||
|
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||||
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
|
|
||||||
import EditOffOutlinedIcon from '@mui/icons-material/EditOffOutlined';
|
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 StarIcon from '@mui/icons-material/Star';
|
||||||
import StarOutlineIcon from '@mui/icons-material/StarOutline';
|
import StarOutlineIcon from '@mui/icons-material/StarOutline';
|
||||||
|
|
||||||
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined';
|
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined';
|
||||||
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
|
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
|
||||||
|
import type { SvgIconProps } from '@mui/material';
|
||||||
|
|
||||||
import CommentsDisabledOutlinedIcon from '@mui/icons-material/CommentsDisabledOutlined';
|
type OptionType =
|
||||||
import InsertCommentOutlinedIcon from '@mui/icons-material/InsertCommentOutlined';
|
| 'deleted'
|
||||||
|
| 'readonly'
|
||||||
|
| 'web_exclude'
|
||||||
|
| 'api_mqtt_exclude'
|
||||||
|
| 'favorite';
|
||||||
|
|
||||||
type OptionType = 'readonly' | 'web_exclude' | 'api_mqtt_exclude' | 'favorite';
|
const OPTION_ICONS: {
|
||||||
|
[type in OptionType]: [
|
||||||
const OPTION_ICONS: { [type in OptionType]: [React.ComponentType<SvgIconProps>, React.ComponentType<SvgIconProps>] } = {
|
React.ComponentType<SvgIconProps>,
|
||||||
|
React.ComponentType<SvgIconProps>
|
||||||
|
];
|
||||||
|
} = {
|
||||||
|
deleted: [DeleteForeverIcon, DeleteOutlineIcon],
|
||||||
readonly: [EditOffOutlinedIcon, EditOutlinedIcon],
|
readonly: [EditOffOutlinedIcon, EditOutlinedIcon],
|
||||||
web_exclude: [VisibilityOffOutlinedIcon, VisibilityOutlinedIcon],
|
web_exclude: [VisibilityOffOutlinedIcon, VisibilityOutlinedIcon],
|
||||||
api_mqtt_exclude: [CommentsDisabledOutlinedIcon, InsertCommentOutlinedIcon],
|
api_mqtt_exclude: [CommentsDisabledOutlinedIcon, InsertCommentOutlinedIcon],
|
||||||
favorite: [StarIcon, StarOutlineIcon]
|
favorite: [StarIcon, StarOutlineIcon]
|
||||||
};
|
};
|
||||||
|
|
||||||
interface OptionIconProps {
|
const OptionIcon = ({ type, isSet }: { type: OptionType; isSet: boolean }) => {
|
||||||
type: OptionType;
|
|
||||||
isSet: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const OptionIcon: FC<OptionIconProps> = ({ type, isSet }) => {
|
|
||||||
const Icon = OPTION_ICONS[type][isSet ? 0 : 1];
|
const Icon = OPTION_ICONS[type][isSet ? 0 : 1];
|
||||||
return isSet ? (
|
return isSet ? (
|
||||||
<Icon color="primary" sx={{ fontSize: 14, verticalAlign: 'middle' }} />
|
<Icon color="primary" sx={{ fontSize: 16, verticalAlign: 'middle' }} />
|
||||||
) : (
|
) : (
|
||||||
<Icon sx={{ fontSize: 14, verticalAlign: 'middle' }} />
|
<Icon sx={{ fontSize: 16, verticalAlign: 'middle' }} />
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
372
interface/src/app/main/Scheduler.tsx
Normal file
372
interface/src/app/main/Scheduler.tsx
Normal file
@@ -0,0 +1,372 @@
|
|||||||
|
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 {
|
||||||
|
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 { 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);
|
||||||
|
const [selectedScheduleItem, setSelectedScheduleItem] = useState<ScheduleItem>();
|
||||||
|
const [dow, setDow] = useState<string[]>([]);
|
||||||
|
const [creating, setCreating] = useState<boolean>(false);
|
||||||
|
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useLayoutTitle(LL.SCHEDULER());
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: schedule,
|
||||||
|
send: fetchSchedule,
|
||||||
|
error
|
||||||
|
} = useRequest(readSchedule, {
|
||||||
|
initialData: []
|
||||||
|
});
|
||||||
|
|
||||||
|
const { send: updateSchedule } = useRequest(
|
||||||
|
(data: Schedule) => writeSchedule(data),
|
||||||
|
{
|
||||||
|
immediate: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function hasScheduleChanged(si: ScheduleItem) {
|
||||||
|
return (
|
||||||
|
si.id !== si.o_id ||
|
||||||
|
(si.name || '') !== (si.o_name || '') ||
|
||||||
|
si.active !== si.o_active ||
|
||||||
|
si.deleted !== si.o_deleted ||
|
||||||
|
si.flags !== si.o_flags ||
|
||||||
|
si.time !== si.o_time ||
|
||||||
|
si.cmd !== si.o_cmd ||
|
||||||
|
si.value !== si.o_value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
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`);
|
||||||
|
});
|
||||||
|
setDow(days.map((date) => formatter.format(date)));
|
||||||
|
}, [locale]);
|
||||||
|
|
||||||
|
const schedule_theme = useTheme({
|
||||||
|
Table: `
|
||||||
|
--data-table-library_grid-template-columns: 36px 210px 100px 192px repeat(1, minmax(100px, 1fr)) 160px;
|
||||||
|
`,
|
||||||
|
BaseRow: `
|
||||||
|
font-size: 14px;
|
||||||
|
.td {
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
BaseCell: `
|
||||||
|
&:nth-of-type(2) {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
&: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-bottom: 1px solid #565656;
|
||||||
|
}
|
||||||
|
&:hover .td {
|
||||||
|
background-color: #177ac9;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
});
|
||||||
|
|
||||||
|
const saveSchedule = async () => {
|
||||||
|
await updateSchedule({
|
||||||
|
schedule: schedule
|
||||||
|
.filter((si) => !si.deleted)
|
||||||
|
.map((condensed_si) => ({
|
||||||
|
id: condensed_si.id,
|
||||||
|
active: condensed_si.active,
|
||||||
|
flags: condensed_si.flags,
|
||||||
|
time: condensed_si.time,
|
||||||
|
cmd: condensed_si.cmd,
|
||||||
|
value: condensed_si.value,
|
||||||
|
name: condensed_si.name
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success(LL.SCHEDULE_UPDATED());
|
||||||
|
})
|
||||||
|
.catch((error: Error) => {
|
||||||
|
toast.error(error.message);
|
||||||
|
})
|
||||||
|
.finally(async () => {
|
||||||
|
await fetchSchedule();
|
||||||
|
setNumChanges(0);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const editScheduleItem = useCallback((si: ScheduleItem) => {
|
||||||
|
setCreating(false);
|
||||||
|
setSelectedScheduleItem(si);
|
||||||
|
setDialogOpen(true);
|
||||||
|
if (si.o_name === undefined) {
|
||||||
|
si.o_name = si.name;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onDialogClose = () => {
|
||||||
|
setDialogOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDialogCancel = async () => {
|
||||||
|
await fetchSchedule().then(() => {
|
||||||
|
setNumChanges(0);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDialogSave = (updatedItem: ScheduleItem) => {
|
||||||
|
setDialogOpen(false);
|
||||||
|
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
|
||||||
|
);
|
||||||
|
|
||||||
|
setNumChanges(new_data.filter((si) => hasScheduleChanged(si)).length);
|
||||||
|
|
||||||
|
return new_data;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const addScheduleItem = () => {
|
||||||
|
setCreating(true);
|
||||||
|
setSelectedScheduleItem({
|
||||||
|
id: Math.floor(Math.random() * (Math.floor(200) - 100) + 100),
|
||||||
|
active: false,
|
||||||
|
deleted: false,
|
||||||
|
flags: ScheduleFlag.SCHEDULE_DAY,
|
||||||
|
time: '',
|
||||||
|
cmd: '',
|
||||||
|
value: '',
|
||||||
|
name: ''
|
||||||
|
});
|
||||||
|
setDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderSchedule = () => {
|
||||||
|
if (!schedule) {
|
||||||
|
return <FormLoader onRetry={fetchSchedule} errorMessage={error?.message} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dayBox = (si: ScheduleItem, flag: number) => (
|
||||||
|
<>
|
||||||
|
<Box>
|
||||||
|
<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.flags - b.flags)
|
||||||
|
}}
|
||||||
|
theme={schedule_theme}
|
||||||
|
layout={{ custom: true }}
|
||||||
|
>
|
||||||
|
{(tableList: ScheduleItem[]) => (
|
||||||
|
<>
|
||||||
|
<Header>
|
||||||
|
<HeaderRow>
|
||||||
|
<HeaderCell />
|
||||||
|
<HeaderCell stiff>{LL.SCHEDULE(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>
|
||||||
|
</HeaderRow>
|
||||||
|
</Header>
|
||||||
|
<Body>
|
||||||
|
{tableList.map((si: ScheduleItem) => (
|
||||||
|
<Row key={si.id} item={si} onClick={() => editScheduleItem(si)}>
|
||||||
|
<Cell stiff>
|
||||||
|
{si.active ? (
|
||||||
|
<CircleIcon
|
||||||
|
color="success"
|
||||||
|
sx={{ fontSize: 16, verticalAlign: 'middle' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<CircleIcon
|
||||||
|
color="error"
|
||||||
|
sx={{ fontSize: 16, verticalAlign: 'middle' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Cell>
|
||||||
|
<Cell stiff>
|
||||||
|
<Stack spacing={0.5} direction="row">
|
||||||
|
<Divider orientation="vertical" flexItem />
|
||||||
|
{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>
|
||||||
|
<Cell>{si.cmd}</Cell>
|
||||||
|
<Cell>{si.value}</Cell>
|
||||||
|
<Cell>{si.name}</Cell>
|
||||||
|
</Row>
|
||||||
|
))}
|
||||||
|
</Body>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionContent>
|
||||||
|
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||||
|
<Box mb={2} color="warning.main">
|
||||||
|
<Typography variant="body1">{LL.SCHEDULER_HELP_1()}.</Typography>
|
||||||
|
</Box>
|
||||||
|
{renderSchedule()}
|
||||||
|
|
||||||
|
{selectedScheduleItem && (
|
||||||
|
<SettingsSchedulerDialog
|
||||||
|
open={dialogOpen}
|
||||||
|
creating={creating}
|
||||||
|
onClose={onDialogClose}
|
||||||
|
onSave={onDialogSave}
|
||||||
|
selectedItem={selectedScheduleItem}
|
||||||
|
validator={schedulerItemValidation(schedule, selectedScheduleItem)}
|
||||||
|
dow={dow}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box mt={1} display="flex" flexWrap="wrap">
|
||||||
|
<Box flexGrow={1}>
|
||||||
|
{numChanges !== 0 && (
|
||||||
|
<ButtonRow>
|
||||||
|
<Button
|
||||||
|
startIcon={<CancelIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={onDialogCancel}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
|
{LL.CANCEL()}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
startIcon={<WarningIcon color="warning" />}
|
||||||
|
variant="contained"
|
||||||
|
color="info"
|
||||||
|
onClick={saveSchedule}
|
||||||
|
>
|
||||||
|
{LL.APPLY_CHANGES(numChanges)}
|
||||||
|
</Button>
|
||||||
|
</ButtonRow>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<Box flexWrap="nowrap" whiteSpace="nowrap">
|
||||||
|
<ButtonRow>
|
||||||
|
<Button
|
||||||
|
startIcon={<AddIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
onClick={addScheduleItem}
|
||||||
|
>
|
||||||
|
{LL.ADD(0)}
|
||||||
|
</Button>
|
||||||
|
</ButtonRow>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</SectionContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
507
interface/src/app/main/Sensors.tsx
Normal file
507
interface/src/app/main/Sensors.tsx
Normal file
@@ -0,0 +1,507 @@
|
|||||||
|
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 UnfoldMoreOutlinedIcon from '@mui/icons-material/UnfoldMoreOutlined';
|
||||||
|
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 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';
|
||||||
|
|
||||||
|
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 [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(
|
||||||
|
() => readSensorData(),
|
||||||
|
{
|
||||||
|
initialData: {
|
||||||
|
ts: [],
|
||||||
|
as: [],
|
||||||
|
analog_enabled: false,
|
||||||
|
platform: 'ESP32'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const { send: sendTemperatureSensor } = useRequest(
|
||||||
|
(data: WriteTemperatureSensor) => writeTemperatureSensor(data),
|
||||||
|
{
|
||||||
|
immediate: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const { send: sendAnalogSensor } = useRequest(
|
||||||
|
(data: WriteAnalogSensor) => writeAnalogSensor(data),
|
||||||
|
{
|
||||||
|
immediate: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
useInterval(() => {
|
||||||
|
if (!temperatureDialogOpen && !analogDialogOpen) {
|
||||||
|
void fetchSensorData();
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
const common_theme = useTheme({
|
||||||
|
BaseRow: `
|
||||||
|
font-size: 14px;
|
||||||
|
.td {
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
HeaderRow: `
|
||||||
|
text-transform: uppercase;
|
||||||
|
background-color: black;
|
||||||
|
color: #90CAF9;
|
||||||
|
.th {
|
||||||
|
border-bottom: 1px solid #565656;
|
||||||
|
}
|
||||||
|
.th {
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
Row: `
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
.td {
|
||||||
|
padding: 8px;
|
||||||
|
border-bottom: 1px solid #565656;
|
||||||
|
}
|
||||||
|
&:hover .td {
|
||||||
|
background-color: #177ac9;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
Cell: `
|
||||||
|
&:last-of-type {
|
||||||
|
text-align: right;
|
||||||
|
},
|
||||||
|
`
|
||||||
|
});
|
||||||
|
|
||||||
|
const temperature_theme = useTheme([
|
||||||
|
common_theme,
|
||||||
|
{
|
||||||
|
Table: `
|
||||||
|
--data-table-library_grid-template-columns: minmax(0, 1fr) 35%;
|
||||||
|
`
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const analog_theme = useTheme([
|
||||||
|
common_theme,
|
||||||
|
{
|
||||||
|
Table: `
|
||||||
|
--data-table-library_grid-template-columns: 80px repeat(1, minmax(0, 1fr)) 120px 110px;
|
||||||
|
`
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
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 />;
|
||||||
|
}
|
||||||
|
if (state.sortKey === sortKey && !state.reverse) {
|
||||||
|
return <KeyboardArrowUpOutlinedIcon />;
|
||||||
|
}
|
||||||
|
return <UnfoldMoreOutlinedIcon />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const analog_sort = useSort(
|
||||||
|
{ nodes: sensorData.as },
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
sortIcon: {
|
||||||
|
iconDefault: <UnfoldMoreOutlinedIcon />,
|
||||||
|
iconUp: <KeyboardArrowUpOutlinedIcon />,
|
||||||
|
iconDown: <KeyboardArrowDownOutlinedIcon />
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const temperature_sort = useSort(
|
||||||
|
{ nodes: sensorData.ts },
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
sortIcon: {
|
||||||
|
iconDefault: <UnfoldMoreOutlinedIcon />,
|
||||||
|
iconUp: <KeyboardArrowUpOutlinedIcon />,
|
||||||
|
iconDown: <KeyboardArrowDownOutlinedIcon />
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
useLayoutTitle(LL.SENSORS());
|
||||||
|
|
||||||
|
const formatDurationMin = (duration_min: number) => {
|
||||||
|
const days = Math.trunc((duration_min * 60000) / 86400000);
|
||||||
|
const hours = Math.trunc((duration_min * 60000) / 3600000) % 24;
|
||||||
|
const minutes = Math.trunc((duration_min * 60000) / 60000) % 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 });
|
||||||
|
}
|
||||||
|
return formatted;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 });
|
||||||
|
case DeviceValueUOM.MINUTES:
|
||||||
|
return value ? formatDurationMin(value) : LL.NUM_MINUTES({ num: 0 });
|
||||||
|
case DeviceValueUOM.SECONDS:
|
||||||
|
return LL.NUM_SECONDS({ num: value });
|
||||||
|
case DeviceValueUOM.NONE:
|
||||||
|
return new Intl.NumberFormat().format(value);
|
||||||
|
case DeviceValueUOM.DEGREES:
|
||||||
|
case DeviceValueUOM.DEGREES_R:
|
||||||
|
case DeviceValueUOM.FAHRENHEIT:
|
||||||
|
return (
|
||||||
|
new Intl.NumberFormat(undefined, {
|
||||||
|
minimumFractionDigits: 1
|
||||||
|
}).format(value) +
|
||||||
|
' ' +
|
||||||
|
DeviceValueUOM_s[uom]
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return new Intl.NumberFormat().format(value) + ' ' + DeviceValueUOM_s[uom];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateTemperatureSensor = (ts: TemperatureSensor) => {
|
||||||
|
if (me.admin) {
|
||||||
|
ts.o_n = ts.n;
|
||||||
|
setSelectedTemperatureSensor(ts);
|
||||||
|
setTemperatureDialogOpen(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTemperatureDialogClose = () => {
|
||||||
|
setTemperatureDialogOpen(false);
|
||||||
|
void fetchSensorData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTemperatureDialogSave = async (ts: TemperatureSensor) => {
|
||||||
|
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(() => {
|
||||||
|
setTemperatureDialogOpen(false);
|
||||||
|
setSelectedTemperatureSensor(undefined);
|
||||||
|
void fetchSensorData();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateAnalogSensor = (as: AnalogSensor) => {
|
||||||
|
if (me.admin) {
|
||||||
|
setCreating(false);
|
||||||
|
as.o_n = as.n;
|
||||||
|
setSelectedAnalogSensor(as);
|
||||||
|
setAnalogDialogOpen(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAnalogDialogClose = () => {
|
||||||
|
setAnalogDialogOpen(false);
|
||||||
|
void fetchSensorData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const addAnalogSensor = () => {
|
||||||
|
setCreating(true);
|
||||||
|
setSelectedAnalogSensor({
|
||||||
|
id: Math.floor(Math.random() * (Math.floor(200) - 100) + 100),
|
||||||
|
n: '',
|
||||||
|
g: 21, // default GPIO 21 which is safe for all platforms
|
||||||
|
u: 0,
|
||||||
|
v: 0,
|
||||||
|
o: 0,
|
||||||
|
t: 0,
|
||||||
|
f: 1,
|
||||||
|
d: false,
|
||||||
|
o_n: ''
|
||||||
|
});
|
||||||
|
setAnalogDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAnalogDialogSave = async (as: AnalogSensor) => {
|
||||||
|
await sendAnalogSensor({
|
||||||
|
id: as.id,
|
||||||
|
gpio: as.g,
|
||||||
|
name: as.n,
|
||||||
|
offset: as.o,
|
||||||
|
factor: as.f,
|
||||||
|
uom: as.u,
|
||||||
|
type: as.t,
|
||||||
|
deleted: as.d
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success(LL.UPDATED_OF(LL.ANALOG_SENSOR(2)));
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error(LL.UPDATE_OF(LL.ANALOG_SENSOR(5)) + ' ' + LL.FAILED(1));
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setAnalogDialogOpen(false);
|
||||||
|
setSelectedAnalogSensor(undefined);
|
||||||
|
void fetchSensorData();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const RenderAnalogSensors = () => (
|
||||||
|
<Table
|
||||||
|
data={{ nodes: sensorData.as }}
|
||||||
|
theme={analog_theme}
|
||||||
|
sort={analog_sort}
|
||||||
|
layout={{ custom: true }}
|
||||||
|
>
|
||||||
|
{(tableList: AnalogSensor[]) => (
|
||||||
|
<>
|
||||||
|
<Header>
|
||||||
|
<HeaderRow>
|
||||||
|
<HeaderCell stiff>
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
style={{ fontSize: '14px', justifyContent: 'flex-start' }}
|
||||||
|
endIcon={getSortIcon(analog_sort.state, 'GPIO')}
|
||||||
|
onClick={() => analog_sort.fns.onToggleSort({ sortKey: 'GPIO' })}
|
||||||
|
>
|
||||||
|
GPIO
|
||||||
|
</Button>
|
||||||
|
</HeaderCell>
|
||||||
|
<HeaderCell resize>
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
style={{ fontSize: '14px', justifyContent: 'flex-start' }}
|
||||||
|
endIcon={getSortIcon(analog_sort.state, 'NAME')}
|
||||||
|
onClick={() => analog_sort.fns.onToggleSort({ sortKey: 'NAME' })}
|
||||||
|
>
|
||||||
|
{LL.NAME(0)}
|
||||||
|
</Button>
|
||||||
|
</HeaderCell>
|
||||||
|
<HeaderCell stiff>
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
style={{ fontSize: '14px', justifyContent: 'flex-start' }}
|
||||||
|
endIcon={getSortIcon(analog_sort.state, 'TYPE')}
|
||||||
|
onClick={() => analog_sort.fns.onToggleSort({ sortKey: 'TYPE' })}
|
||||||
|
>
|
||||||
|
{LL.TYPE(0)}
|
||||||
|
</Button>
|
||||||
|
</HeaderCell>
|
||||||
|
<HeaderCell stiff>
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
style={{ fontSize: '14px', justifyContent: 'flex-end' }}
|
||||||
|
endIcon={getSortIcon(analog_sort.state, 'VALUE')}
|
||||||
|
onClick={() => analog_sort.fns.onToggleSort({ sortKey: 'VALUE' })}
|
||||||
|
>
|
||||||
|
{LL.VALUE(0)}
|
||||||
|
</Button>
|
||||||
|
</HeaderCell>
|
||||||
|
</HeaderRow>
|
||||||
|
</Header>
|
||||||
|
<Body>
|
||||||
|
{tableList.map((a: AnalogSensor) => (
|
||||||
|
<Row key={a.id} item={a} onClick={() => updateAnalogSensor(a)}>
|
||||||
|
<Cell stiff>{a.g}</Cell>
|
||||||
|
<Cell>{a.n}</Cell>
|
||||||
|
<Cell stiff>{AnalogTypeNames[a.t]} </Cell>
|
||||||
|
{a.t === AnalogType.DIGITAL_OUT || a.t === AnalogType.DIGITAL_IN ? (
|
||||||
|
<Cell stiff>{a.v ? LL.ON() : LL.OFF()}</Cell>
|
||||||
|
) : (
|
||||||
|
<Cell stiff>{a.t ? formatValue(a.v, a.u) : ''}</Cell>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
))}
|
||||||
|
</Body>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<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
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{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>
|
||||||
|
)}
|
||||||
|
</SectionContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Sensors;
|
||||||
353
interface/src/app/main/SensorsAnalogDialog.tsx
Normal file
353
interface/src/app/main/SensorsAnalogDialog.tsx
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
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 {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
InputAdornment,
|
||||||
|
MenuItem,
|
||||||
|
TextField,
|
||||||
|
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 { 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';
|
||||||
|
|
||||||
|
interface DashboardSensorsAnalogDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSave: (as: AnalogSensor) => void;
|
||||||
|
creating: boolean;
|
||||||
|
selectedItem: AnalogSensor;
|
||||||
|
validator: Schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SensorsAnalogDialog = ({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
onSave,
|
||||||
|
creating,
|
||||||
|
selectedItem,
|
||||||
|
validator
|
||||||
|
}: DashboardSensorsAnalogDialogProps) => {
|
||||||
|
const { LL } = useI18nContext();
|
||||||
|
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
|
const [editItem, setEditItem] = useState<AnalogSensor>(selectedItem);
|
||||||
|
const updateFormValue = updateValue(setEditItem);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
setFieldErrors(undefined);
|
||||||
|
setEditItem(selectedItem);
|
||||||
|
}
|
||||||
|
}, [open, selectedItem]);
|
||||||
|
|
||||||
|
const handleClose = (_event, reason: 'backdropClick' | 'escapeKeyDown') => {
|
||||||
|
if (reason !== 'backdropClick') {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const save = async () => {
|
||||||
|
try {
|
||||||
|
setFieldErrors(undefined);
|
||||||
|
await validate(validator, editItem);
|
||||||
|
onSave(editItem);
|
||||||
|
} catch (error) {
|
||||||
|
setFieldErrors(error as ValidateFieldsError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const remove = () => {
|
||||||
|
editItem.d = true;
|
||||||
|
onSave(editItem);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog sx={dialogStyle} open={open} onClose={handleClose}>
|
||||||
|
<DialogTitle>
|
||||||
|
{creating ? LL.ADD(1) + ' ' + LL.NEW(0) : LL.EDIT()}
|
||||||
|
{LL.ANALOG_SENSOR(0)}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent dividers>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid>
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
|
name="g"
|
||||||
|
label="GPIO"
|
||||||
|
sx={{ width: '11ch' }}
|
||||||
|
value={numberValue(editItem.g)}
|
||||||
|
type="number"
|
||||||
|
variant="outlined"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
{creating && (
|
||||||
|
<Grid>
|
||||||
|
<Box color="warning.main" mt={2}>
|
||||||
|
<Typography variant="body2">{LL.WARN_GPIO()}</Typography>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
<Grid>
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
|
name="n"
|
||||||
|
label={LL.NAME(0)}
|
||||||
|
value={editItem.n}
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<TextField
|
||||||
|
name="t"
|
||||||
|
label={LL.TYPE(0)}
|
||||||
|
value={editItem.t}
|
||||||
|
fullWidth
|
||||||
|
select
|
||||||
|
onChange={updateFormValue}
|
||||||
|
>
|
||||||
|
{AnalogTypeNames.map((val, i) => (
|
||||||
|
<MenuItem key={val} value={i}>
|
||||||
|
{val}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
</Grid>
|
||||||
|
{editItem.t >= AnalogType.COUNTER && editItem.t <= AnalogType.RATE && (
|
||||||
|
<Grid>
|
||||||
|
<TextField
|
||||||
|
name="u"
|
||||||
|
label={LL.UNIT()}
|
||||||
|
value={editItem.u}
|
||||||
|
sx={{ width: '15ch' }}
|
||||||
|
select
|
||||||
|
onChange={updateFormValue}
|
||||||
|
>
|
||||||
|
{DeviceValueUOM_s.map((val, i) => (
|
||||||
|
<MenuItem key={val} value={i}>
|
||||||
|
{val}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
{editItem.t === AnalogType.ADC && (
|
||||||
|
<Grid>
|
||||||
|
<TextField
|
||||||
|
name="o"
|
||||||
|
label={LL.OFFSET()}
|
||||||
|
value={numberValue(editItem.o)}
|
||||||
|
type="number"
|
||||||
|
sx={{ width: '11ch' }}
|
||||||
|
variant="outlined"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
slotProps={{
|
||||||
|
input: {
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">mV</InputAdornment>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
htmlInput: { min: '0', max: '3300', step: '1' }
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
{editItem.t === AnalogType.COUNTER && (
|
||||||
|
<Grid>
|
||||||
|
<TextField
|
||||||
|
name="o"
|
||||||
|
label={LL.STARTVALUE()}
|
||||||
|
value={numberValue(editItem.o)}
|
||||||
|
type="number"
|
||||||
|
sx={{ width: '11ch' }}
|
||||||
|
variant="outlined"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
slotProps={{
|
||||||
|
htmlInput: { step: '0.001' }
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
{editItem.t >= AnalogType.COUNTER && editItem.t <= AnalogType.RATE && (
|
||||||
|
<Grid>
|
||||||
|
<TextField
|
||||||
|
name="f"
|
||||||
|
label={LL.FACTOR()}
|
||||||
|
value={numberValue(editItem.f)}
|
||||||
|
sx={{ width: '11ch' }}
|
||||||
|
type="number"
|
||||||
|
variant="outlined"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
slotProps={{
|
||||||
|
htmlInput: { step: '0.001' }
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
{editItem.t === AnalogType.DIGITAL_OUT &&
|
||||||
|
(editItem.g === 25 || editItem.g === 26) && (
|
||||||
|
<Grid>
|
||||||
|
<TextField
|
||||||
|
name="o"
|
||||||
|
label={LL.VALUE(0)}
|
||||||
|
value={numberValue(editItem.o)}
|
||||||
|
sx={{ width: '11ch' }}
|
||||||
|
type="number"
|
||||||
|
variant="outlined"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
slotProps={{
|
||||||
|
htmlInput: { min: '0', max: '255', step: '1' }
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
{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>
|
||||||
|
<TextField
|
||||||
|
name="f"
|
||||||
|
label={LL.FREQ()}
|
||||||
|
value={numberValue(editItem.f)}
|
||||||
|
type="number"
|
||||||
|
variant="outlined"
|
||||||
|
sx={{ width: '11ch' }}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
slotProps={{
|
||||||
|
input: {
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">Hz</InputAdornment>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
htmlInput: { min: '1', max: '5000', step: '1' }
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<TextField
|
||||||
|
name="o"
|
||||||
|
label={LL.DUTY_CYCLE()}
|
||||||
|
value={numberValue(editItem.o)}
|
||||||
|
type="number"
|
||||||
|
sx={{ width: '11ch' }}
|
||||||
|
variant="outlined"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
slotProps={{
|
||||||
|
input: {
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">%</InputAdornment>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
htmlInput: { min: '0', max: '100', step: '0.1' }
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
{!creating && (
|
||||||
|
<Box flexGrow={1} sx={{ '& button': { mt: 0 } }}>
|
||||||
|
<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={<WarningIcon color="warning" />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={save}
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
{creating ? LL.ADD(0) : LL.UPDATE()}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SensorsAnalogDialog;
|
||||||
136
interface/src/app/main/SensorsTemperatureDialog.tsx
Normal file
136
interface/src/app/main/SensorsTemperatureDialog.tsx
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
|
import WarningIcon from '@mui/icons-material/Warning';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
InputAdornment,
|
||||||
|
TextField,
|
||||||
|
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 { ValidatedTextField } from 'components';
|
||||||
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
import { numberValue, updateValue } from 'utils';
|
||||||
|
import { validate } from 'validators';
|
||||||
|
|
||||||
|
import type { TemperatureSensor } from './types';
|
||||||
|
|
||||||
|
interface SensorsTemperatureDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSave: (ts: TemperatureSensor) => void;
|
||||||
|
selectedItem: TemperatureSensor;
|
||||||
|
validator: Schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SensorsTemperatureDialog = ({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
onSave,
|
||||||
|
selectedItem,
|
||||||
|
validator
|
||||||
|
}: SensorsTemperatureDialogProps) => {
|
||||||
|
const { LL } = useI18nContext();
|
||||||
|
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
|
const [editItem, setEditItem] = useState<TemperatureSensor>(selectedItem);
|
||||||
|
const updateFormValue = updateValue(setEditItem);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
setFieldErrors(undefined);
|
||||||
|
setEditItem(selectedItem);
|
||||||
|
}
|
||||||
|
}, [open, selectedItem]);
|
||||||
|
|
||||||
|
const handleClose = (_event, reason: 'backdropClick' | 'escapeKeyDown') => {
|
||||||
|
if (reason !== 'backdropClick') {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const save = async () => {
|
||||||
|
try {
|
||||||
|
setFieldErrors(undefined);
|
||||||
|
await validate(validator, editItem);
|
||||||
|
onSave(editItem);
|
||||||
|
} catch (error) {
|
||||||
|
setFieldErrors(error as ValidateFieldsError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog sx={dialogStyle} open={open} onClose={handleClose}>
|
||||||
|
<DialogTitle>
|
||||||
|
{LL.EDIT()} {LL.TEMP_SENSOR()}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent dividers>
|
||||||
|
<Box color="warning.main" p={0} pl={0} pr={0} mt={0} mb={2}>
|
||||||
|
<Typography variant="body2">
|
||||||
|
{LL.ID_OF(LL.SENSOR(0))}: {editItem.id}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid>
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
|
name="n"
|
||||||
|
label={LL.NAME(0)}
|
||||||
|
value={editItem.n}
|
||||||
|
sx={{ width: '30ch' }}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<TextField
|
||||||
|
name="o"
|
||||||
|
label={LL.OFFSET()}
|
||||||
|
value={numberValue(editItem.o)}
|
||||||
|
sx={{ width: '11ch' }}
|
||||||
|
type="number"
|
||||||
|
variant="outlined"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
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={onClose}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
|
{LL.CANCEL()}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
startIcon={<WarningIcon color="warning" />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={save}
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
{LL.UPDATE()}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SensorsTemperatureDialog;
|
||||||
62
interface/src/app/main/deviceValue.ts
Normal file
62
interface/src/app/main/deviceValue.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
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;
|
||||||
|
const minutes = Math.trunc((duration_min * 60000) / 60000) % 60;
|
||||||
|
|
||||||
|
let formatted = '';
|
||||||
|
if (days) {
|
||||||
|
formatted += LL.NUM_DAYS({ num: days });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hours) {
|
||||||
|
if (formatted) formatted += ' ';
|
||||||
|
formatted += LL.NUM_HOURS({ num: hours });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minutes) {
|
||||||
|
if (formatted) formatted += ' ';
|
||||||
|
formatted += LL.NUM_MINUTES({ num: minutes });
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatted;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 });
|
||||||
|
case DeviceValueUOM.MINUTES:
|
||||||
|
return value ? formatDurationMin(LL, value) : LL.NUM_MINUTES({ num: 0 });
|
||||||
|
case DeviceValueUOM.SECONDS:
|
||||||
|
return LL.NUM_SECONDS({ num: value });
|
||||||
|
case DeviceValueUOM.NONE:
|
||||||
|
return new Intl.NumberFormat().format(value);
|
||||||
|
case DeviceValueUOM.DEGREES:
|
||||||
|
case DeviceValueUOM.DEGREES_R:
|
||||||
|
case DeviceValueUOM.FAHRENHEIT:
|
||||||
|
return (
|
||||||
|
new Intl.NumberFormat(undefined, {
|
||||||
|
minimumFractionDigits: 1
|
||||||
|
}).format(value) +
|
||||||
|
' ' +
|
||||||
|
DeviceValueUOM_s[uom]
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return new Intl.NumberFormat().format(value) + ' ' + DeviceValueUOM_s[uom];
|
||||||
|
}
|
||||||
|
}
|
||||||
446
interface/src/app/main/types.ts
Normal file
446
interface/src/app/main/types.ts
Normal file
@@ -0,0 +1,446 @@
|
|||||||
|
export interface Settings {
|
||||||
|
locale: string;
|
||||||
|
tx_mode: number;
|
||||||
|
ems_bus_id: number;
|
||||||
|
syslog_enabled: boolean;
|
||||||
|
syslog_level: number;
|
||||||
|
syslog_mark_interval: number;
|
||||||
|
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;
|
||||||
|
dallas_gpio: number;
|
||||||
|
dallas_parasite: boolean;
|
||||||
|
led_gpio: number;
|
||||||
|
hide_led: boolean;
|
||||||
|
low_clock: boolean;
|
||||||
|
notoken_api: boolean;
|
||||||
|
readonly_mode: boolean;
|
||||||
|
analog_enabled: boolean;
|
||||||
|
pbutton_gpio: number;
|
||||||
|
trace_raw: boolean;
|
||||||
|
board_profile: string;
|
||||||
|
bool_format: number;
|
||||||
|
bool_dashboard: number;
|
||||||
|
enum_format: number;
|
||||||
|
fahrenheit: boolean;
|
||||||
|
phy_type: number;
|
||||||
|
eth_power: number;
|
||||||
|
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 {
|
||||||
|
BUS_STATUS_CONNECTED = 0,
|
||||||
|
BUS_STATUS_TX_ERRORS = 1,
|
||||||
|
BUS_STATUS_OFFLINE = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Stat {
|
||||||
|
id: number; // id
|
||||||
|
s: number; // success
|
||||||
|
f: number; // fail
|
||||||
|
q: number; // quality
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Activity {
|
||||||
|
stats: Stat[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Device {
|
||||||
|
id: number; // id index
|
||||||
|
tn: string; // device type translated name
|
||||||
|
t: number; // device type id
|
||||||
|
b: string; // brand
|
||||||
|
n: string; // name
|
||||||
|
d: number; // deviceid
|
||||||
|
p: number; // productid
|
||||||
|
v: string; // version
|
||||||
|
e: number; // entities
|
||||||
|
url?: string; // lowercase type name used in API URL
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TemperatureSensor {
|
||||||
|
id: string; // id string
|
||||||
|
n: string; // name/alias
|
||||||
|
t?: number; // temp, optional
|
||||||
|
o: number; // offset
|
||||||
|
u: number; // uom
|
||||||
|
o_n?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AnalogSensor {
|
||||||
|
id: number;
|
||||||
|
g: number; // GPIO
|
||||||
|
n: string;
|
||||||
|
v: number;
|
||||||
|
u: number;
|
||||||
|
o: number;
|
||||||
|
f: number;
|
||||||
|
t: number;
|
||||||
|
d: boolean; // deleted flag
|
||||||
|
o_n?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WriteTemperatureSensor {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
offset: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SensorData {
|
||||||
|
ts: TemperatureSensor[];
|
||||||
|
as: AnalogSensor[];
|
||||||
|
analog_enabled: boolean;
|
||||||
|
platform: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CoreData {
|
||||||
|
connected: boolean;
|
||||||
|
devices: Device[];
|
||||||
|
}
|
||||||
|
|
||||||
|
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?: 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?: string; // steps for up/down, optional
|
||||||
|
m?: number; // min, optional
|
||||||
|
x?: number; // max, optional
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeviceData {
|
||||||
|
nodes: DeviceValue[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeviceEntity {
|
||||||
|
id: string; // shortname
|
||||||
|
v?: unknown; // value, in any format, optional
|
||||||
|
n?: string; // fullname, optional
|
||||||
|
cn?: string; // custom fullname, optional
|
||||||
|
t?: string; // tag for name
|
||||||
|
m: DeviceEntityMask; // mask
|
||||||
|
w: boolean; // writeable
|
||||||
|
mi?: number; // min value
|
||||||
|
ma?: number; // max value
|
||||||
|
o_m?: number; // original mask before edits
|
||||||
|
o_cn?: string; // original cn before edits
|
||||||
|
o_mi?: number; // original min value
|
||||||
|
o_ma?: number; // original max value
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum DeviceValueUOM {
|
||||||
|
NONE = 0,
|
||||||
|
DEGREES,
|
||||||
|
DEGREES_R,
|
||||||
|
PERCENT,
|
||||||
|
LMIN,
|
||||||
|
KWH,
|
||||||
|
WH,
|
||||||
|
HOURS,
|
||||||
|
MINUTES,
|
||||||
|
UA,
|
||||||
|
BAR,
|
||||||
|
KW,
|
||||||
|
W,
|
||||||
|
KB,
|
||||||
|
SECONDS,
|
||||||
|
DBM,
|
||||||
|
FAHRENHEIT,
|
||||||
|
MV,
|
||||||
|
SQM,
|
||||||
|
M3,
|
||||||
|
L,
|
||||||
|
KMIN,
|
||||||
|
K,
|
||||||
|
VOLTS,
|
||||||
|
MBAR,
|
||||||
|
LH
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeviceValueUOM_s = [
|
||||||
|
'',
|
||||||
|
'°C',
|
||||||
|
'°C',
|
||||||
|
'%',
|
||||||
|
'l/min',
|
||||||
|
'kWh',
|
||||||
|
'Wh',
|
||||||
|
'hours',
|
||||||
|
'minutes',
|
||||||
|
'µA',
|
||||||
|
'bar',
|
||||||
|
'kW',
|
||||||
|
'W',
|
||||||
|
'KB',
|
||||||
|
'seconds',
|
||||||
|
'dBm',
|
||||||
|
'°F',
|
||||||
|
'mV',
|
||||||
|
'm²',
|
||||||
|
'm³',
|
||||||
|
'l',
|
||||||
|
'K*min',
|
||||||
|
'K',
|
||||||
|
'V',
|
||||||
|
'mbar',
|
||||||
|
'l/h'
|
||||||
|
];
|
||||||
|
|
||||||
|
export enum AnalogType {
|
||||||
|
REMOVED = -1,
|
||||||
|
NOTUSED = 0,
|
||||||
|
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',
|
||||||
|
'Counter',
|
||||||
|
'ADC',
|
||||||
|
'Timer',
|
||||||
|
'Rate',
|
||||||
|
'Digital Out',
|
||||||
|
'PWM 0',
|
||||||
|
'PWM 1',
|
||||||
|
'PWM 2'
|
||||||
|
];
|
||||||
|
|
||||||
|
type BoardProfiles = Record<string, string>;
|
||||||
|
|
||||||
|
export const BOARD_PROFILES: BoardProfiles = {
|
||||||
|
S32: 'BBQKees Gateway S32',
|
||||||
|
S32S3: 'BBQKees Gateway S3',
|
||||||
|
E32: 'BBQKees Gateway E32',
|
||||||
|
E32V2: 'BBQKees Gateway E32 V2',
|
||||||
|
NODEMCU: 'NodeMCU 32S',
|
||||||
|
'MH-ET': 'MH-ET Live D1 Mini',
|
||||||
|
LOLIN: 'Lolin D32',
|
||||||
|
OLIMEX: 'Olimex ESP32-EVB',
|
||||||
|
OLIMEXPOE: 'Olimex ESP32-POE',
|
||||||
|
C3MINI: 'Wemos C3 Mini',
|
||||||
|
S2MINI: 'Wemos S2 Mini',
|
||||||
|
S3MINI: 'Liligo S3'
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface BoardProfile {
|
||||||
|
board_profile: string;
|
||||||
|
led_gpio: number;
|
||||||
|
dallas_gpio: number;
|
||||||
|
rx_gpio: number;
|
||||||
|
tx_gpio: number;
|
||||||
|
pbutton_gpio: number;
|
||||||
|
phy_type: number;
|
||||||
|
eth_power: number;
|
||||||
|
eth_phy_addr: number;
|
||||||
|
eth_clock_mode: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface APIcall {
|
||||||
|
device: string;
|
||||||
|
cmd: string;
|
||||||
|
id: number;
|
||||||
|
data?: string; // optional
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Action {
|
||||||
|
action: string;
|
||||||
|
param?: string; // optional
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WriteAnalogSensor {
|
||||||
|
id: number;
|
||||||
|
gpio: number;
|
||||||
|
name: string;
|
||||||
|
factor: number;
|
||||||
|
offset: number;
|
||||||
|
uom: number;
|
||||||
|
type: number;
|
||||||
|
deleted: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum DeviceEntityMask {
|
||||||
|
DV_DEFAULT = 0,
|
||||||
|
DV_WEB_EXCLUDE = 1,
|
||||||
|
DV_API_MQTT_EXCLUDE = 2,
|
||||||
|
DV_READONLY = 4,
|
||||||
|
DV_FAVORITE = 8,
|
||||||
|
DV_DELETED = 128
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScheduleItem {
|
||||||
|
id: number; // unique index
|
||||||
|
active: boolean;
|
||||||
|
deleted?: boolean;
|
||||||
|
flags: number;
|
||||||
|
time: string; // also used for Condition and On Change
|
||||||
|
cmd: string;
|
||||||
|
value: string;
|
||||||
|
name: string; // can be empty
|
||||||
|
o_id?: number;
|
||||||
|
o_active?: boolean;
|
||||||
|
o_deleted?: boolean;
|
||||||
|
o_flags?: number;
|
||||||
|
o_time?: string;
|
||||||
|
o_cmd?: string;
|
||||||
|
o_value?: string;
|
||||||
|
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,
|
||||||
|
SCHEDULE_TUE = 4,
|
||||||
|
SCHEDULE_WED = 8,
|
||||||
|
SCHEDULE_THU = 16,
|
||||||
|
SCHEDULE_FRI = 32,
|
||||||
|
SCHEDULE_SAT = 64,
|
||||||
|
// 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;
|
||||||
|
offset: number;
|
||||||
|
factor: number;
|
||||||
|
uom: number;
|
||||||
|
value_type: number;
|
||||||
|
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;
|
||||||
|
o_offset?: number;
|
||||||
|
o_factor?: number;
|
||||||
|
o_uom?: number;
|
||||||
|
o_value_type?: number;
|
||||||
|
o_deleted?: boolean;
|
||||||
|
o_writeable?: boolean;
|
||||||
|
o_value?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Entities {
|
||||||
|
entities: EntityItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// matches emsdevice.h DeviceType
|
||||||
|
export const enum DeviceType {
|
||||||
|
SYSTEM = 0,
|
||||||
|
TEMPERATURESENSOR = 1,
|
||||||
|
ANALOGSENSOR = 2,
|
||||||
|
SCHEDULER = 3,
|
||||||
|
CUSTOM = 4,
|
||||||
|
BOILER,
|
||||||
|
THERMOSTAT,
|
||||||
|
MIXER,
|
||||||
|
SOLAR,
|
||||||
|
HEATPUMP,
|
||||||
|
GATEWAY,
|
||||||
|
SWITCH,
|
||||||
|
CONTROLLER,
|
||||||
|
CONNECT,
|
||||||
|
ALERT,
|
||||||
|
EXTENSION,
|
||||||
|
GENERIC,
|
||||||
|
HEATSOURCE,
|
||||||
|
VENTILATION,
|
||||||
|
WATER,
|
||||||
|
POOL,
|
||||||
|
UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
// matches emsdevicevalue.h
|
||||||
|
export const enum DeviceValueType {
|
||||||
|
BOOL,
|
||||||
|
INT8,
|
||||||
|
UINT8,
|
||||||
|
INT16,
|
||||||
|
UINT16,
|
||||||
|
UINT24,
|
||||||
|
TIME, // same as UINT24
|
||||||
|
UINT32,
|
||||||
|
ENUM,
|
||||||
|
STRING, // RAW
|
||||||
|
CMD
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeviceValueTypeNames = [
|
||||||
|
'BOOL',
|
||||||
|
'INT8',
|
||||||
|
'UINT8',
|
||||||
|
'INT16',
|
||||||
|
'UINT16',
|
||||||
|
'UINT24',
|
||||||
|
'TIME',
|
||||||
|
'UINT32',
|
||||||
|
'ENUM',
|
||||||
|
'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,37 +1,61 @@
|
|||||||
import { FC, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { ValidateFieldsError } from 'async-validator';
|
|
||||||
import { range } from 'lodash';
|
|
||||||
|
|
||||||
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
|
import WarningIcon from '@mui/icons-material/Warning';
|
||||||
import { Button, Checkbox, MenuItem } from '@mui/material';
|
import { Button, Checkbox, MenuItem } from '@mui/material';
|
||||||
import SaveIcon from '@mui/icons-material/Save';
|
|
||||||
|
|
||||||
import { createAPSettingsValidator, validate } from '../../validators';
|
import * as APApi from 'api/ap';
|
||||||
|
|
||||||
|
import type { ValidateFieldsError } from 'async-validator';
|
||||||
import {
|
import {
|
||||||
BlockFormControlLabel,
|
BlockFormControlLabel,
|
||||||
|
BlockNavigation,
|
||||||
ButtonRow,
|
ButtonRow,
|
||||||
FormLoader,
|
FormLoader,
|
||||||
SectionContent,
|
SectionContent,
|
||||||
ValidatedPasswordField,
|
ValidatedPasswordField,
|
||||||
ValidatedTextField
|
ValidatedTextField,
|
||||||
} from '../../components';
|
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';
|
||||||
|
|
||||||
import { APProvisionMode, APSettings } from '../../types';
|
export const isAPEnabled = ({ provision_mode }: APSettingsType) =>
|
||||||
import { numberValue, updateValue, useRest } from '../../utils';
|
provision_mode === APProvisionMode.AP_MODE_ALWAYS ||
|
||||||
import * as APApi from '../../api/ap';
|
provision_mode === APProvisionMode.AP_MODE_DISCONNECTED;
|
||||||
|
|
||||||
export const isAPEnabled = ({ provision_mode }: APSettings) => {
|
const APSettings = () => {
|
||||||
return provision_mode === APProvisionMode.AP_MODE_ALWAYS || provision_mode === APProvisionMode.AP_MODE_DISCONNECTED;
|
const {
|
||||||
};
|
loadData,
|
||||||
|
saving,
|
||||||
const APSettingsForm: FC = () => {
|
data,
|
||||||
const { loadData, saving, data, setData, saveData, errorMessage } = useRest<APSettings>({
|
updateDataValue,
|
||||||
|
origData,
|
||||||
|
dirtyFlags,
|
||||||
|
setDirtyFlags,
|
||||||
|
blocker,
|
||||||
|
saveData,
|
||||||
|
errorMessage
|
||||||
|
} = useRest<APSettingsType>({
|
||||||
read: APApi.readAPSettings,
|
read: APApi.readAPSettings,
|
||||||
update: APApi.updateAPSettings
|
update: APApi.updateAPSettings
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
|
useLayoutTitle(LL.SETTINGS_OF(LL.ACCESS_POINT(0)));
|
||||||
|
|
||||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
|
|
||||||
const updateFormValue = updateValue(setData);
|
const updateFormValue = updateValueDirty(
|
||||||
|
origData,
|
||||||
|
dirtyFlags,
|
||||||
|
setDirtyFlags,
|
||||||
|
updateDataValue
|
||||||
|
);
|
||||||
|
|
||||||
const content = () => {
|
const content = () => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
@@ -42,18 +66,23 @@ const APSettingsForm: FC = () => {
|
|||||||
try {
|
try {
|
||||||
setFieldErrors(undefined);
|
setFieldErrors(undefined);
|
||||||
await validate(createAPSettingsValidator(data), data);
|
await validate(createAPSettingsValidator(data), data);
|
||||||
saveData();
|
await saveData();
|
||||||
} catch (errors: any) {
|
} catch (error) {
|
||||||
setFieldErrors(errors);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
fieldErrors={fieldErrors}
|
fieldErrors={fieldErrors}
|
||||||
name="provision_mode"
|
name="provision_mode"
|
||||||
label="Provide Access Point…"
|
label={LL.AP_PROVIDE() + '...'}
|
||||||
value={data.provision_mode}
|
value={data.provision_mode}
|
||||||
fullWidth
|
fullWidth
|
||||||
select
|
select
|
||||||
@@ -61,16 +90,22 @@ const APSettingsForm: FC = () => {
|
|||||||
onChange={updateFormValue}
|
onChange={updateFormValue}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
>
|
>
|
||||||
<MenuItem value={APProvisionMode.AP_MODE_ALWAYS}>Always</MenuItem>
|
<MenuItem value={APProvisionMode.AP_MODE_ALWAYS}>
|
||||||
<MenuItem value={APProvisionMode.AP_MODE_DISCONNECTED}>When WiFi Disconnected</MenuItem>
|
{LL.AP_PROVIDE_TEXT_1()}
|
||||||
<MenuItem value={APProvisionMode.AP_NEVER}>Never</MenuItem>
|
</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>
|
</ValidatedTextField>
|
||||||
{isAPEnabled(data) && (
|
{isAPEnabled(data) && (
|
||||||
<>
|
<>
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
fieldErrors={fieldErrors}
|
fieldErrors={fieldErrors}
|
||||||
name="ssid"
|
name="ssid"
|
||||||
label="Access Point SSID"
|
label={LL.ACCESS_POINT(2) + ' SSID'}
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={data.ssid}
|
value={data.ssid}
|
||||||
@@ -80,7 +115,7 @@ const APSettingsForm: FC = () => {
|
|||||||
<ValidatedPasswordField
|
<ValidatedPasswordField
|
||||||
fieldErrors={fieldErrors}
|
fieldErrors={fieldErrors}
|
||||||
name="password"
|
name="password"
|
||||||
label="Access Point Password"
|
label={LL.ACCESS_POINT(2) + ' ' + LL.PASSWORD()}
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={data.password}
|
value={data.password}
|
||||||
@@ -90,7 +125,7 @@ const APSettingsForm: FC = () => {
|
|||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
fieldErrors={fieldErrors}
|
fieldErrors={fieldErrors}
|
||||||
name="channel"
|
name="channel"
|
||||||
label="Preferred Channel"
|
label={LL.AP_PREFERRED_CHANNEL()}
|
||||||
value={numberValue(data.channel)}
|
value={numberValue(data.channel)}
|
||||||
fullWidth
|
fullWidth
|
||||||
select
|
select
|
||||||
@@ -106,13 +141,19 @@ const APSettingsForm: FC = () => {
|
|||||||
))}
|
))}
|
||||||
</ValidatedTextField>
|
</ValidatedTextField>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={<Checkbox name="ssid_hidden" checked={data.ssid_hidden} onChange={updateFormValue} />}
|
control={
|
||||||
label="Hide SSID"
|
<Checkbox
|
||||||
|
name="ssid_hidden"
|
||||||
|
checked={data.ssid_hidden}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={LL.AP_HIDE_SSID()}
|
||||||
/>
|
/>
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
fieldErrors={fieldErrors}
|
fieldErrors={fieldErrors}
|
||||||
name="max_clients"
|
name="max_clients"
|
||||||
label="Max Clients"
|
label={LL.AP_MAX_CLIENTS()}
|
||||||
value={numberValue(data.max_clients)}
|
value={numberValue(data.max_clients)}
|
||||||
fullWidth
|
fullWidth
|
||||||
select
|
select
|
||||||
@@ -130,7 +171,7 @@ const APSettingsForm: FC = () => {
|
|||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
fieldErrors={fieldErrors}
|
fieldErrors={fieldErrors}
|
||||||
name="local_ip"
|
name="local_ip"
|
||||||
label="Local IP"
|
label={LL.AP_LOCAL_IP()}
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={data.local_ip}
|
value={data.local_ip}
|
||||||
@@ -140,7 +181,7 @@ const APSettingsForm: FC = () => {
|
|||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
fieldErrors={fieldErrors}
|
fieldErrors={fieldErrors}
|
||||||
name="gateway_ip"
|
name="gateway_ip"
|
||||||
label="Gateway"
|
label={LL.NETWORK_GATEWAY()}
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={data.gateway_ip}
|
value={data.gateway_ip}
|
||||||
@@ -150,7 +191,7 @@ const APSettingsForm: FC = () => {
|
|||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
fieldErrors={fieldErrors}
|
fieldErrors={fieldErrors}
|
||||||
name="subnet_mask"
|
name="subnet_mask"
|
||||||
label="Subnet"
|
label={LL.NETWORK_SUBNET()}
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={data.subnet_mask}
|
value={data.subnet_mask}
|
||||||
@@ -159,27 +200,40 @@ const APSettingsForm: FC = () => {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<ButtonRow>
|
{dirtyFlags && dirtyFlags.length !== 0 && (
|
||||||
<Button
|
<ButtonRow>
|
||||||
startIcon={<SaveIcon />}
|
<Button
|
||||||
disabled={saving}
|
startIcon={<CancelIcon />}
|
||||||
variant="outlined"
|
disabled={saving}
|
||||||
color="primary"
|
variant="outlined"
|
||||||
type="submit"
|
color="secondary"
|
||||||
onClick={validateAndSubmit}
|
type="submit"
|
||||||
>
|
onClick={loadData}
|
||||||
Save
|
>
|
||||||
</Button>
|
{LL.CANCEL()}
|
||||||
</ButtonRow>
|
</Button>
|
||||||
|
<Button
|
||||||
|
startIcon={<WarningIcon color="warning" />}
|
||||||
|
disabled={saving}
|
||||||
|
variant="contained"
|
||||||
|
color="info"
|
||||||
|
type="submit"
|
||||||
|
onClick={validateAndSubmit}
|
||||||
|
>
|
||||||
|
{LL.APPLY_CHANGES(dirtyFlags.length)}
|
||||||
|
</Button>
|
||||||
|
</ButtonRow>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContent title="Access Point Settings" titleGutter>
|
<SectionContent>
|
||||||
|
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||||
{content()}
|
{content()}
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default APSettingsForm;
|
export default APSettings;
|
||||||
861
interface/src/app/settings/ApplicationSettings.tsx
Normal file
861
interface/src/app/settings/ApplicationSettings.tsx
Normal file
@@ -0,0 +1,861 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
|
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
|
||||||
|
import WarningIcon from '@mui/icons-material/Warning';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
Divider,
|
||||||
|
InputAdornment,
|
||||||
|
MenuItem,
|
||||||
|
TextField,
|
||||||
|
Typography
|
||||||
|
} from '@mui/material';
|
||||||
|
import Grid from '@mui/material/Grid2';
|
||||||
|
|
||||||
|
import { readSystemStatus } from 'api/system';
|
||||||
|
|
||||||
|
import { useRequest } from 'alova/client';
|
||||||
|
import RestartMonitor from 'app/status/RestartMonitor';
|
||||||
|
import type { ValidateFieldsError } from 'async-validator';
|
||||||
|
import {
|
||||||
|
BlockFormControlLabel,
|
||||||
|
BlockNavigation,
|
||||||
|
ButtonRow,
|
||||||
|
FormLoader,
|
||||||
|
MessageBox,
|
||||||
|
SectionContent,
|
||||||
|
ValidatedTextField,
|
||||||
|
useLayoutTitle
|
||||||
|
} from 'components';
|
||||||
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
import { numberValue, updateValueDirty, useRest } from 'utils';
|
||||||
|
import { validate } from 'validators';
|
||||||
|
|
||||||
|
import { API, getBoardProfile, readSettings, writeSettings } from '../../api/app';
|
||||||
|
import { BOARD_PROFILES } from '../main/types';
|
||||||
|
import type { APIcall, Settings } from '../main/types';
|
||||||
|
import { createSettingsValidator } from '../main/validators';
|
||||||
|
|
||||||
|
export function boardProfileSelectItems() {
|
||||||
|
return Object.keys(BOARD_PROFILES).map((code) => (
|
||||||
|
<MenuItem key={code} value={code}>
|
||||||
|
{BOARD_PROFILES[code]}
|
||||||
|
</MenuItem>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
const ApplicationSettings = () => {
|
||||||
|
const { data: hardwareData } = useRequest(readSystemStatus);
|
||||||
|
|
||||||
|
const {
|
||||||
|
loadData,
|
||||||
|
saveData,
|
||||||
|
updateDataValue,
|
||||||
|
data,
|
||||||
|
origData,
|
||||||
|
dirtyFlags,
|
||||||
|
setDirtyFlags,
|
||||||
|
blocker,
|
||||||
|
errorMessage,
|
||||||
|
restartNeeded
|
||||||
|
} = useRest<Settings>({
|
||||||
|
read: readSettings,
|
||||||
|
update: writeSettings
|
||||||
|
});
|
||||||
|
|
||||||
|
const [restarting, setRestarting] = useState<boolean>();
|
||||||
|
|
||||||
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
|
const updateFormValue = updateValueDirty(
|
||||||
|
origData,
|
||||||
|
dirtyFlags,
|
||||||
|
setDirtyFlags,
|
||||||
|
updateDataValue
|
||||||
|
);
|
||||||
|
|
||||||
|
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
|
|
||||||
|
const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
|
||||||
|
immediate: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const { loading: processingBoard, send: readBoardProfile } = useRequest(
|
||||||
|
(boardProfile: string) => getBoardProfile(boardProfile),
|
||||||
|
{
|
||||||
|
immediate: false
|
||||||
|
}
|
||||||
|
).onSuccess((event) => {
|
||||||
|
const response = event.data as Settings;
|
||||||
|
updateDataValue({
|
||||||
|
...data,
|
||||||
|
board_profile: response.board_profile,
|
||||||
|
led_gpio: response.led_gpio,
|
||||||
|
dallas_gpio: response.dallas_gpio,
|
||||||
|
rx_gpio: response.rx_gpio,
|
||||||
|
tx_gpio: response.tx_gpio,
|
||||||
|
pbutton_gpio: response.pbutton_gpio,
|
||||||
|
phy_type: response.phy_type,
|
||||||
|
eth_power: response.eth_power,
|
||||||
|
eth_phy_addr: response.eth_phy_addr,
|
||||||
|
eth_clock_mode: response.eth_clock_mode
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const doRestart = async () => {
|
||||||
|
setRestarting(true);
|
||||||
|
await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch(
|
||||||
|
(error: Error) => {
|
||||||
|
toast.error(error.message);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateBoardProfile = async (board_profile: string) => {
|
||||||
|
await readBoardProfile(board_profile).catch((error: Error) => {
|
||||||
|
toast.error(error.message);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useLayoutTitle(LL.SETTINGS_OF(LL.APPLICATION()));
|
||||||
|
|
||||||
|
const SecondsInputProps = {
|
||||||
|
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||||
|
};
|
||||||
|
const MilliSecondsInputProps = {
|
||||||
|
endAdornment: <InputAdornment position="end">ms</InputAdornment>
|
||||||
|
};
|
||||||
|
const MinutesInputProps = {
|
||||||
|
endAdornment: <InputAdornment position="end">{LL.MINUTES()}</InputAdornment>
|
||||||
|
};
|
||||||
|
const HoursInputProps = {
|
||||||
|
endAdornment: <InputAdornment position="end">{LL.HOURS()}</InputAdornment>
|
||||||
|
};
|
||||||
|
|
||||||
|
const content = () => {
|
||||||
|
if (!data || !hardwareData) {
|
||||||
|
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateAndSubmit = async () => {
|
||||||
|
try {
|
||||||
|
setFieldErrors(undefined);
|
||||||
|
await validate(createSettingsValidator(data), data);
|
||||||
|
} catch (error) {
|
||||||
|
setFieldErrors(error as ValidateFieldsError);
|
||||||
|
} finally {
|
||||||
|
await saveData();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeBoardProfile = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const boardProfile = event.target.value;
|
||||||
|
updateFormValue(event);
|
||||||
|
if (boardProfile === 'CUSTOM') {
|
||||||
|
updateDataValue({
|
||||||
|
...data,
|
||||||
|
board_profile: boardProfile
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
void updateBoardProfile(boardProfile);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const restart = async () => {
|
||||||
|
await validateAndSubmit();
|
||||||
|
await doRestart();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Typography sx={{ pb: 1 }} variant="h6" color="primary">
|
||||||
|
{LL.SERVICES()}
|
||||||
|
</Typography>
|
||||||
|
<Typography color="secondary">API</Typography>
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={data.notoken_api}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
name="notoken_api"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={LL.BYPASS_TOKEN()}
|
||||||
|
/>
|
||||||
|
<Typography color="secondary">Console</Typography>
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={data.telnet_enabled}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
name="telnet_enabled"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={LL.ENABLE_TELNET()}
|
||||||
|
/>
|
||||||
|
<Typography color="secondary">Modbus</Typography>
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={data.modbus_enabled}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
name="modbus_enabled"
|
||||||
|
disabled={!hardwareData.psram}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={LL.ENABLE_MODBUS()}
|
||||||
|
/>
|
||||||
|
{data.modbus_enabled && (
|
||||||
|
<Grid container spacing={2} rowSpacing={0}>
|
||||||
|
<Grid>
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
|
name="modbus_max_clients"
|
||||||
|
label={LL.AP_MAX_CLIENTS()}
|
||||||
|
variant="outlined"
|
||||||
|
value={numberValue(data.modbus_max_clients)}
|
||||||
|
type="number"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
|
name="modbus_port"
|
||||||
|
label="Port"
|
||||||
|
variant="outlined"
|
||||||
|
value={numberValue(data.modbus_port)}
|
||||||
|
type="number"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
|
name="modbus_timeout"
|
||||||
|
label="Timeout"
|
||||||
|
slotProps={{
|
||||||
|
input: MilliSecondsInputProps
|
||||||
|
}}
|
||||||
|
variant="outlined"
|
||||||
|
value={numberValue(data.modbus_timeout)}
|
||||||
|
type="number"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
<Typography color="secondary">Syslog</Typography>
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={data.syslog_enabled}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
name="syslog_enabled"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={LL.ENABLE_SYSLOG()}
|
||||||
|
/>
|
||||||
|
{data.syslog_enabled && (
|
||||||
|
<Grid container spacing={2} rowSpacing={0}>
|
||||||
|
<Grid>
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
|
name="syslog_host"
|
||||||
|
label="Host"
|
||||||
|
variant="outlined"
|
||||||
|
value={data.syslog_host}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
|
name="syslog_port"
|
||||||
|
label="Port"
|
||||||
|
variant="outlined"
|
||||||
|
value={numberValue(data.syslog_port)}
|
||||||
|
type="number"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<TextField
|
||||||
|
name="syslog_level"
|
||||||
|
label={LL.LOG_LEVEL()}
|
||||||
|
value={data.syslog_level}
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
select
|
||||||
|
>
|
||||||
|
<MenuItem value={-1}>OFF</MenuItem>
|
||||||
|
<MenuItem value={3}>ERR</MenuItem>
|
||||||
|
<MenuItem value={5}>NOTICE</MenuItem>
|
||||||
|
<MenuItem value={6}>INFO</MenuItem>
|
||||||
|
<MenuItem value={7}>DEBUG</MenuItem>
|
||||||
|
<MenuItem value={9}>ALL</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
|
name="syslog_mark_interval"
|
||||||
|
label={LL.MARK_INTERVAL()}
|
||||||
|
slotProps={{
|
||||||
|
input: SecondsInputProps
|
||||||
|
}}
|
||||||
|
variant="outlined"
|
||||||
|
value={numberValue(data.syslog_mark_interval)}
|
||||||
|
type="number"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
<Typography sx={{ pb: 1, pt: 2 }} variant="h6" color="primary">
|
||||||
|
{LL.SENSORS()}
|
||||||
|
</Typography>
|
||||||
|
<Typography color="secondary">Analog</Typography>
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={data.analog_enabled}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
name="analog_enabled"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={LL.ENABLE_ANALOG()}
|
||||||
|
/>
|
||||||
|
{data.dallas_gpio !== 0 && (
|
||||||
|
<>
|
||||||
|
<Typography color="secondary">{LL.TEMPERATURE()}</Typography>
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={data.dallas_parasite}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
name="dallas_parasite"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={LL.ENABLE_PARASITE()}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Typography sx={{ pb: 1, pt: 2 }} variant="h6" color="primary">
|
||||||
|
{LL.FORMATTING_OPTIONS()}
|
||||||
|
</Typography>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid size={3}>
|
||||||
|
<TextField
|
||||||
|
name="locale"
|
||||||
|
label={LL.LANGUAGE_ENTITIES()}
|
||||||
|
value={data.locale}
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
select
|
||||||
|
>
|
||||||
|
<MenuItem value="cz">Česky (CZ)</MenuItem>
|
||||||
|
<MenuItem value="de">Deutsch (DE)</MenuItem>
|
||||||
|
<MenuItem value="en">English (EN)</MenuItem>
|
||||||
|
<MenuItem value="fr">Français (FR)</MenuItem>
|
||||||
|
<MenuItem value="it">Italiano (IT)</MenuItem>
|
||||||
|
<MenuItem value="nl">Nederlands (NL)</MenuItem>
|
||||||
|
<MenuItem value="no">Norsk (NO)</MenuItem>
|
||||||
|
<MenuItem value="pl">Polski (PL)</MenuItem>
|
||||||
|
<MenuItem value="sk">Slovenčina (SK)</MenuItem>
|
||||||
|
<MenuItem value="sv">Svenska (SV)</MenuItem>
|
||||||
|
<MenuItem value="tr">Türk (TR)</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={3}>
|
||||||
|
<TextField
|
||||||
|
name="bool_dashboard"
|
||||||
|
label={LL.BOOLEAN_FORMAT_DASHBOARD()}
|
||||||
|
value={data.bool_dashboard}
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
select
|
||||||
|
>
|
||||||
|
<MenuItem value={1}>{LL.ONOFF()}</MenuItem>
|
||||||
|
<MenuItem value={2}>{LL.ONOFF_CAP()}</MenuItem>
|
||||||
|
<MenuItem value={3}>true/false</MenuItem>
|
||||||
|
<MenuItem value={5}>1/0</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={3}>
|
||||||
|
<TextField
|
||||||
|
name="bool_format"
|
||||||
|
label={LL.BOOLEAN_FORMAT_API()}
|
||||||
|
value={data.bool_format}
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
select
|
||||||
|
>
|
||||||
|
<MenuItem value={1}>{LL.ONOFF()}</MenuItem>
|
||||||
|
<MenuItem value={2}>{LL.ONOFF_CAP()}</MenuItem>
|
||||||
|
<MenuItem value={3}>"true"/"false"</MenuItem>
|
||||||
|
<MenuItem value={4}>true/false</MenuItem>
|
||||||
|
<MenuItem value={5}>"1"/"0"</MenuItem>
|
||||||
|
<MenuItem value={6}>1/0</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={3}>
|
||||||
|
<TextField
|
||||||
|
name="enum_format"
|
||||||
|
label={LL.ENUM_FORMAT()}
|
||||||
|
value={data.enum_format}
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
select
|
||||||
|
>
|
||||||
|
<MenuItem value={1}>{LL.VALUE(5)}</MenuItem>
|
||||||
|
<MenuItem value={2}>{LL.INDEX()}</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={data.fahrenheit}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
name="fahrenheit"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={LL.CONVERT_FAHRENHEIT()}
|
||||||
|
/>
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={data.trace_raw}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
name="trace_raw"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={LL.LOG_HEX()}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Typography sx={{ pb: 1, pt: 2 }} variant="h6" color="primary">
|
||||||
|
{LL.SETTINGS_OF(LL.HARDWARE())}
|
||||||
|
</Typography>
|
||||||
|
<TextField
|
||||||
|
name="board_profile"
|
||||||
|
label={LL.BOARD_PROFILE()}
|
||||||
|
value={data.board_profile}
|
||||||
|
disabled={processingBoard || hardwareData.model.startsWith('BBQKees')}
|
||||||
|
variant="outlined"
|
||||||
|
onChange={changeBoardProfile}
|
||||||
|
margin="normal"
|
||||||
|
select
|
||||||
|
>
|
||||||
|
{boardProfileSelectItems()}
|
||||||
|
<Divider />
|
||||||
|
<MenuItem key={'CUSTOM'} value={'CUSTOM'}>
|
||||||
|
{LL.CUSTOM()}…
|
||||||
|
</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
{data.board_profile === 'CUSTOM' && (
|
||||||
|
<>
|
||||||
|
<Grid container spacing={2} rowSpacing={0}>
|
||||||
|
<Grid>
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
|
name="rx_gpio"
|
||||||
|
label={LL.GPIO_OF('Rx')}
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={numberValue(data.rx_gpio)}
|
||||||
|
type="number"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
|
name="tx_gpio"
|
||||||
|
label={LL.GPIO_OF('Tx')}
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={numberValue(data.tx_gpio)}
|
||||||
|
type="number"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
|
name="pbutton_gpio"
|
||||||
|
label={LL.GPIO_OF(LL.BUTTON())}
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={numberValue(data.pbutton_gpio)}
|
||||||
|
type="number"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
|
name="dallas_gpio"
|
||||||
|
label={
|
||||||
|
LL.GPIO_OF(LL.TEMPERATURE()) + ' (0=' + LL.DISABLED(1) + ')'
|
||||||
|
}
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={numberValue(data.dallas_gpio)}
|
||||||
|
type="number"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
|
name="led_gpio"
|
||||||
|
label={LL.GPIO_OF('LED') + ' (0=' + LL.DISABLED(1) + ')'}
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={numberValue(data.led_gpio)}
|
||||||
|
type="number"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<TextField
|
||||||
|
name="phy_type"
|
||||||
|
label={LL.PHY_TYPE()}
|
||||||
|
value={data.phy_type}
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
select
|
||||||
|
>
|
||||||
|
<MenuItem value={0}>{LL.DISABLED(1)}</MenuItem>
|
||||||
|
<MenuItem value={1}>LAN8720</MenuItem>
|
||||||
|
<MenuItem value={2}>TLK110</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
{data.phy_type !== 0 && (
|
||||||
|
<Grid container spacing={2} rowSpacing={0}>
|
||||||
|
<Grid>
|
||||||
|
<TextField
|
||||||
|
name="eth_power"
|
||||||
|
label={LL.GPIO_OF('PHY Power') + ' (-1=' + LL.DISABLED(1) + ')'}
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={numberValue(data.eth_power)}
|
||||||
|
type="number"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<TextField
|
||||||
|
name="eth_phy_addr"
|
||||||
|
label={LL.ADDRESS_OF('PHY I²C')}
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={numberValue(data.eth_phy_addr)}
|
||||||
|
type="number"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<TextField
|
||||||
|
name="eth_clock_mode"
|
||||||
|
label="PHY Clk"
|
||||||
|
value={data.eth_clock_mode}
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
select
|
||||||
|
>
|
||||||
|
<MenuItem value={0}>GPIO0_IN</MenuItem>
|
||||||
|
<MenuItem value={1}>GPIO0_OUT</MenuItem>
|
||||||
|
<MenuItem value={2}>GPIO16_OUT</MenuItem>
|
||||||
|
<MenuItem value={3}>GPIO17_OUT</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Grid container spacing={2} rowSpacing={0}>
|
||||||
|
<Grid>
|
||||||
|
<TextField
|
||||||
|
name="tx_mode"
|
||||||
|
label={LL.TX_MODE()}
|
||||||
|
value={data.tx_mode}
|
||||||
|
variant="outlined"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
sx={{ width: '15ch' }}
|
||||||
|
select
|
||||||
|
>
|
||||||
|
<MenuItem value={1}>EMS</MenuItem>
|
||||||
|
<MenuItem value={2}>EMS+</MenuItem>
|
||||||
|
<MenuItem value={3}>HT3</MenuItem>
|
||||||
|
<MenuItem value={4}>{LL.HARDWARE()}</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<TextField
|
||||||
|
name="ems_bus_id"
|
||||||
|
label={LL.ID_OF(LL.EMS_BUS(0))}
|
||||||
|
value={data.ems_bus_id}
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
select
|
||||||
|
>
|
||||||
|
<MenuItem value={0x0a}>Terminal (0x0A)</MenuItem>
|
||||||
|
<MenuItem value={0x0b}>Service Key (0x0B)</MenuItem>
|
||||||
|
<MenuItem value={0x0d}>Modem (0x0D)</MenuItem>
|
||||||
|
<MenuItem value={0x0e}>Converter (0x0E)</MenuItem>
|
||||||
|
<MenuItem value={0x0f}>Time Module (0x0F)</MenuItem>
|
||||||
|
<MenuItem value={0x48}>Gateway 1 (0x48)</MenuItem>
|
||||||
|
<MenuItem value={0x49}>Gateway 2 (0x49)</MenuItem>
|
||||||
|
<MenuItem value={0x4a}>Gateway 3 (0x4A)</MenuItem>
|
||||||
|
<MenuItem value={0x4b}>Gateway 4 (0x4B)</MenuItem>
|
||||||
|
<MenuItem value={0x4c}>Gateway 5 (0x4C)</MenuItem>
|
||||||
|
<MenuItem value={0x4d}>Gateway 7 (0x4D)</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={data.readonly_mode}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
name="readonly_mode"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={LL.READONLY()}
|
||||||
|
/>
|
||||||
|
{data.led_gpio !== 0 && (
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={data.hide_led}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
name="hide_led"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={LL.HIDE_LED()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={data.low_clock}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
name="low_clock"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={LL.UNDERCLOCK_CPU()}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Typography sx={{ pb: 1, pt: 2 }} variant="h6" color="primary">
|
||||||
|
{LL.SPECIAL_FUNCTIONS()}
|
||||||
|
</Typography>
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={data.developer_mode}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
name="developer_mode"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={LL.DEVELOPER_MODE()}
|
||||||
|
/>
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={data.boiler_heatingoff}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
name="boiler_heatingoff"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={LL.HEATINGOFF()}
|
||||||
|
/>
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={data.remote_timeout_en}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
name="remote_timeout_en"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={LL.REMOTE_TIMEOUT_EN()}
|
||||||
|
/>
|
||||||
|
{data.remote_timeout_en && (
|
||||||
|
<Box mt={2}>
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
|
name="remote_timeout"
|
||||||
|
label={LL.REMOTE_TIMEOUT()}
|
||||||
|
slotProps={{
|
||||||
|
input: HoursInputProps
|
||||||
|
}}
|
||||||
|
variant="outlined"
|
||||||
|
value={numberValue(data.remote_timeout)}
|
||||||
|
type="number"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<Grid container spacing={2} rowSpacing={0}>
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={data.shower_timer}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
name="shower_timer"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={LL.ENABLE_SHOWER_TIMER()}
|
||||||
|
/>
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={data.shower_alert}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
name="shower_alert"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={LL.ENABLE_SHOWER_ALERT()}
|
||||||
|
disabled={!data.shower_timer}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid container spacing={2} sx={{ pt: 2 }}>
|
||||||
|
{data.shower_timer && (
|
||||||
|
<Grid>
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
|
name="shower_min_duration"
|
||||||
|
label={LL.MIN_DURATION()}
|
||||||
|
slotProps={{
|
||||||
|
input: SecondsInputProps
|
||||||
|
}}
|
||||||
|
variant="outlined"
|
||||||
|
value={numberValue(data.shower_min_duration)}
|
||||||
|
fullWidth
|
||||||
|
type="number"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
{data.shower_alert && (
|
||||||
|
<>
|
||||||
|
<Grid>
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
|
name="shower_alert_trigger"
|
||||||
|
label={LL.TRIGGER_TIME()}
|
||||||
|
slotProps={{
|
||||||
|
input: MinutesInputProps
|
||||||
|
}}
|
||||||
|
variant="outlined"
|
||||||
|
value={numberValue(data.shower_alert_trigger)}
|
||||||
|
fullWidth
|
||||||
|
type="number"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
disabled={!data.shower_timer}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
|
name="shower_alert_coldshot"
|
||||||
|
label={LL.COLD_SHOT_DURATION()}
|
||||||
|
slotProps={{
|
||||||
|
input: SecondsInputProps
|
||||||
|
}}
|
||||||
|
variant="outlined"
|
||||||
|
value={numberValue(data.shower_alert_coldshot)}
|
||||||
|
fullWidth
|
||||||
|
type="number"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
disabled={!data.shower_timer}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{restartNeeded && (
|
||||||
|
<MessageBox my={2} level="warning" message={LL.RESTART_TEXT(0)}>
|
||||||
|
<Button
|
||||||
|
startIcon={<PowerSettingsNewIcon />}
|
||||||
|
variant="contained"
|
||||||
|
color="error"
|
||||||
|
onClick={restart}
|
||||||
|
>
|
||||||
|
{LL.RESTART()}
|
||||||
|
</Button>
|
||||||
|
</MessageBox>
|
||||||
|
)}
|
||||||
|
{!restartNeeded && dirtyFlags && dirtyFlags.length !== 0 && (
|
||||||
|
<ButtonRow>
|
||||||
|
<Button
|
||||||
|
startIcon={<CancelIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
type="submit"
|
||||||
|
onClick={loadData}
|
||||||
|
>
|
||||||
|
{LL.CANCEL()}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
startIcon={<WarningIcon color="warning" />}
|
||||||
|
variant="contained"
|
||||||
|
color="info"
|
||||||
|
type="submit"
|
||||||
|
onClick={validateAndSubmit}
|
||||||
|
>
|
||||||
|
{LL.APPLY_CHANGES(dirtyFlags.length)}
|
||||||
|
</Button>
|
||||||
|
</ButtonRow>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionContent>
|
||||||
|
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||||
|
{restarting ? <RestartMonitor /> : content()}
|
||||||
|
</SectionContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ApplicationSettings;
|
||||||
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;
|
||||||
507
interface/src/app/settings/MqttSettings.tsx
Normal file
507
interface/src/app/settings/MqttSettings.tsx
Normal file
@@ -0,0 +1,507 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
|
import WarningIcon from '@mui/icons-material/Warning';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
InputAdornment,
|
||||||
|
MenuItem,
|
||||||
|
TextField,
|
||||||
|
Typography
|
||||||
|
} from '@mui/material';
|
||||||
|
import Grid from '@mui/material/Grid2';
|
||||||
|
|
||||||
|
import * as MqttApi from 'api/mqtt';
|
||||||
|
|
||||||
|
import type { ValidateFieldsError } from 'async-validator';
|
||||||
|
import {
|
||||||
|
BlockFormControlLabel,
|
||||||
|
BlockNavigation,
|
||||||
|
ButtonRow,
|
||||||
|
FormLoader,
|
||||||
|
SectionContent,
|
||||||
|
ValidatedPasswordField,
|
||||||
|
ValidatedTextField,
|
||||||
|
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 MqttSettings = () => {
|
||||||
|
const {
|
||||||
|
loadData,
|
||||||
|
saving,
|
||||||
|
data,
|
||||||
|
updateDataValue,
|
||||||
|
origData,
|
||||||
|
dirtyFlags,
|
||||||
|
setDirtyFlags,
|
||||||
|
blocker,
|
||||||
|
saveData,
|
||||||
|
errorMessage
|
||||||
|
} = 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 SecondsInputProps = {
|
||||||
|
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||||
|
};
|
||||||
|
|
||||||
|
const content = () => {
|
||||||
|
if (!data) {
|
||||||
|
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateAndSubmit = async () => {
|
||||||
|
try {
|
||||||
|
setFieldErrors(undefined);
|
||||||
|
await validate(createMqttSettingsValidator(data), data);
|
||||||
|
await saveData();
|
||||||
|
} catch (error) {
|
||||||
|
setFieldErrors(error as ValidateFieldsError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
name="enabled"
|
||||||
|
checked={data.enabled}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={LL.ENABLE_MQTT()}
|
||||||
|
/>
|
||||||
|
<Grid container spacing={2} rowSpacing={0}>
|
||||||
|
<Grid>
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
|
name="host"
|
||||||
|
label={LL.ADDRESS_OF(LL.BROKER())}
|
||||||
|
multiline
|
||||||
|
variant="outlined"
|
||||||
|
value={data.host}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
|
name="port"
|
||||||
|
label="Port"
|
||||||
|
variant="outlined"
|
||||||
|
value={numberValue(data.port)}
|
||||||
|
type="number"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
|
name="base"
|
||||||
|
label={LL.BASE_TOPIC()}
|
||||||
|
variant="outlined"
|
||||||
|
value={data.base}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<TextField
|
||||||
|
name="client_id"
|
||||||
|
label={LL.ID_OF(LL.CLIENT()) + ' (' + LL.OPTIONAL() + ')'}
|
||||||
|
variant="outlined"
|
||||||
|
value={data.client_id}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<TextField
|
||||||
|
name="username"
|
||||||
|
label={LL.USERNAME(0)}
|
||||||
|
variant="outlined"
|
||||||
|
value={data.username}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<ValidatedPasswordField
|
||||||
|
name="password"
|
||||||
|
label={LL.PASSWORD()}
|
||||||
|
variant="outlined"
|
||||||
|
value={data.password}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
|
name="keep_alive"
|
||||||
|
label="Keep Alive"
|
||||||
|
slotProps={{
|
||||||
|
input: SecondsInputProps
|
||||||
|
}}
|
||||||
|
variant="outlined"
|
||||||
|
value={numberValue(data.keep_alive)}
|
||||||
|
type="number"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<TextField
|
||||||
|
name="mqtt_qos"
|
||||||
|
label="QoS"
|
||||||
|
value={data.mqtt_qos}
|
||||||
|
variant="outlined"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
select
|
||||||
|
>
|
||||||
|
<MenuItem value={0}>0</MenuItem>
|
||||||
|
<MenuItem value={1}>1</MenuItem>
|
||||||
|
<MenuItem value={2}>2</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
</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}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={LL.MQTT_CLEAN_SESSION()}
|
||||||
|
/>
|
||||||
|
<BlockFormControlLabel
|
||||||
|
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>
|
||||||
|
<TextField
|
||||||
|
name="nested_format"
|
||||||
|
label={LL.MQTT_FORMAT()}
|
||||||
|
value={data.nested_format}
|
||||||
|
variant="outlined"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
select
|
||||||
|
>
|
||||||
|
<MenuItem value={1}>{LL.MQTT_NEST_1()}</MenuItem>
|
||||||
|
<MenuItem value={2}>{LL.MQTT_NEST_2()}</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
name="send_response"
|
||||||
|
checked={data.send_response}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={LL.MQTT_RESPONSE()}
|
||||||
|
/>
|
||||||
|
{!data.ha_enabled && (
|
||||||
|
<Grid container spacing={2} rowSpacing={0}>
|
||||||
|
<Grid>
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
name="publish_single"
|
||||||
|
checked={data.publish_single}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={LL.MQTT_PUBLISH_TEXT_1()}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
{data.publish_single && (
|
||||||
|
<Grid>
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
name="publish_single2cmd"
|
||||||
|
checked={data.publish_single2cmd}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={LL.MQTT_PUBLISH_TEXT_2()}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
{!data.publish_single && (
|
||||||
|
<Grid container spacing={2} rowSpacing={0}>
|
||||||
|
<Grid>
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
name="ha_enabled"
|
||||||
|
checked={data.ha_enabled}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={LL.MQTT_PUBLISH_TEXT_3()}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
{data.ha_enabled && (
|
||||||
|
<Grid container spacing={2} rowSpacing={0}>
|
||||||
|
<Grid>
|
||||||
|
<TextField
|
||||||
|
name="discovery_type"
|
||||||
|
label={LL.MQTT_PUBLISH_TEXT_5()}
|
||||||
|
value={data.discovery_type}
|
||||||
|
variant="outlined"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
select
|
||||||
|
>
|
||||||
|
<MenuItem value={0}>Home Assistant</MenuItem>
|
||||||
|
<MenuItem value={1}>Domoticz</MenuItem>
|
||||||
|
<MenuItem value={2}>Domoticz (latest)</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<TextField
|
||||||
|
name="discovery_prefix"
|
||||||
|
label={LL.MQTT_PUBLISH_TEXT_4()}
|
||||||
|
variant="outlined"
|
||||||
|
value={data.discovery_prefix}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<TextField
|
||||||
|
name="entity_format"
|
||||||
|
label={LL.MQTT_ENTITY_FORMAT()}
|
||||||
|
value={data.entity_format}
|
||||||
|
variant="outlined"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
select
|
||||||
|
>
|
||||||
|
<MenuItem value={0}>{LL.MQTT_ENTITY_FORMAT_0()}</MenuItem>
|
||||||
|
<MenuItem value={3}>
|
||||||
|
{LL.MQTT_ENTITY_FORMAT_1()} (v3.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>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
||||||
|
{LL.MQTT_PUBLISH_INTERVALS()} (0=auto)
|
||||||
|
</Typography>
|
||||||
|
<Grid container spacing={2} rowSpacing={0}>
|
||||||
|
<Grid>
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
|
name="publish_time_heartbeat"
|
||||||
|
label="Heartbeat"
|
||||||
|
slotProps={{
|
||||||
|
input: SecondsInputProps
|
||||||
|
}}
|
||||||
|
variant="outlined"
|
||||||
|
value={numberValue(data.publish_time_heartbeat)}
|
||||||
|
type="number"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<TextField
|
||||||
|
name="publish_time_boiler"
|
||||||
|
label={LL.MQTT_INT_BOILER()}
|
||||||
|
variant="outlined"
|
||||||
|
value={numberValue(data.publish_time_boiler)}
|
||||||
|
type="number"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
slotProps={{
|
||||||
|
input: SecondsInputProps
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<TextField
|
||||||
|
name="publish_time_thermostat"
|
||||||
|
label={LL.MQTT_INT_THERMOSTATS()}
|
||||||
|
variant="outlined"
|
||||||
|
value={numberValue(data.publish_time_thermostat)}
|
||||||
|
type="number"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
slotProps={{
|
||||||
|
input: SecondsInputProps
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<TextField
|
||||||
|
name="publish_time_solar"
|
||||||
|
label={LL.MQTT_INT_SOLAR()}
|
||||||
|
variant="outlined"
|
||||||
|
value={numberValue(data.publish_time_solar)}
|
||||||
|
type="number"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
slotProps={{
|
||||||
|
input: SecondsInputProps
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<TextField
|
||||||
|
name="publish_time_mixer"
|
||||||
|
label={LL.MQTT_INT_MIXER()}
|
||||||
|
variant="outlined"
|
||||||
|
value={numberValue(data.publish_time_mixer)}
|
||||||
|
type="number"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
slotProps={{
|
||||||
|
input: SecondsInputProps
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<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()}
|
||||||
|
variant="outlined"
|
||||||
|
value={numberValue(data.publish_time_sensor)}
|
||||||
|
type="number"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
slotProps={{
|
||||||
|
input: SecondsInputProps
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<TextField
|
||||||
|
name="publish_time_other"
|
||||||
|
label={LL.DEFAULT(0)}
|
||||||
|
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="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>
|
||||||
|
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||||
|
{content()}
|
||||||
|
</SectionContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MqttSettings;
|
||||||
154
interface/src/app/settings/NTPSettings.tsx
Normal file
154
interface/src/app/settings/NTPSettings.tsx
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
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 * 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,
|
||||||
|
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';
|
||||||
|
|
||||||
|
import { TIME_ZONES, selectedTimeZone, timeZoneSelectItems } from './TZ';
|
||||||
|
|
||||||
|
const NTPSettings = () => {
|
||||||
|
const {
|
||||||
|
loadData,
|
||||||
|
saving,
|
||||||
|
data,
|
||||||
|
updateDataValue,
|
||||||
|
origData,
|
||||||
|
dirtyFlags,
|
||||||
|
setDirtyFlags,
|
||||||
|
blocker,
|
||||||
|
saveData,
|
||||||
|
errorMessage
|
||||||
|
} = useRest<NTPSettingsType>({
|
||||||
|
read: NTPApi.readNTPSettings,
|
||||||
|
update: NTPApi.updateNTPSettings
|
||||||
|
});
|
||||||
|
|
||||||
|
const { LL } = useI18nContext();
|
||||||
|
useLayoutTitle(LL.SETTINGS_OF('NTP'));
|
||||||
|
|
||||||
|
const updateFormValue = updateValueDirty(
|
||||||
|
origData,
|
||||||
|
dirtyFlags,
|
||||||
|
setDirtyFlags,
|
||||||
|
updateDataValue
|
||||||
|
);
|
||||||
|
|
||||||
|
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
|
|
||||||
|
const content = () => {
|
||||||
|
if (!data) {
|
||||||
|
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateAndSubmit = async () => {
|
||||||
|
try {
|
||||||
|
setFieldErrors(undefined);
|
||||||
|
await validate(NTP_SETTINGS_VALIDATOR, data);
|
||||||
|
await saveData();
|
||||||
|
} catch (error) {
|
||||||
|
setFieldErrors(error as ValidateFieldsError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeTimeZone = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
updateFormValue(event);
|
||||||
|
void updateState(readNTPSettings(), (settings: NTPSettingsType) => ({
|
||||||
|
...settings,
|
||||||
|
tz_label: event.target.value,
|
||||||
|
tz_format: TIME_ZONES[event.target.value]
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
name="enabled"
|
||||||
|
checked={data.enabled}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={LL.ENABLE_NTP()}
|
||||||
|
/>
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
|
name="server"
|
||||||
|
label={LL.NTP_SERVER()}
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={data.server}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
|
name="tz_label"
|
||||||
|
label={LL.TIME_ZONE()}
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={selectedTimeZone(data.tz_label, data.tz_format)}
|
||||||
|
onChange={changeTimeZone}
|
||||||
|
margin="normal"
|
||||||
|
select
|
||||||
|
>
|
||||||
|
<MenuItem disabled>{LL.TIME_ZONE()}...</MenuItem>
|
||||||
|
{timeZoneSelectItems()}
|
||||||
|
</ValidatedTextField>
|
||||||
|
{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>
|
||||||
|
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||||
|
{content()}
|
||||||
|
</SectionContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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';
|
import { MenuItem } from '@mui/material';
|
||||||
|
|
||||||
type TimeZones = {
|
type TimeZones = Record<string, string>;
|
||||||
[name: string]: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const TIME_ZONES: TimeZones = {
|
export const TIME_ZONES: TimeZones = {
|
||||||
'Africa/Abidjan': 'GMT0',
|
'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;
|
||||||
408
interface/src/app/settings/network/NetworkSettings.tsx
Normal file
408
interface/src/app/settings/network/NetworkSettings.tsx
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
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';
|
||||||
|
import LockOpenIcon from '@mui/icons-material/LockOpen';
|
||||||
|
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
|
||||||
|
import WarningIcon from '@mui/icons-material/Warning';
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
IconButton,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemAvatar,
|
||||||
|
ListItemText,
|
||||||
|
MenuItem,
|
||||||
|
TextField,
|
||||||
|
Typography
|
||||||
|
} from '@mui/material';
|
||||||
|
|
||||||
|
import * as NetworkApi from 'api/network';
|
||||||
|
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
|
||||||
|
} from 'components';
|
||||||
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
import type { NetworkSettingsType } from 'types';
|
||||||
|
import { updateValueDirty, useRest } from 'utils';
|
||||||
|
import { validate } from 'validators';
|
||||||
|
import { createNetworkSettingsValidator } from 'validators/network';
|
||||||
|
|
||||||
|
import RestartMonitor from '../../status/RestartMonitor';
|
||||||
|
import { WiFiConnectionContext } from './WiFiConnectionContext';
|
||||||
|
import { isNetworkOpen, networkSecurityMode } from './WiFiNetworkSelector';
|
||||||
|
|
||||||
|
const NetworkSettings = () => {
|
||||||
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
|
const { selectedNetwork, deselectNetwork } = useContext(WiFiConnectionContext);
|
||||||
|
|
||||||
|
const [initialized, setInitialized] = useState(false);
|
||||||
|
const [restarting, setRestarting] = useState(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
loadData,
|
||||||
|
saving,
|
||||||
|
data,
|
||||||
|
updateDataValue,
|
||||||
|
origData,
|
||||||
|
dirtyFlags,
|
||||||
|
setDirtyFlags,
|
||||||
|
blocker,
|
||||||
|
saveData,
|
||||||
|
errorMessage,
|
||||||
|
restartNeeded
|
||||||
|
} = useRest<NetworkSettingsType>({
|
||||||
|
read: NetworkApi.readNetworkSettings,
|
||||||
|
update: NetworkApi.updateNetworkSettings
|
||||||
|
});
|
||||||
|
|
||||||
|
const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
|
||||||
|
immediate: false
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!initialized && data) {
|
||||||
|
if (selectedNetwork) {
|
||||||
|
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 [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
|
|
||||||
|
useEffect(() => deselectNetwork, [deselectNetwork]);
|
||||||
|
|
||||||
|
const content = () => {
|
||||||
|
if (!data) {
|
||||||
|
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateAndSubmit = async () => {
|
||||||
|
try {
|
||||||
|
setFieldErrors(undefined);
|
||||||
|
await validate(createNetworkSettingsValidator(data), data);
|
||||||
|
await saveData();
|
||||||
|
} catch (error) {
|
||||||
|
setFieldErrors(error as ValidateFieldsError);
|
||||||
|
}
|
||||||
|
deselectNetwork();
|
||||||
|
};
|
||||||
|
|
||||||
|
const setCancel = async () => {
|
||||||
|
deselectNetwork();
|
||||||
|
await loadData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const doRestart = async () => {
|
||||||
|
setRestarting(true);
|
||||||
|
await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch(
|
||||||
|
(error: Error) => {
|
||||||
|
toast.error(error.message);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Typography variant="h6" color="primary">
|
||||||
|
WiFi
|
||||||
|
</Typography>
|
||||||
|
{selectedNetwork ? (
|
||||||
|
<List>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<Avatar>
|
||||||
|
{isNetworkOpen(selectedNetwork) ? <LockOpenIcon /> : <LockIcon />}
|
||||||
|
</Avatar>
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText
|
||||||
|
primary={selectedNetwork.ssid}
|
||||||
|
secondary={
|
||||||
|
'Security: ' +
|
||||||
|
networkSecurityMode(selectedNetwork) +
|
||||||
|
', Ch: ' +
|
||||||
|
selectedNetwork.channel +
|
||||||
|
', bssid: ' +
|
||||||
|
selectedNetwork.bssid
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<IconButton onClick={setCancel}>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
) : (
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
|
name="ssid"
|
||||||
|
label={'SSID (' + LL.NETWORK_BLANK_SSID() + ')'}
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={data.ssid}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
|
name="bssid"
|
||||||
|
label={'BSSID (' + LL.NETWORK_BLANK_BSSID() + ')'}
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={data.bssid}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
{(!selectedNetwork || !isNetworkOpen(selectedNetwork)) && (
|
||||||
|
<ValidatedPasswordField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
|
name="password"
|
||||||
|
label={LL.PASSWORD()}
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={data.password}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<TextField
|
||||||
|
name="tx_power"
|
||||||
|
label={LL.TX_POWER()}
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={data.tx_power}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={LL.NETWORK_DISABLE_SLEEP()}
|
||||||
|
/>
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
name="bandwidth20"
|
||||||
|
checked={data.bandwidth20}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={LL.NETWORK_LOW_BAND()}
|
||||||
|
/>
|
||||||
|
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
||||||
|
{LL.GENERAL_OPTIONS()}
|
||||||
|
</Typography>
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
|
name="hostname"
|
||||||
|
label={LL.HOSTNAME()}
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={data.hostname}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
name="enableMDNS"
|
||||||
|
checked={data.enableMDNS}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={LL.NETWORK_USE_DNS()}
|
||||||
|
/>
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
name="enableCORS"
|
||||||
|
checked={data.enableCORS}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={LL.NETWORK_ENABLE_CORS()}
|
||||||
|
/>
|
||||||
|
{data.enableCORS && (
|
||||||
|
<TextField
|
||||||
|
name="CORSOrigin"
|
||||||
|
label={LL.NETWORK_CORS_ORIGIN()}
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={data.CORSOrigin}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
name="static_ip_config"
|
||||||
|
checked={data.static_ip_config}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={LL.NETWORK_FIXED_IP()}
|
||||||
|
/>
|
||||||
|
{data.static_ip_config && (
|
||||||
|
<>
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
|
name="local_ip"
|
||||||
|
label={LL.AP_LOCAL_IP()}
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={data.local_ip}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
|
name="gateway_ip"
|
||||||
|
label={LL.NETWORK_GATEWAY()}
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={data.gateway_ip}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
|
name="subnet_mask"
|
||||||
|
label={LL.NETWORK_SUBNET()}
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={data.subnet_mask}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
|
name="dns_ip_1"
|
||||||
|
label="DNS #1"
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={data.dns_ip_1}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
|
name="dns_ip_2"
|
||||||
|
label="DNS #2"
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={data.dns_ip_2}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{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 &&
|
||||||
|
(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>
|
||||||
|
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||||
|
{restarting ? <RestartMonitor /> : content()}
|
||||||
|
</SectionContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NetworkSettings;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createContext } from 'react';
|
import { createContext } from 'react';
|
||||||
import { WiFiNetwork } from '../../types';
|
|
||||||
|
import type { WiFiNetwork } from 'types';
|
||||||
|
|
||||||
export interface WiFiConnectionContextValue {
|
export interface WiFiConnectionContextValue {
|
||||||
selectedNetwork?: WiFiNetwork;
|
selectedNetwork?: WiFiNetwork;
|
||||||
@@ -8,4 +9,6 @@ export interface WiFiConnectionContextValue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const WiFiConnectionContextDefaultValue = {} as WiFiConnectionContextValue;
|
const WiFiConnectionContextDefaultValue = {} as WiFiConnectionContextValue;
|
||||||
export const WiFiConnectionContext = createContext(WiFiConnectionContextDefaultValue);
|
export const WiFiConnectionContext = createContext(
|
||||||
|
WiFiConnectionContextDefaultValue
|
||||||
|
);
|
||||||
78
interface/src/app/settings/network/WiFiNetworkScanner.tsx
Normal file
78
interface/src/app/settings/network/WiFiNetworkScanner.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import PermScanWifiIcon from '@mui/icons-material/PermScanWifi';
|
||||||
|
import { Button } from '@mui/material';
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
|
const NUM_POLLS = 10;
|
||||||
|
const POLLING_FREQUENCY = 1000;
|
||||||
|
|
||||||
|
const WiFiNetworkScanner = () => {
|
||||||
|
const pollCount = useRef(0);
|
||||||
|
const { LL } = useI18nContext();
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string>();
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
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) {
|
||||||
|
pollCount.current = completedPollCount;
|
||||||
|
setTimeout(getNetworkList, POLLING_FREQUENCY);
|
||||||
|
} else {
|
||||||
|
setErrorMessage(LL.PROBLEM_LOADING());
|
||||||
|
pollCount.current = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderNetworkScanner = () => {
|
||||||
|
if (!networkList) {
|
||||||
|
return (
|
||||||
|
<FormLoader message={LL.SCANNING() + '...'} errorMessage={errorMessage} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <WiFiNetworkSelector networkList={networkList} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionContent>
|
||||||
|
{renderNetworkScanner()}
|
||||||
|
<ButtonRow>
|
||||||
|
<Button
|
||||||
|
startIcon={<PermScanWifiIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
onClick={scanNetworks}
|
||||||
|
disabled={!errorMessage && !networkList}
|
||||||
|
>
|
||||||
|
{LL.SCAN_AGAIN()}…
|
||||||
|
</Button>
|
||||||
|
</ButtonRow>
|
||||||
|
</SectionContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WiFiNetworkScanner;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user