mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-08 08:49:52 +03:00
Compare commits
3626 Commits
v3.6.0
...
cd155ba680
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd155ba680 | ||
|
|
2a565dc677 | ||
|
|
9bf57c3e22 | ||
|
|
766281d8d2 | ||
|
|
bd128072c0 | ||
|
|
88e4ba7ecf | ||
|
|
96e5251050 | ||
|
|
187b163ffd | ||
|
|
9d04058984 | ||
|
|
12b06aa657 | ||
|
|
47b3e4bf00 | ||
|
|
f0f40bbcac | ||
|
|
036e2917a5 | ||
|
|
d294c418c1 | ||
|
|
40da7572cd | ||
|
|
532dc66282 | ||
|
|
8913f38fd0 | ||
|
|
11782eef8b | ||
|
|
b5f4eb6c62 | ||
|
|
ee4f58ce20 | ||
|
|
0fe0ee77b3 | ||
|
|
74e58aaa3d | ||
|
|
d39d6c7f1f | ||
|
|
4043eaf271 | ||
|
|
73ac60a8b2 | ||
|
|
1d6b283033 | ||
|
|
08ca4e44e8 | ||
|
|
255c173469 | ||
|
|
aeee318cca | ||
|
|
eb14e89c35 | ||
|
|
8411ea6773 | ||
|
|
015110a72e | ||
|
|
dae345f359 | ||
|
|
4cfd9b699c | ||
|
|
bd6371fd9d | ||
|
|
7507596869 | ||
|
|
5d99bd923b | ||
|
|
7402776248 | ||
|
|
8b5cc82df9 | ||
|
|
7c5351f15f | ||
|
|
b29f02e5dd | ||
|
|
e9e7162bcd | ||
|
|
b24aae9123 | ||
|
|
9dbd634322 | ||
|
|
daffc94c7f | ||
|
|
93de0e2f42 | ||
|
|
145172b6e9 | ||
|
|
c4a2f8bac8 | ||
|
|
0c0c928efc | ||
|
|
4d829b0b78 | ||
|
|
01e7d9b027 | ||
|
|
439da1d1e9 | ||
|
|
ac45c17204 | ||
|
|
cd24c7815b | ||
|
|
9665efbf38 | ||
|
|
d8aafdbfd4 | ||
|
|
0c6aef5b60 | ||
|
|
48a1bd0fe6 | ||
|
|
44cfffe8a4 | ||
|
|
e75bf8871e | ||
|
|
22703f4100 | ||
|
|
c066ab8400 | ||
|
|
4a0625e31c | ||
|
|
9aaaba5bb7 | ||
|
|
689a3a9a69 | ||
|
|
391a312f0c | ||
|
|
f782eac0cf | ||
|
|
d88513d789 | ||
|
|
59d07e81d6 | ||
|
|
419fe8ef5d | ||
|
|
4cfcba18ee | ||
|
|
b1d6ab3c96 | ||
|
|
ae26754bc8 | ||
|
|
61c3b47269 | ||
|
|
50bedb2b39 | ||
|
|
13db83a6de | ||
|
|
ec43a07866 | ||
|
|
fbc11b8ef8 | ||
|
|
f1c5a911f9 | ||
|
|
76c0aa6be8 | ||
|
|
61bf2332bb | ||
|
|
39ca956e1f | ||
|
|
0683b77437 | ||
|
|
5da2760dc6 | ||
|
|
3fabaf900f | ||
|
|
b2a8738672 | ||
|
|
c3b9c1ef98 | ||
|
|
aef6b6e92d | ||
|
|
dc46dac02a | ||
|
|
025c430611 | ||
|
|
995ab7233d | ||
|
|
1507989ca3 | ||
|
|
022e808b14 | ||
|
|
9b604e9c78 | ||
|
|
cd3cc09386 | ||
|
|
0df21a7843 | ||
|
|
9225ad2ad9 | ||
|
|
227b1ac59b | ||
|
|
a9a6e32dd1 | ||
|
|
3c4278029f | ||
|
|
3b4e09208e | ||
|
|
e9e0688737 | ||
|
|
7bb1b7bb91 | ||
|
|
4302bc9978 | ||
|
|
60d884df88 | ||
|
|
177c635bc1 | ||
|
|
9a97c28bf0 | ||
|
|
deb87cf5d7 | ||
|
|
a50227638b | ||
|
|
92b1515c8a | ||
|
|
0c5cf0475c | ||
|
|
f26e937514 | ||
|
|
1e4ca8b57f | ||
|
|
4d88bbd28f | ||
|
|
0ce110df9e | ||
|
|
3759fc81ba | ||
|
|
7965ecd856 | ||
|
|
7b0169bb68 | ||
|
|
f10f3d5305 | ||
|
|
ed9e2704b0 | ||
|
|
c47dd0e523 | ||
|
|
80c75bae77 | ||
|
|
cfa973b08b | ||
|
|
83987b71e0 | ||
|
|
a318f34988 | ||
|
|
5cc1660675 | ||
|
|
8a48da38b8 | ||
|
|
d514e67eb8 | ||
|
|
69964482f8 | ||
|
|
2aa691212c | ||
|
|
c27134f185 | ||
|
|
c8033692b1 | ||
|
|
c537d0ab8b | ||
|
|
bee703eb1f | ||
|
|
5d2bd6a2af | ||
|
|
67f0f40a8a | ||
|
|
e796fbef7a | ||
|
|
da7ef04741 | ||
|
|
ddb318dfc6 | ||
|
|
88643dc8e3 | ||
|
|
cf3854563d | ||
|
|
4b2468d616 | ||
|
|
4b08aba9c4 | ||
|
|
0a18add447 | ||
|
|
ca8d23ff3a | ||
|
|
c7e833194f | ||
|
|
f63f658421 | ||
|
|
13fcf09470 | ||
|
|
f560cbd60c | ||
|
|
38ead7e10f | ||
|
|
326bba9b42 | ||
|
|
d9a18bf255 | ||
|
|
6c42cbfb4b | ||
|
|
6691c81956 | ||
|
|
2f95ef305d | ||
|
|
7afde0ce6e | ||
|
|
f3cdafe7d0 | ||
|
|
4bf23e1bda | ||
|
|
aca66457f9 | ||
|
|
121887bdce | ||
|
|
32d7cf4e9c | ||
|
|
3f8227e95e | ||
|
|
10d84261da | ||
|
|
51848d8347 | ||
|
|
1c0669144f | ||
|
|
41a2ba6e5d | ||
|
|
b6fe9e7569 | ||
|
|
faa2c5f1aa | ||
|
|
c71034ff12 | ||
|
|
71be615bbe | ||
|
|
ce53fd1d04 | ||
|
|
1772876f9e | ||
|
|
cd5dbebea9 | ||
|
|
6c67b78a1c | ||
|
|
9b4deb271b | ||
|
|
0e9283af5c | ||
|
|
58011700fe | ||
|
|
079a08ff7b | ||
|
|
b64a55e460 | ||
|
|
2b7ef5b6ba | ||
|
|
d62eef4eca | ||
|
|
0f6d6e69f5 | ||
|
|
97f689b8a7 | ||
|
|
be9b4a070c | ||
|
|
bc15dd4463 | ||
|
|
1613caea86 | ||
|
|
4b39ab76ab | ||
|
|
d04c882590 | ||
|
|
a199bf21e1 | ||
|
|
3ae8722ece | ||
|
|
6a6cef57cf | ||
|
|
090491aab6 | ||
|
|
ca81a02a8c | ||
|
|
f64188bd5d | ||
|
|
d3e0f180c5 | ||
|
|
3f4d87a1d2 | ||
|
|
1d89e651a4 | ||
|
|
fad67b4ef9 | ||
|
|
ce1c22ee35 | ||
|
|
169b5f34ea | ||
|
|
131c03714f | ||
|
|
024c4a0c21 | ||
|
|
b0d111d86f | ||
|
|
faeef9c821 | ||
|
|
60a5b28a21 | ||
|
|
f4b5cf04a0 | ||
|
|
d69f26acac | ||
|
|
2a50701107 | ||
|
|
ab099c45b8 | ||
|
|
47f21019a0 | ||
|
|
5c473c2b3d | ||
|
|
9ddc587334 | ||
|
|
9b0a7a4872 | ||
|
|
97528e9df6 | ||
|
|
b797e1e2bb | ||
|
|
bb52d35c99 | ||
|
|
4c9026e11a | ||
|
|
6cd4e8a5b6 | ||
|
|
dfe037a7d3 | ||
|
|
b87622185d | ||
|
|
0318f8156d | ||
|
|
0c03fa1308 | ||
|
|
2d6e02171f | ||
|
|
2da312bf15 | ||
|
|
52f59a7b1d | ||
|
|
6c8624298c | ||
|
|
ce2d2fb867 | ||
|
|
ba2ad4e175 | ||
|
|
b3320c3e48 | ||
|
|
dc1094b6ba | ||
|
|
bb60568d83 | ||
|
|
67b5c5dd26 | ||
|
|
6866d5b7a9 | ||
|
|
b4e9af89ee | ||
|
|
775ed99b22 | ||
|
|
2ec13273fd | ||
|
|
a846b01103 | ||
|
|
6dffb08545 | ||
|
|
16f7cc148d | ||
|
|
568431ada4 | ||
|
|
6034c1e5eb | ||
|
|
e3566feefb | ||
|
|
ea46c79278 | ||
|
|
a02831e04e | ||
|
|
3b8c973f2a | ||
|
|
2c65936b3e | ||
|
|
bc04c34d58 | ||
|
|
5047f1752e | ||
|
|
1fa7a6c549 | ||
|
|
f9ebe33a7d | ||
|
|
e719dd963d | ||
|
|
c6b0099581 | ||
|
|
71726530c0 | ||
|
|
a749ecb298 | ||
|
|
7e963529c4 | ||
|
|
c76409cf3f | ||
|
|
efd0872690 | ||
|
|
57098b578f | ||
|
|
6472e9e224 | ||
|
|
026828efc7 | ||
|
|
fe6e9be4d3 | ||
|
|
7745a6f9a1 | ||
|
|
c523a379fe | ||
|
|
2854e9cbe9 | ||
|
|
4985f104c7 | ||
|
|
a0ea5f7ea1 | ||
|
|
efc35d0594 | ||
|
|
ccd6c6f8ad | ||
|
|
b426e0eb45 | ||
|
|
c53e1de569 | ||
|
|
8058e98748 | ||
|
|
ba419d09eb | ||
|
|
c701247652 | ||
|
|
12754d1c07 | ||
|
|
9a4daba31a | ||
|
|
a2aa2dccdd | ||
|
|
25af51a8e8 | ||
|
|
ddc597ff03 | ||
|
|
75310afd63 | ||
|
|
3095323c93 | ||
|
|
4667718f12 | ||
|
|
2cb1c5d7e7 | ||
|
|
d7b9754ddb | ||
|
|
99b769626e | ||
|
|
70035b059c | ||
|
|
b252c2f95a | ||
|
|
baa4f2eb39 | ||
|
|
c7b970b5b0 | ||
|
|
3ed5b65191 | ||
|
|
6e01c00d46 | ||
|
|
e72afc9065 | ||
|
|
de2f3e712d | ||
|
|
cacb92cd47 | ||
|
|
3eb20fa700 | ||
|
|
d35574d494 | ||
|
|
3cb29220ab | ||
|
|
d91812ab8f | ||
|
|
856f8efd25 | ||
|
|
d32378ccd4 | ||
|
|
f5006d1a11 | ||
|
|
c3468e6308 | ||
|
|
9f4de56099 | ||
|
|
78738de811 | ||
|
|
e64596ad61 | ||
|
|
024357ae80 | ||
|
|
3f07d3b75f | ||
|
|
cb7c695c67 | ||
|
|
912a764c2d | ||
|
|
c9c0f55b64 | ||
|
|
6991677cf9 | ||
|
|
83330907cd | ||
|
|
3571998da3 | ||
|
|
77465ebe81 | ||
|
|
006e5493e2 | ||
|
|
b29136433d | ||
|
|
0347a4b8b0 | ||
|
|
3330103a8d | ||
|
|
0a02252fee | ||
|
|
642f2116d2 | ||
|
|
7b8e45c2f7 | ||
|
|
0f2244607f | ||
|
|
32ab9dda45 | ||
|
|
b6659b8586 | ||
|
|
5eb85066ef | ||
|
|
9398fc72a0 | ||
|
|
1bcd453e3f | ||
|
|
d405478a13 | ||
|
|
c927e5f496 | ||
|
|
c5dbd7452e | ||
|
|
3a0b4ea587 | ||
|
|
6f4cdb7122 | ||
|
|
b5d6757660 | ||
|
|
0dcde46296 | ||
|
|
ac7e91beff | ||
|
|
9ea1e2752d | ||
|
|
b5471aef94 | ||
|
|
f31329ceff | ||
|
|
40d48f4407 | ||
|
|
11b7e1f86e | ||
|
|
e364a71eda | ||
|
|
e006bebb86 | ||
|
|
fa3d42a1c7 | ||
|
|
e435fd4391 | ||
|
|
d42efb32ab | ||
|
|
ad8f2dc823 | ||
|
|
f2ff14f511 | ||
|
|
e5e9d4c713 | ||
|
|
4c5f93000b | ||
|
|
0c7301a020 | ||
|
|
74c63bf17e | ||
|
|
4564e0b828 | ||
|
|
2ab9607989 | ||
|
|
29d198b46d | ||
|
|
8387ca0c07 | ||
|
|
3796fb8027 | ||
|
|
1da08633ec | ||
|
|
b097e372e4 | ||
|
|
846776929e | ||
|
|
6e1d56b8ee | ||
|
|
32f7eb7299 | ||
|
|
3fc9c3b56c | ||
|
|
e2e46543d2 | ||
|
|
a2b22198ec | ||
|
|
642b59f729 | ||
|
|
9e81de2164 | ||
|
|
d44797db1d | ||
|
|
673ee3f79b | ||
|
|
38a8179544 | ||
|
|
cfb59ac6a0 | ||
|
|
80c26e1adb | ||
|
|
d0de6e8d0f | ||
|
|
68be7d00ff | ||
|
|
d3e6043911 | ||
|
|
373895b36a | ||
|
|
594e10dbe1 | ||
|
|
e3c5b462da | ||
|
|
8a1376b169 | ||
|
|
900e26cf9f | ||
|
|
071e81f29b | ||
|
|
d1bd861ff0 | ||
|
|
f5925dbb3b | ||
|
|
b1eedcb1d8 | ||
|
|
ed7a9f43de | ||
|
|
67885950ef | ||
|
|
74ddb771e9 | ||
|
|
584d0e0b48 | ||
|
|
8d9ca33ea3 | ||
|
|
f0eea1a6a3 | ||
|
|
79cc0377c0 | ||
|
|
115cec08fa | ||
|
|
6df592c2b8 | ||
|
|
f2b81489ba | ||
|
|
0d8760219a | ||
|
|
e8e8d9c130 | ||
|
|
8f712412f5 | ||
|
|
f8ece46163 | ||
|
|
cd8b1add54 | ||
|
|
1c415a9715 | ||
|
|
e1e3601640 | ||
|
|
c2f718b49a | ||
|
|
cee1874689 | ||
|
|
57d172aac2 | ||
|
|
bb26900213 | ||
|
|
9d1ee27533 | ||
|
|
7fd735f667 | ||
|
|
2e79c3a5c6 | ||
|
|
d769999f10 | ||
|
|
bd93d26361 | ||
|
|
72feefe709 | ||
|
|
1d6c2c9664 | ||
|
|
85c78bc8e9 | ||
|
|
18bdcfe050 | ||
|
|
a6f77250b5 | ||
|
|
9874ecde82 | ||
|
|
584b8788be | ||
|
|
7eb15652c7 | ||
|
|
2c28a607ba | ||
|
|
45eca462e7 | ||
|
|
4474868afc | ||
|
|
6c6b5b060d | ||
|
|
c03eb290d1 | ||
|
|
de9bd44071 | ||
|
|
b7458b0686 | ||
|
|
a660ec1afa | ||
|
|
c4f6f01f7e | ||
|
|
9f60560f2b | ||
|
|
7b50f80cb8 | ||
|
|
656d275c56 | ||
|
|
eca17f2b2c | ||
|
|
7f60279aa3 | ||
|
|
5227fafa1b | ||
|
|
4991e2b7cd | ||
|
|
34c514709a | ||
|
|
7dfedfeb10 | ||
|
|
c8bf4cae17 | ||
|
|
2818e268b6 | ||
|
|
7a95c11f62 | ||
|
|
d712b1cce9 | ||
|
|
03fa92352b | ||
|
|
e121fdb47f | ||
|
|
c05793f64f | ||
|
|
3836610d81 | ||
|
|
c3f87cd321 | ||
|
|
4d5a27f45e | ||
|
|
c37c1aaad5 | ||
|
|
110c0df6fb | ||
|
|
04ac3be242 | ||
|
|
44c4ee8bc0 | ||
|
|
57c4d550a3 | ||
|
|
4bcb95eece | ||
|
|
0a92d455c8 | ||
|
|
edb30931ae | ||
|
|
d03ab7a16f | ||
|
|
ea9b6b3e00 | ||
|
|
bae6b600bd | ||
|
|
7eac920985 | ||
|
|
00c2b5992c | ||
|
|
43d2fa1f00 | ||
|
|
92d40d9287 | ||
|
|
7a47a2090f | ||
|
|
0d98491a97 | ||
|
|
16cf16616e | ||
|
|
d9f56ef3ae | ||
|
|
c8934af9bb | ||
|
|
cbacaa98d9 | ||
|
|
f1b6a0baf3 | ||
|
|
4d15f48e2b | ||
|
|
2ffd00e28a | ||
|
|
8ef5be9a7f | ||
|
|
8769faeb83 | ||
|
|
cefbf2d4d6 | ||
|
|
bfd5082054 | ||
|
|
3d3a634d94 | ||
|
|
1cb078cd3c | ||
|
|
22683f5d12 | ||
|
|
57b42aa7c2 | ||
|
|
4a59743024 | ||
|
|
66cec18dee | ||
|
|
ad71938fde | ||
|
|
d4155d6e9e | ||
|
|
af6be4c6b1 | ||
|
|
cfbd0168c3 | ||
|
|
61b9bd7581 | ||
|
|
4c7ad7124e | ||
|
|
0413314cfb | ||
|
|
ab7cbe8ead | ||
|
|
d1264828eb | ||
|
|
76a317b5c0 | ||
|
|
ec11ae2ef7 | ||
|
|
b857c6eb44 | ||
|
|
3f8add73ac | ||
|
|
08b0ddbb7f | ||
|
|
c44cb7e7fd | ||
|
|
77ff61046e | ||
|
|
a8eb06bef2 | ||
|
|
566b5c8ea5 | ||
|
|
eeccd076a0 | ||
|
|
f45ac9d0ef | ||
|
|
cdd9acddfa | ||
|
|
e484f11d12 | ||
|
|
19572f313a | ||
|
|
af9ad5d624 | ||
|
|
8adca69140 | ||
|
|
01710316ed | ||
|
|
ab80c82a22 | ||
|
|
dceafe65a7 | ||
|
|
4734a81fdb | ||
|
|
a2c099e615 | ||
|
|
f9b88a1b6b | ||
|
|
7f2b8cc971 | ||
|
|
582fb3d72f | ||
|
|
bf4fa74742 | ||
|
|
c62b3b9864 | ||
|
|
0ae9795d6b | ||
|
|
7488c31cd3 | ||
|
|
dc0e634004 | ||
|
|
57e7c0ae4f | ||
|
|
7214acfa20 | ||
|
|
ff6d47bb9c | ||
|
|
81971ba53f | ||
|
|
a58f37e3eb | ||
|
|
09a746cf8e | ||
|
|
ea70119138 | ||
|
|
5a9c5b5e2d | ||
|
|
7fb09c5045 | ||
|
|
705171f305 | ||
|
|
400d1a5f1a | ||
|
|
c4855cc5f2 | ||
|
|
1e4b487299 | ||
|
|
cbd883103e | ||
|
|
2ce12943cd | ||
|
|
4151a82b3b | ||
|
|
b871081ef1 | ||
|
|
da51d1d7d9 | ||
|
|
82dae30224 | ||
|
|
1fdac2fdab | ||
|
|
56b23e27d7 | ||
|
|
ba29aa62d3 | ||
|
|
be2342285f | ||
|
|
5595c01221 | ||
|
|
133cddef5b | ||
|
|
0fdba1f84d | ||
|
|
e10ec26e79 | ||
|
|
ffbb397dba | ||
|
|
799076d0c4 | ||
|
|
1fbd10df27 | ||
|
|
4a2f82f1e8 | ||
|
|
d913e4d90b | ||
|
|
0958c29c9e | ||
|
|
36e1c9f79d | ||
|
|
8fedac53dd | ||
|
|
a1c6159fc5 | ||
|
|
5b33acba5e | ||
|
|
4abaef2943 | ||
|
|
ad71773293 | ||
|
|
5b07309939 | ||
|
|
7a044a1dcd | ||
|
|
6507764157 | ||
|
|
7dfa8fc883 | ||
|
|
6df7965bb2 | ||
|
|
02a3dee764 | ||
|
|
d5895f1710 | ||
|
|
2b90ad3f6d | ||
|
|
0bb61b4296 | ||
|
|
2378fb547c | ||
|
|
a79ff3f417 | ||
|
|
4bc93615c5 | ||
|
|
685f0d93e5 | ||
|
|
1d7b6674bb | ||
|
|
014405e451 | ||
|
|
aff3ca3ad3 | ||
|
|
b723d09952 | ||
|
|
bfff842c82 | ||
|
|
ad7d21764d | ||
|
|
f2ae84b004 | ||
|
|
a81956654e | ||
|
|
3cb662799f | ||
|
|
9188d03d61 | ||
|
|
3df2d36453 | ||
|
|
0ab7eb42e4 | ||
|
|
84d4fb37fa | ||
|
|
aa9b38da03 | ||
|
|
8342867807 | ||
|
|
d8cff865da | ||
|
|
096f7e1c88 | ||
|
|
0608d847f5 | ||
|
|
b20360c2a5 | ||
|
|
59b5086cab | ||
|
|
2e3024ab61 | ||
|
|
b5299719da | ||
|
|
2620f56e0d | ||
|
|
20b978c46c | ||
|
|
5c8a18df68 | ||
|
|
3464d6c324 | ||
|
|
eac0cc0521 | ||
|
|
0953d37303 | ||
|
|
1d3fec2a95 | ||
|
|
61b374b7c0 | ||
|
|
c47cc0d5f1 | ||
|
|
939882efbf | ||
|
|
f42cbf548e | ||
|
|
91075ace37 | ||
|
|
de6405f8d1 | ||
|
|
2ffcaf4a9e | ||
|
|
1bda62309b | ||
|
|
83659e5da8 | ||
|
|
a6e136561e | ||
|
|
a75d7487fc | ||
|
|
31b0dd8d58 | ||
|
|
696bd1f455 | ||
|
|
18355efde2 | ||
|
|
d5100134e4 | ||
|
|
b932242e04 | ||
|
|
73ccff3412 | ||
|
|
e5f852a7ed | ||
|
|
581f19462d | ||
|
|
eb59b37251 | ||
|
|
f3696f60cd | ||
|
|
6b4e21f5db | ||
|
|
3d4d5b7bbc | ||
|
|
e6f15681c0 | ||
|
|
5f52a646ff | ||
|
|
be4f9296a5 | ||
|
|
c3181f589c | ||
|
|
872cd40f56 | ||
|
|
30b9de49bf | ||
|
|
8a91c6eb2f | ||
|
|
243471e21d | ||
|
|
b8f97ec94d | ||
|
|
8576a6f253 | ||
|
|
b33e6ceca9 | ||
|
|
8eaf7f32cd | ||
|
|
becdc8cef5 | ||
|
|
651688219c | ||
|
|
b9a4bb3511 | ||
|
|
b318274129 | ||
|
|
8b0e5ba8e7 | ||
|
|
4a9b74b311 | ||
|
|
371b198eb6 | ||
|
|
a6dfdb2c4c | ||
|
|
3122c2b2a9 | ||
|
|
9ac8d149fb | ||
|
|
91e1b0b3b8 | ||
|
|
01636ced88 | ||
|
|
53e587537f | ||
|
|
77eeacf121 | ||
|
|
a89c42d659 | ||
|
|
ba4bc423f4 | ||
|
|
8cd341576d | ||
|
|
006eae5862 | ||
|
|
6e29de4463 | ||
|
|
92d816b990 | ||
|
|
37ad1968b5 | ||
|
|
01793dd4f6 | ||
|
|
9a7f7fa1d5 | ||
|
|
648675d002 | ||
|
|
462d865fc9 | ||
|
|
9f24851948 | ||
|
|
a5e5ec5098 | ||
|
|
db90546bc3 | ||
|
|
2d9ea3ee8d | ||
|
|
4d3cafcf29 | ||
|
|
8685ffb1bf | ||
|
|
86408b3452 | ||
|
|
eab94f3b84 | ||
|
|
9123dbcc9e | ||
|
|
ec6f426b06 | ||
|
|
5482937332 | ||
|
|
0b667703c2 | ||
|
|
c732c96fc2 | ||
|
|
e3d260429c | ||
|
|
77eb2c747b | ||
|
|
6853cd738f | ||
|
|
d58776beab | ||
|
|
94a7b1e438 | ||
|
|
c3f7540f74 | ||
|
|
4642a50f69 | ||
|
|
b23bcf3f0b | ||
|
|
570678e3d3 | ||
|
|
64a2f5eb11 | ||
|
|
24fba8b382 | ||
|
|
9339ef481a | ||
|
|
075789b902 | ||
|
|
1dd1b47faf | ||
|
|
525a164c69 | ||
|
|
b60f333edb | ||
|
|
2323fdfe56 | ||
|
|
9b7fed4d1f | ||
|
|
67c59c9b4b | ||
|
|
b5fea921e6 | ||
|
|
eeb071afc6 | ||
|
|
5669873101 | ||
|
|
d371c9bc82 | ||
|
|
153dd19fc6 | ||
|
|
ae258a75d9 | ||
|
|
37c4be321f | ||
|
|
c810d58064 | ||
|
|
5a27817d11 | ||
|
|
4be2f9283d | ||
|
|
a65162fbbc | ||
|
|
494cf3b6a8 | ||
|
|
b900194402 | ||
|
|
d6e72e72d7 | ||
|
|
960baadeca | ||
|
|
ed92b37869 | ||
|
|
1598809815 | ||
|
|
3131969fc1 | ||
|
|
fb44bc33b9 | ||
|
|
59a806ac8c | ||
|
|
6a2a27e47e | ||
|
|
251d0af028 | ||
|
|
8fa800e2f7 | ||
|
|
df9d20ad88 | ||
|
|
c07754047d | ||
|
|
9c00af317e | ||
|
|
d3d132ec45 | ||
|
|
7adba972e7 | ||
|
|
d83399bd1f | ||
|
|
4471da4aa9 | ||
|
|
b3be1d9351 | ||
|
|
c6b8c2a630 | ||
|
|
3f51c21dc7 | ||
|
|
a8cdbc4fd6 | ||
|
|
95e5babb13 | ||
|
|
74cb23a8bb | ||
|
|
6be304f295 | ||
|
|
e702b0b733 | ||
|
|
bfbd263c74 | ||
|
|
472f922369 | ||
|
|
b13c608ff3 | ||
|
|
5464909121 | ||
|
|
d874b3f808 | ||
|
|
92108bc743 | ||
|
|
66ca1e52bb | ||
|
|
3589094d06 | ||
|
|
801ed6ef79 | ||
|
|
2547ae45a8 | ||
|
|
93e4abe72d | ||
|
|
eb87651c47 | ||
|
|
4138598db2 | ||
|
|
913bbd6e3b | ||
|
|
d35881b05b | ||
|
|
33b54ccf12 | ||
|
|
a8775b2200 | ||
|
|
6d3746222d | ||
|
|
fe169ac80f | ||
|
|
83724e3d44 | ||
|
|
a4db3ef5c4 | ||
|
|
f8adad7865 | ||
|
|
12c094228e | ||
|
|
902ea80807 | ||
|
|
73831d9ac6 | ||
|
|
6bfda79441 | ||
|
|
112de78fc5 | ||
|
|
adbd2381e1 | ||
|
|
397f3f546e | ||
|
|
43d3c28e16 | ||
|
|
747a64b869 | ||
|
|
7c2e5560bd | ||
|
|
9194db9f70 | ||
|
|
fece00c0c6 | ||
|
|
ba3ae5ea56 | ||
|
|
4c69c9e445 | ||
|
|
97925c47fd | ||
|
|
ad89fe15b1 | ||
|
|
29035cabfe | ||
|
|
9b3d43d27f | ||
|
|
0b4f17473a | ||
|
|
b0c29b57c7 | ||
|
|
6d22f6aebf | ||
|
|
3e3e10e6a0 | ||
|
|
b854c777c8 | ||
|
|
b71fdd77e8 | ||
|
|
0f6f7cea19 | ||
|
|
58beb092c2 | ||
|
|
c3f200f73b | ||
|
|
98640c11b1 | ||
|
|
d7904bdcaf | ||
|
|
ce05a94d58 | ||
|
|
c0ed62dc7a | ||
|
|
1557fa98b1 | ||
|
|
81a530f153 | ||
|
|
a4733c3e6a | ||
|
|
7851b8e94c | ||
|
|
36838f7690 | ||
|
|
fdd87d0757 | ||
|
|
c8822aff64 | ||
|
|
4b3205fc9c | ||
|
|
afc05ae9e8 | ||
|
|
9c3044efa0 | ||
|
|
8caeb129c1 | ||
|
|
0427504f0e | ||
|
|
4f11a7caa1 | ||
|
|
150695c185 | ||
|
|
3558591480 | ||
|
|
f7a24052c2 | ||
|
|
75c452486c | ||
|
|
40cab6775c | ||
|
|
392829c7db | ||
|
|
4621c9d616 | ||
|
|
006d664ec9 | ||
|
|
0cc9ac4dd8 | ||
|
|
502096dc22 | ||
|
|
8c424c7a64 | ||
|
|
d7c118b88a | ||
|
|
1fdb0b7516 | ||
|
|
c2fc771756 | ||
|
|
6f759c5bc4 | ||
|
|
facbbf1353 | ||
|
|
a218c7a781 | ||
|
|
5f42709eab | ||
|
|
5ec0f657a0 | ||
|
|
812911ffbb | ||
|
|
55235687ba | ||
|
|
a970009d20 | ||
|
|
4afc16e2cb | ||
|
|
3772d72b43 | ||
|
|
607f949638 | ||
|
|
473cf7c8af | ||
|
|
e0909df06c | ||
|
|
5fc606ef6d | ||
|
|
a3032f4da7 | ||
|
|
7fdd65e8ca | ||
|
|
dc6bf883f1 | ||
|
|
ce5edd93b4 | ||
|
|
e36e6bec9c | ||
|
|
c8fd08b6d2 | ||
|
|
c0d693c1c8 | ||
|
|
5d2a6e2898 | ||
|
|
4547a5ceb0 | ||
|
|
76151c4395 | ||
|
|
481089b1b4 | ||
|
|
463787b7f4 | ||
|
|
e2258a1c43 | ||
|
|
5528f29b6a | ||
|
|
e3861d54c9 | ||
|
|
3368f2803c | ||
|
|
6ca1d68d23 | ||
|
|
f3873e330d | ||
|
|
be0aba1b59 | ||
|
|
321187d0d2 | ||
|
|
1e5bc9dcc5 | ||
|
|
f769bbbe42 | ||
|
|
cf93081252 | ||
|
|
30fca2a190 | ||
|
|
b7654c7e33 | ||
|
|
a3964102c9 | ||
|
|
727d9bdf2e | ||
|
|
4471c1fad6 | ||
|
|
4de56af85a | ||
|
|
b11f9be5e7 | ||
|
|
272bb84a16 | ||
|
|
c0bffe4ccf | ||
|
|
92c32b1abb | ||
|
|
35cf87d155 | ||
|
|
4c7b8a1db1 | ||
|
|
29990001b2 | ||
|
|
add9d12aca | ||
|
|
a5b1742bca | ||
|
|
676b164ce8 | ||
|
|
bb9315777a | ||
|
|
f11b9ee420 | ||
|
|
3ba0bb80e7 | ||
|
|
0850737208 | ||
|
|
c851b14d35 | ||
|
|
e62bb101e2 | ||
|
|
4a0680ee4d | ||
|
|
89ccaf1e26 | ||
|
|
eeef5a862a | ||
|
|
558f43813b | ||
|
|
c59657ac3b | ||
|
|
381e8de617 | ||
|
|
1ebbdbe557 | ||
|
|
2751aca37a | ||
|
|
acebc03d69 | ||
|
|
a7a3207cca | ||
|
|
f5acf6c0a4 | ||
|
|
d47b3b9276 | ||
|
|
5c60ea9c15 | ||
|
|
aec887373e | ||
|
|
4e5ad82cff | ||
|
|
7971d3bb93 | ||
|
|
549f7e30db | ||
|
|
a5879d1995 | ||
|
|
9e5d78a29c | ||
|
|
0191a95416 | ||
|
|
2276490c67 | ||
|
|
d60e42a483 | ||
|
|
7b5b5bb5ee | ||
|
|
aef4c2245d | ||
|
|
62f460163f | ||
|
|
7ddc2ccdad | ||
|
|
2437976aed | ||
|
|
b1b8a128d5 | ||
|
|
b9a0744433 | ||
|
|
7049aa8892 | ||
|
|
dbd6d38176 | ||
|
|
52b9e4650e | ||
|
|
4a3cb8c525 | ||
|
|
3680fad909 | ||
|
|
c80ec368a9 | ||
|
|
9fbb28ee33 | ||
|
|
92ff32f8ba | ||
|
|
e974967903 | ||
|
|
3752247d1c | ||
|
|
82e470a425 | ||
|
|
a67de57249 | ||
|
|
9157da3e0b | ||
|
|
fe5cab66fb | ||
|
|
3402b6d2dd | ||
|
|
1255381420 | ||
|
|
a8f17ec1b3 | ||
|
|
05904d9de9 | ||
|
|
89ade2e107 | ||
|
|
ca5d631dfc | ||
|
|
de7bd38850 | ||
|
|
aea9a4429d | ||
|
|
9f3d3c7a44 | ||
|
|
d7b59c2613 | ||
|
|
269c68b945 | ||
|
|
fc32eb7f12 | ||
|
|
56efb15938 | ||
|
|
53c1f4ebb6 | ||
|
|
0b1f87b6ce | ||
|
|
6371a0d298 | ||
|
|
757044dde9 | ||
|
|
0abdd9e2c2 | ||
|
|
b7548476ed | ||
|
|
0fee98c4c2 | ||
|
|
e3354b2a49 | ||
|
|
d06ea9590e | ||
|
|
e52f988261 | ||
|
|
a6b89cee58 | ||
|
|
f840543f0f | ||
|
|
c696974644 | ||
|
|
3ebb0225c8 | ||
|
|
2508a30b79 | ||
|
|
056febda6c | ||
|
|
28c523af24 | ||
|
|
09b8dcdb10 | ||
|
|
3daf1145e0 | ||
|
|
da77d52585 | ||
|
|
c628b4b5ac | ||
|
|
eea32f3027 | ||
|
|
92e5433165 | ||
|
|
af41979fd1 | ||
|
|
fd63428e6c | ||
|
|
a82b32ce97 | ||
|
|
c3ee1991a9 | ||
|
|
1f04c1fc2c | ||
|
|
f071fd842b | ||
|
|
b924d07447 | ||
|
|
87de5d1670 | ||
|
|
4173d4d42a | ||
|
|
402f5600d3 | ||
|
|
268c582a86 | ||
|
|
346bcb977f | ||
|
|
4cf251f613 | ||
|
|
1db093e6f4 | ||
|
|
10ee8ef120 | ||
|
|
b8a7c2750c | ||
|
|
654c9c39ae | ||
|
|
c365055825 | ||
|
|
7a6670db0d | ||
|
|
0a46bcc14f | ||
|
|
ef9d889e3d | ||
|
|
a33d116bf2 | ||
|
|
fa4dcdcc6b | ||
|
|
5f11bb3bb0 | ||
|
|
fdc3b19170 | ||
|
|
e15f9dec5a | ||
|
|
f8c279edb0 | ||
|
|
bf21097745 | ||
|
|
b6d2ab6adb | ||
|
|
1811558069 | ||
|
|
d899b58284 | ||
|
|
9e8faaab64 | ||
|
|
5b1266a638 | ||
|
|
52552de300 | ||
|
|
11fd81f25f | ||
|
|
a8b93dd571 | ||
|
|
9b91faba10 | ||
|
|
9b4ea2af94 | ||
|
|
c5a5b3b20c | ||
|
|
68fceaab87 | ||
|
|
6b3cb00917 | ||
|
|
00beb2a039 | ||
|
|
0927a2b44b | ||
|
|
b00f5dd8c0 | ||
|
|
367260b143 | ||
|
|
38efb0de67 | ||
|
|
df92e9253f | ||
|
|
afc2afbac0 | ||
|
|
7b4cd5c4d1 | ||
|
|
06fa541931 | ||
|
|
7f5ab93644 | ||
|
|
c2e90e2710 | ||
|
|
fdf7ab1aa7 | ||
|
|
e7334a2492 | ||
|
|
847c3ac2cd | ||
|
|
cce5c0d09e | ||
|
|
77330c99f4 | ||
|
|
a4c07b8472 | ||
|
|
5dd644fe1c | ||
|
|
fc53917512 | ||
|
|
8284627b1d | ||
|
|
f6f0c326ef | ||
|
|
2780a5f148 | ||
|
|
4785f78f28 | ||
|
|
3720ac4e2f | ||
|
|
5d3695f72c | ||
|
|
62dcfb3244 | ||
|
|
64de164e9d | ||
|
|
aa065aa24e | ||
|
|
bba0b76e3f | ||
|
|
2623178a00 | ||
|
|
6ab3ce8b02 | ||
|
|
b193b45f1f | ||
|
|
324f9d0686 | ||
|
|
e74af57c16 | ||
|
|
74b935c27f | ||
|
|
f6964e360b | ||
|
|
e9441c40aa | ||
|
|
9da73ba04f | ||
|
|
2cd60883d2 | ||
|
|
6a0a3ea440 | ||
|
|
4a71059c7c | ||
|
|
bed0f3674b | ||
|
|
7837bc1e30 | ||
|
|
81d42c21e5 | ||
|
|
5777d7cb0a | ||
|
|
9689f0a7da | ||
|
|
64a1e00501 | ||
|
|
fa630b9e64 | ||
|
|
761ee9e7d0 | ||
|
|
000a73156a | ||
|
|
086aef6ef2 | ||
|
|
94f822d39f | ||
|
|
ea4c1f92d3 | ||
|
|
eb9e72e257 | ||
|
|
1d8e32ec74 | ||
|
|
6cf4985a04 | ||
|
|
3c44f89804 | ||
|
|
993fa9fead | ||
|
|
bab44d58ff | ||
|
|
5bfb65fa91 | ||
|
|
197f943d63 | ||
|
|
b6f932e019 | ||
|
|
6bbd7d6333 | ||
|
|
98708f2293 | ||
|
|
df75e86bf4 | ||
|
|
92041d5d3f | ||
|
|
ed9e7f9e2f | ||
|
|
f9062463fd | ||
|
|
88b39be386 | ||
|
|
5f13fcf744 | ||
|
|
e6e0e9700f | ||
|
|
1e1dfccbee | ||
|
|
628f845b16 | ||
|
|
2def5a237a | ||
|
|
d6e66982fd | ||
|
|
a88f9070b9 | ||
|
|
e430ecf85c | ||
|
|
13dfc87c57 | ||
|
|
60ac6aa4ba | ||
|
|
5a37517f08 | ||
|
|
30b7b6db34 | ||
|
|
2df67c2d64 | ||
|
|
fd81dc9f5b | ||
|
|
ef3862fff0 | ||
|
|
06bb5324f2 | ||
|
|
38abe96fe1 | ||
|
|
5f1fa3f59b | ||
|
|
ba0f77e41b | ||
|
|
e28794c77a | ||
|
|
3dd67acc8d | ||
|
|
971e2869a2 | ||
|
|
83a49c1814 | ||
|
|
11e398ad49 | ||
|
|
4037e1db5a | ||
|
|
8bf97c067f | ||
|
|
f23e92bd2e | ||
|
|
c8d22f72b3 | ||
|
|
4514ae2210 | ||
|
|
52adf6749e | ||
|
|
47599e632a | ||
|
|
d527f4662d | ||
|
|
29016b6342 | ||
|
|
87210f947c | ||
|
|
fcf8432325 | ||
|
|
a41ded6673 | ||
|
|
7598290c5f | ||
|
|
eed676f521 | ||
|
|
6e9bc2d47a | ||
|
|
21acd900e5 | ||
|
|
48ab1abf39 | ||
|
|
b6053e9a7c | ||
|
|
65e0ae174a | ||
|
|
b2ffe8fe49 | ||
|
|
19a3632c04 | ||
|
|
5325904050 | ||
|
|
944f94997b | ||
|
|
1bb7d2f150 | ||
|
|
20455fdc00 | ||
|
|
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 | ||
|
|
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 | ||
|
|
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 | ||
|
|
ba21293c42 | ||
|
|
8574b44d4e | ||
|
|
76901d97d2 | ||
|
|
1cbfc91912 | ||
|
|
7d6bb6b9c8 | ||
|
|
c81f579464 | ||
|
|
2a6fedc6b3 | ||
|
|
e3a7e9fe33 | ||
|
|
8898ec9419 | ||
|
|
548fdd823b | ||
|
|
b60127c3ba | ||
|
|
095255b9b7 | ||
|
|
2b486ffa36 | ||
|
|
c3f9d9ddd6 | ||
|
|
740f3b4ef7 | ||
|
|
932a496f47 | ||
|
|
d36246d4d1 | ||
|
|
19094d47aa | ||
|
|
24cca67625 | ||
|
|
41443d4efe | ||
|
|
6e08356ff7 | ||
|
|
751410ca58 | ||
|
|
60beeddb66 | ||
|
|
c61c34f10e | ||
|
|
96a04da1ff | ||
|
|
bd8472b34e | ||
|
|
09228e4637 | ||
|
|
40a79c51ce | ||
|
|
01a15a5c85 | ||
|
|
1e15f65b0d | ||
|
|
0818728c25 | ||
|
|
2edfe0f42c | ||
|
|
a2eb8dfe83 | ||
|
|
4c60545057 | ||
|
|
d06b3285bd | ||
|
|
4dcfe8e0f6 | ||
|
|
42e679d5ba | ||
|
|
4db1c7dfca | ||
|
|
64fb84dd54 | ||
|
|
5ad03facce | ||
|
|
ad3bf294d8 | ||
|
|
6dba452aea | ||
|
|
cff5b8c949 | ||
|
|
aed45968db | ||
|
|
2de855e044 | ||
|
|
dfd9fe4d01 | ||
|
|
049f956131 | ||
|
|
b503e2bb90 | ||
|
|
b300181d33 | ||
|
|
905b52e39d | ||
|
|
8fcfb3d8f7 | ||
|
|
a17a9b71a2 | ||
|
|
64d46fe8f7 | ||
|
|
7962af0872 | ||
|
|
873b75240c | ||
|
|
9e405c5a5a | ||
|
|
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 | ||
|
|
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 | ||
|
|
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 | ||
|
|
b1f2273bb5 | ||
|
|
e9cf3f5ab5 |
132
.github/CODE_OF_CONDUCT.md
vendored
Normal file
132
.github/CODE_OF_CONDUCT.md
vendored
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
|
||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, religion, or sexual identity
|
||||||
|
and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our
|
||||||
|
community include:
|
||||||
|
|
||||||
|
* Demonstrating empathy and kindness toward other people
|
||||||
|
* Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
* Giving and gracefully accepting constructive feedback
|
||||||
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
|
and learning from the experience
|
||||||
|
* Focusing on what is best not just for us as individuals, but for the
|
||||||
|
overall community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery, and sexual attention or
|
||||||
|
advances of any kind
|
||||||
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or email
|
||||||
|
address, without their explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
|
decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
Examples of representing our community include using an official e-mail address,
|
||||||
|
posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the community leaders responsible for enforcement.
|
||||||
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series
|
||||||
|
of actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or
|
||||||
|
permanent ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within
|
||||||
|
the community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 2.0, available at
|
||||||
|
[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by
|
||||||
|
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available
|
||||||
|
at [https://www.contributor-covenant.org/translations][translations].
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
|
||||||
|
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||||
|
[FAQ]: https://www.contributor-covenant.org/faq
|
||||||
|
[translations]: https://www.contributor-covenant.org/translations
|
||||||
30
.github/ISSUE_TEMPLATE/bug_report.md
vendored
30
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,32 +1,32 @@
|
|||||||
---
|
---
|
||||||
name: Problem Report
|
name: Problem Report/Change Request
|
||||||
about: Create a Report to help us improve
|
about: Create a Report to help us improve
|
||||||
---
|
---
|
||||||
|
|
||||||
<!-- Thanks for reporting a problem for this project. READ THIS FIRST:
|
<!-- Thanks for reporting an issue for this project. READ THIS FIRST:
|
||||||
|
|
||||||
Please DO NOT OPEN AN ISSUE if your EMS-ESP version is not the latest from the dev branch, please update your device before submitting your issue. Your problem might already be solved. The latest precompiled binaries of EMS-ESP can be downloaded from https://github.com/emsesp/EMS-ESP32/releases/tag/latest
|
Please DO NOT OPEN AN ISSUE if your EMS-ESP version is not the latest from the dev branch, please update your device before submitting your issue. Your issue might already be solved. The latest precompiled binaries of EMS-ESP can be downloaded from https://github.com/emsesp/EMS-ESP32/releases/tag/latest
|
||||||
|
|
||||||
Please take a few minutes to complete the requested information below.
|
Please take a few minutes to complete the requested information below.
|
||||||
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
### PROBLEM DESCRIPTION
|
### DESCRIPTION
|
||||||
|
|
||||||
_A clear and concise description of what the problem is._
|
_A clear and concise description of what the problem is or the change requested._
|
||||||
|
|
||||||
### REQUESTED INFORMATION
|
### REQUESTED INFORMATION
|
||||||
|
|
||||||
_Make sure your have performed every step and checked the applicable boxes before submitting your issue. Thank you!_
|
_Make sure your have performed every step and checked the applicable boxes before submitting your issue. Thank you!_
|
||||||
|
|
||||||
- [ ] Searched the problem in [issues](https://github.com/emsesp/EMS-ESP32/issues)
|
- [ ] Searched the issue in [issues](https://github.com/emsesp/EMS-ESP32/issues)
|
||||||
- [ ] Searched the problem in [discussions](https://github.com/emsesp/EMS-ESP32/discussions)
|
- [ ] Searched the issue in [discussions](https://github.com/emsesp/EMS-ESP32/discussions)
|
||||||
- [ ] Searched the problem in the [docs](https://emsesp.github.io/docs/Troubleshooting/)
|
- [ ] Searched the issue in the [docs](https://docs.emsesp.org/Troubleshooting/)
|
||||||
- [ ] Searched the problem in the [chat](https://discord.gg/3J3GgnzpyT)
|
- [ ] Searched the issue in the [chat](https://discord.gg/3J3GgnzpyT)
|
||||||
- [ ] Provide the output of http://ems-esp.local/api/system :
|
- [ ] Provide the System information in the area below, taken from `http://<IP>/api/system`
|
||||||
|
|
||||||
```lua
|
```json
|
||||||
System information output here:
|
Paste System information here....
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -41,10 +41,10 @@ _A clear and concise description of what you expected to happen._
|
|||||||
|
|
||||||
### SCREENSHOTS
|
### SCREENSHOTS
|
||||||
|
|
||||||
_If applicable, add screenshots to help explain your problem._
|
_If applicable, add screenshots to help explain your issue._
|
||||||
|
|
||||||
### ADDITIONAL CONTEXT
|
### ADDITIONAL CONTEXT
|
||||||
|
|
||||||
_Add any other context about the problem here._
|
_Add any other context about the issue here._
|
||||||
|
|
||||||
**(Please, remember to close the issue when the problem has been addressed)**
|
**(Please remember to close the issue when it has been addressed)**
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,7 +1,7 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: EMS-ESP Docs
|
- name: EMS-ESP Docs
|
||||||
url: https://emsesp.github.io/docs/
|
url: https://docs.emsesp.org
|
||||||
about: All the information related to EMS-ESP.
|
about: All the information related to EMS-ESP.
|
||||||
- name: EMS-ESP Discussions and Support
|
- name: EMS-ESP Discussions and Support
|
||||||
url: https://github.com/emsesp/EMS-ESP32/discussions
|
url: https://github.com/emsesp/EMS-ESP32/discussions
|
||||||
|
|||||||
1
.github/SUPPORT.md
vendored
Normal file
1
.github/SUPPORT.md
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Support
|
||||||
73
.github/workflows/dev_release.yml
vendored
Normal file
73
.github/workflows/dev_release.yml
vendored
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
name: 'Build dev release'
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- 'src/emsesp_version.h'
|
||||||
|
branches:
|
||||||
|
- 'dev'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pre-release:
|
||||||
|
name: 'Build Dev Release'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
|
||||||
|
- name: Install python 3.13
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.13'
|
||||||
|
|
||||||
|
- name: Install Node.js 22
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Enable Corepack
|
||||||
|
run: corepack enable pnpm
|
||||||
|
|
||||||
|
- name: Get the EMS-ESP version
|
||||||
|
id: build_info
|
||||||
|
run: |
|
||||||
|
version=`grep -E '^#define EMSESP_APP_VERSION' ./src/emsesp_version.h | awk -F'"' '{print $2}'`
|
||||||
|
echo "VERSION=$version" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Install PlatformIO
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -U platformio
|
||||||
|
python -m pip install intelhex
|
||||||
|
|
||||||
|
- name: Build the WebUI
|
||||||
|
run: |
|
||||||
|
cd interface
|
||||||
|
pnpm install
|
||||||
|
pnpm typesafe-i18n --no-watch
|
||||||
|
sed -i "s/= 'pl'/= 'en'/" ./src/i18n/i18n-util.ts
|
||||||
|
pnpm build
|
||||||
|
pnpm webUI
|
||||||
|
|
||||||
|
- name: Build all PIO target environments
|
||||||
|
run: |
|
||||||
|
platformio run
|
||||||
|
env:
|
||||||
|
NO_BUILD_WEBUI: true
|
||||||
|
|
||||||
|
- name: Create GitHub Release
|
||||||
|
id: 'automatic_releases'
|
||||||
|
uses: emsesp/action-automatic-releases@v1.0.0
|
||||||
|
with:
|
||||||
|
repo_token: '${{ secrets.GITHUB_TOKEN }}'
|
||||||
|
title: Development Build v${{steps.build_info.outputs.VERSION}}
|
||||||
|
automatic_release_tag: 'latest'
|
||||||
|
prerelease: true
|
||||||
|
files: |
|
||||||
|
CHANGELOG_LATEST.md
|
||||||
|
./build/firmware/*.*
|
||||||
24
.github/workflows/github-releases-to-discord.yml
vendored
Normal file
24
.github/workflows/github-releases-to-discord.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
name: 'Publish releases to discord'
|
||||||
|
|
||||||
|
on:
|
||||||
|
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
|
||||||
37
.github/workflows/pr_check.yml
vendored
Normal file
37
.github/workflows/pr_check.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
name: 'Pre-check on PR'
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
pull_request:
|
||||||
|
branches: dev
|
||||||
|
paths:
|
||||||
|
- '**.c'
|
||||||
|
- '**.cpp'
|
||||||
|
- '**.h'
|
||||||
|
- '**.hpp'
|
||||||
|
- '**.json'
|
||||||
|
- '**.py'
|
||||||
|
- '**.md'
|
||||||
|
- '.github/workflows/pr_check.yml'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pre-release:
|
||||||
|
name: 'Automatic pre-release build'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install python 3.11
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.11'
|
||||||
|
|
||||||
|
- name: Install PlatformIO
|
||||||
|
run: |
|
||||||
|
pip install wheel
|
||||||
|
pip install -U platformio
|
||||||
|
|
||||||
|
- name: Build native
|
||||||
|
run: |
|
||||||
|
platformio run -e native
|
||||||
58
.github/workflows/pre_release.yml
vendored
58
.github/workflows/pre_release.yml
vendored
@@ -1,58 +0,0 @@
|
|||||||
name: 'pre-release'
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- 'dev'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
pre-release:
|
|
||||||
name: 'Automatic pre-release build'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: actions/setup-python@v4
|
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: '18'
|
|
||||||
|
|
||||||
- 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 run typesafe-i18n --no-watch
|
|
||||||
sed -i "s/= 'pl'/= 'en'/" ./src/i18n/i18n-util.ts
|
|
||||||
yarn run build
|
|
||||||
|
|
||||||
- name: Build firmware
|
|
||||||
run: |
|
|
||||||
platformio run -e ci
|
|
||||||
|
|
||||||
- name: Build S3 firmware
|
|
||||||
run: |
|
|
||||||
platformio run -e ci_s3
|
|
||||||
|
|
||||||
- name: Create a GH Release
|
|
||||||
id: 'automatic_releases'
|
|
||||||
uses: 'marvinpinto/action-automatic-releases@latest'
|
|
||||||
with:
|
|
||||||
repo_token: '${{ secrets.GITHUB_TOKEN }}'
|
|
||||||
title: Development Build v${{steps.build_info.outputs.VERSION}}
|
|
||||||
automatic_release_tag: 'latest'
|
|
||||||
prerelease: true
|
|
||||||
files: |
|
|
||||||
CHANGELOG_LATEST.md
|
|
||||||
./build/firmware/*.*
|
|
||||||
33
.github/workflows/sonar_check.yml
vendored
33
.github/workflows/sonar_check.yml
vendored
@@ -1,30 +1,31 @@
|
|||||||
|
# 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 and analyze
|
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:
|
||||||
BUILD_WRAPPER_OUT_DIR: build_wrapper_output_directory
|
BUILD_WRAPPER_OUT_DIR: bw-output
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- 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: Install sonar-scanner and build-wrapper
|
- name: Install Build Wrapper
|
||||||
uses: SonarSource/sonarcloud-github-c-cpp@v2
|
uses: SonarSource/sonarqube-scan-action/install-build-wrapper@master
|
||||||
- name: Run build-wrapper
|
- name: Run Build Wrapper
|
||||||
run: |
|
run: build-wrapper-linux-x86-64 --out-dir ${{ env.BUILD_WRAPPER_OUT_DIR }} make all
|
||||||
build-wrapper-linux-x86-64 --out-dir ${{ env.BUILD_WRAPPER_OUT_DIR }} make all
|
- name: SonarQube Scan
|
||||||
- name: Run sonar-scanner
|
uses: SonarSource/sonarqube-scan-action@master
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||||
run: |
|
|
||||||
sonar-scanner --define sonar.cfamily.build-wrapper-output="${{ env.BUILD_WRAPPER_OUT_DIR }}"
|
|
||||||
|
|||||||
62
.github/workflows/stable_release.yml
vendored
Normal file
62
.github/workflows/stable_release.yml
vendored
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
name: 'Build stable release'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
tagged-release:
|
||||||
|
name: 'Build Stable Release'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
|
||||||
|
- name: Install python 3.13
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.13'
|
||||||
|
|
||||||
|
- name: Install Node.js 22
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Enable Corepack
|
||||||
|
run: corepack enable pnpm
|
||||||
|
|
||||||
|
- name: Install PlatformIO
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -U platformio
|
||||||
|
python -m pip install intelhex
|
||||||
|
|
||||||
|
- name: Build the WebUI
|
||||||
|
run: |
|
||||||
|
cd interface
|
||||||
|
pnpm install
|
||||||
|
pnpm typesafe-i18n --no-watch
|
||||||
|
sed -i "s/= 'pl'/= 'en'/" ./src/i18n/i18n-util.ts
|
||||||
|
pnpm build
|
||||||
|
pnpm webUI
|
||||||
|
|
||||||
|
- name: Build all PIO target environments
|
||||||
|
run: |
|
||||||
|
platformio run
|
||||||
|
env:
|
||||||
|
NO_BUILD_WEBUI: true
|
||||||
|
|
||||||
|
- name: Create GitHub Release
|
||||||
|
uses: emsesp/action-automatic-releases@v1.0.0
|
||||||
|
with:
|
||||||
|
repo_token: '${{ secrets.GITHUB_TOKEN }}'
|
||||||
|
prerelease: false
|
||||||
|
files: |
|
||||||
|
CHANGELOG.md
|
||||||
|
./build/firmware/*.*
|
||||||
23
.github/workflows/stale_issues.yml
vendored
Normal file
23
.github/workflows/stale_issues.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
name: "Mark or close stale issues and PRs"
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "30 1 * * *"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
stale:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/stale@v9
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
days-before-stale: 30
|
||||||
|
days-before-close: 5
|
||||||
|
stale-issue-message: "This issue is stale because it has been open 30 days with no activity. Remove stale label or comment otherwise this will be closed in 5 days."
|
||||||
|
stale-pr-message: "This PR has been automatically marked as stale because there has been no activity in last 30 days. It will be closed if no further activity occurs. Thank you for your contributions."
|
||||||
|
close-issue-message: "This issue was closed because it has been stalled for 5 days with no activity."
|
||||||
|
close-pr-message: "This PR was automatically closed because of being stale."
|
||||||
|
stale-pr-label: "stale"
|
||||||
|
stale-issue-label: "stale"
|
||||||
|
exempt-issue-labels: "bug,enhancement,pinned,security"
|
||||||
|
exempt-pr-labels: "bug,enhancement,pinned,security"
|
||||||
46
.github/workflows/tagged_release.yml
vendored
46
.github/workflows/tagged_release.yml
vendored
@@ -1,46 +0,0 @@
|
|||||||
name: 'tagged-release'
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- 'v*'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
tagged-release:
|
|
||||||
name: 'Tagged Release'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: actions/setup-python@v4
|
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: '18'
|
|
||||||
|
|
||||||
- name: Install PlatformIO
|
|
||||||
run: |
|
|
||||||
python -m pip install --upgrade pip
|
|
||||||
pip install -U platformio
|
|
||||||
platformio upgrade
|
|
||||||
pio pkg update
|
|
||||||
|
|
||||||
- name: Build WebUI
|
|
||||||
run: |
|
|
||||||
cd interface
|
|
||||||
yarn install
|
|
||||||
yarn run typesafe-i18n --no-watch
|
|
||||||
sed -i "s/= 'pl'/= 'en'/" ./src/i18n/i18n-util.ts
|
|
||||||
yarn run build
|
|
||||||
|
|
||||||
- name: Build firmware
|
|
||||||
run: |
|
|
||||||
platformio run -e ci
|
|
||||||
|
|
||||||
- name: Release
|
|
||||||
uses: 'marvinpinto/action-automatic-releases@latest'
|
|
||||||
with:
|
|
||||||
repo_token: '${{ secrets.GITHUB_TOKEN }}'
|
|
||||||
prerelease: false
|
|
||||||
files: |
|
|
||||||
CHANGELOG.md
|
|
||||||
./build/firmware/*.*
|
|
||||||
60
.github/workflows/test_release.yml
vendored
60
.github/workflows/test_release.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: 'test-release'
|
name: 'Build test release'
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@@ -6,48 +6,63 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- 'dev2'
|
- 'dev2'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
pre-release:
|
pre-release:
|
||||||
name: 'Automatic test-release build'
|
name: 'Build Test Release'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: actions/setup-python@v4
|
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: '18'
|
|
||||||
|
|
||||||
- name: Get EMS-ESP source code and version
|
- name: Install python 3.13
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.13'
|
||||||
|
|
||||||
|
- name: Install Node.js 22
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Enable Corepack
|
||||||
|
run: corepack enable pnpm
|
||||||
|
|
||||||
|
- name: Get the 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/emsesp_version.h | awk -F'"' '{print $2}'`
|
||||||
echo "VERSION=$version" >> $GITHUB_OUTPUT
|
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
|
||||||
|
python -m pip install intelhex
|
||||||
|
|
||||||
- name: Build WebUI
|
- name: Build the WebUI
|
||||||
run: |
|
run: |
|
||||||
cd interface
|
cd interface
|
||||||
yarn install
|
pnpm install
|
||||||
yarn run typesafe-i18n --no-watch
|
pnpm typesafe-i18n --no-watch
|
||||||
sed -i "s/= 'pl'/= 'en'/" ./src/i18n/i18n-util.ts
|
sed -i "s/= 'pl'/= 'en'/" ./src/i18n/i18n-util.ts
|
||||||
yarn run build
|
pnpm build
|
||||||
|
pnpm webUI
|
||||||
|
|
||||||
- name: Build firmware
|
- name: Build all target environments
|
||||||
run: |
|
run: |
|
||||||
platformio run -e ci
|
platformio run
|
||||||
|
env:
|
||||||
|
NO_BUILD_WEBUI: true
|
||||||
|
|
||||||
- name: Build S3 firmware
|
- name: Create GitHub Release
|
||||||
run: |
|
|
||||||
platformio run -e ci_s3
|
|
||||||
|
|
||||||
- name: Create a GH 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: Test Build v${{steps.build_info.outputs.VERSION}}
|
title: Test Build v${{steps.build_info.outputs.VERSION}}
|
||||||
@@ -56,3 +71,4 @@ jobs:
|
|||||||
files: |
|
files: |
|
||||||
CHANGELOG_LATEST.md
|
CHANGELOG_LATEST.md
|
||||||
./build/firmware/*.*
|
./build/firmware/*.*
|
||||||
|
|
||||||
|
|||||||
54
.gitignore
vendored
54
.gitignore
vendored
@@ -2,7 +2,7 @@
|
|||||||
.vscode/c_cpp_properties.json
|
.vscode/c_cpp_properties.json
|
||||||
.vscode/extensions.json
|
.vscode/extensions.json
|
||||||
.vscode/launch.json
|
.vscode/launch.json
|
||||||
# .vscode/settings.json
|
.vscode/settings.json
|
||||||
|
|
||||||
# c++ compiling
|
# c++ compiling
|
||||||
.clang_complete
|
.clang_complete
|
||||||
@@ -16,13 +16,11 @@ pio_local.ini
|
|||||||
# OS specific
|
# OS specific
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*Thumbs.db
|
*Thumbs.db
|
||||||
emsesp
|
|
||||||
|
|
||||||
# web specfic
|
# web specific
|
||||||
build/
|
build/
|
||||||
dist/
|
dist/
|
||||||
/data/www
|
/data/www
|
||||||
/lib/framework/WWWData.h
|
|
||||||
/interface/build
|
/interface/build
|
||||||
node_modules
|
node_modules
|
||||||
/interface/.eslintcache
|
/interface/.eslintcache
|
||||||
@@ -30,18 +28,10 @@ stats.html
|
|||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
.pnp.*
|
.pnp.*
|
||||||
.yarn/*
|
analyse.html
|
||||||
!.yarn/patches
|
interface/vite.config.ts.timestamp*
|
||||||
!.yarn/plugins
|
*.local
|
||||||
!.yarn/releases
|
src/ESP32React/WWWData.h
|
||||||
!.yarn/sdks
|
|
||||||
!.yarn/versions
|
|
||||||
|
|
||||||
# scripts
|
|
||||||
test.sh
|
|
||||||
scripts/run.sh
|
|
||||||
scripts/__pycache__
|
|
||||||
/scripts/stackdmp.txt
|
|
||||||
|
|
||||||
# i18n generated files
|
# i18n generated files
|
||||||
interface/src/i18n/i18n-react.tsx
|
interface/src/i18n/i18n-react.tsx
|
||||||
@@ -50,11 +40,35 @@ interface/src/i18n/i18n-util.ts
|
|||||||
interface/src/i18n/i18n-util.sync.ts
|
interface/src/i18n/i18n-util.sync.ts
|
||||||
interface/src/i18n/i18n-util.async.ts
|
interface/src/i18n/i18n-util.async.ts
|
||||||
|
|
||||||
|
# scripts
|
||||||
|
test.sh
|
||||||
|
scripts/run.sh
|
||||||
|
scripts/__pycache__
|
||||||
|
scripts/stackdmp.txt
|
||||||
|
|
||||||
# sonar
|
# sonar
|
||||||
.scannerwork/
|
.scannerwork/
|
||||||
sonar/
|
sonar/
|
||||||
build_wrapper_output_directory/
|
bw-output/
|
||||||
|
|
||||||
# entity dump results
|
# standalone executable for testing
|
||||||
# dump_entities.csv
|
emsesp
|
||||||
# dump_entities.xls*
|
interface/tsconfig.tsbuildinfo
|
||||||
|
|
||||||
|
# python virtual environment
|
||||||
|
venv/
|
||||||
|
|
||||||
|
# cspell
|
||||||
|
words-found-verbose.txt
|
||||||
|
|
||||||
|
# sonarlint
|
||||||
|
compile_commands.json
|
||||||
|
|
||||||
|
# pioarduino + hybrid
|
||||||
|
managed_components
|
||||||
|
dependencies.lock
|
||||||
|
CMakeLists.txt
|
||||||
|
.dummy/*
|
||||||
|
logs/*
|
||||||
|
sdkconfig.*
|
||||||
|
sdkconfig_tasmota_esp32
|
||||||
|
|||||||
6
.markdownlint.json
Normal file
6
.markdownlint.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"MD033": false,
|
||||||
|
"MD013": false,
|
||||||
|
"MD045": false,
|
||||||
|
"MD041": false
|
||||||
|
}
|
||||||
6
.prettierignore
Normal file
6
.prettierignore
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
*/node_modules/
|
||||||
|
build/
|
||||||
|
dist/*
|
||||||
|
interface/src/i18n/*
|
||||||
|
|
||||||
|
.typesafe-i18n.json
|
||||||
11
.prettierrc
11
.prettierrc
@@ -1,8 +1,13 @@
|
|||||||
{
|
{
|
||||||
|
"plugins": ["@trivago/prettier-plugin-sort-imports"],
|
||||||
"trailingComma": "none",
|
"trailingComma": "none",
|
||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
"semi": true,
|
"semi": true,
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"printWidth": 120,
|
"printWidth": 85,
|
||||||
"bracketSpacing": true
|
"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"
|
||||||
|
}
|
||||||
13
.vscode/extensions.json
vendored
13
.vscode/extensions.json
vendored
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
|
||||||
// for the documentation about the extensions.json format
|
|
||||||
"recommendations": [
|
|
||||||
"arcanis.vscode-zipfs",
|
|
||||||
"dbaeumer.vscode-eslint",
|
|
||||||
"esbenp.prettier-vscode",
|
|
||||||
"platformio.platformio-ide"
|
|
||||||
],
|
|
||||||
"unwantedRecommendations": [
|
|
||||||
"ms-vscode.cpptools-extension-pack"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
144
.vscode/settings.json
vendored
144
.vscode/settings.json
vendored
@@ -1,45 +1,101 @@
|
|||||||
{
|
{
|
||||||
"search.exclude": {
|
"search.exclude": {
|
||||||
"**/.yarn": true,
|
"**/.yarn": true,
|
||||||
"**/.pnp.*": true
|
"**/.pnp.*": true
|
||||||
},
|
},
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll": true
|
"source.fixAll": "explicit"
|
||||||
// "source.organizeImports": true
|
},
|
||||||
},
|
"eslint.validate": [
|
||||||
"eslint.nodePath": "interface/.yarn/sdks",
|
"typescript"
|
||||||
"eslint.workingDirectories": ["interface"],
|
],
|
||||||
"prettier.prettierPath": "",
|
"eslint.codeActionsOnSave.rules": null,
|
||||||
"typescript.tsdk": "interface/.yarn/sdks/typescript/lib",
|
"eslint.nodePath": "interface/.yarn/sdks",
|
||||||
"typescript.enablePromptUseWorkspaceTsdk": true,
|
"eslint.workingDirectories": ["interface"],
|
||||||
"files.associations": {
|
"prettier.prettierPath": "",
|
||||||
"*.tsx": "typescriptreact",
|
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||||
"*.tcc": "cpp",
|
"files.associations": {
|
||||||
"optional": "cpp",
|
"*.tsx": "typescriptreact",
|
||||||
"istream": "cpp",
|
"*.tcc": "cpp",
|
||||||
"ostream": "cpp",
|
"optional": "cpp",
|
||||||
"ratio": "cpp",
|
"istream": "cpp",
|
||||||
"system_error": "cpp",
|
"ostream": "cpp",
|
||||||
"array": "cpp",
|
"ratio": "cpp",
|
||||||
"functional": "cpp",
|
"system_error": "cpp",
|
||||||
"regex": "cpp",
|
"array": "cpp",
|
||||||
"tuple": "cpp",
|
"functional": "cpp",
|
||||||
"type_traits": "cpp",
|
"regex": "cpp",
|
||||||
"utility": "cpp",
|
"tuple": "cpp",
|
||||||
"string": "cpp",
|
"type_traits": "cpp",
|
||||||
"string_view": "cpp"
|
"utility": "cpp",
|
||||||
},
|
"string": "cpp",
|
||||||
"todo-tree.filtering.excludeGlobs": [
|
"string_view": "cpp",
|
||||||
"**/vendor/**",
|
"atomic": "cpp",
|
||||||
"**/node_modules/**",
|
"bitset": "cpp",
|
||||||
"**/dist/**",
|
"cctype": "cpp",
|
||||||
"**/bower_components/**",
|
"chrono": "cpp",
|
||||||
"**/build/**",
|
"clocale": "cpp",
|
||||||
"**/.vscode/**",
|
"cmath": "cpp",
|
||||||
"**/.github/**",
|
"condition_variable": "cpp",
|
||||||
"**/_output/**",
|
"cstdarg": "cpp",
|
||||||
"**/*.min.*",
|
"cstddef": "cpp",
|
||||||
"**/*.map",
|
"cstdint": "cpp",
|
||||||
"**/ArduinoJson/**"
|
"cstdio": "cpp",
|
||||||
]
|
"cstdlib": "cpp",
|
||||||
}
|
"cstring": "cpp",
|
||||||
|
"ctime": "cpp",
|
||||||
|
"cwchar": "cpp",
|
||||||
|
"cwctype": "cpp",
|
||||||
|
"deque": "cpp",
|
||||||
|
"list": "cpp",
|
||||||
|
"unordered_map": "cpp",
|
||||||
|
"unordered_set": "cpp",
|
||||||
|
"vector": "cpp",
|
||||||
|
"exception": "cpp",
|
||||||
|
"algorithm": "cpp",
|
||||||
|
"iterator": "cpp",
|
||||||
|
"map": "cpp",
|
||||||
|
"memory": "cpp",
|
||||||
|
"memory_resource": "cpp",
|
||||||
|
"numeric": "cpp",
|
||||||
|
"random": "cpp",
|
||||||
|
"set": "cpp",
|
||||||
|
"fstream": "cpp",
|
||||||
|
"initializer_list": "cpp",
|
||||||
|
"iomanip": "cpp",
|
||||||
|
"iosfwd": "cpp",
|
||||||
|
"iostream": "cpp",
|
||||||
|
"limits": "cpp",
|
||||||
|
"mutex": "cpp",
|
||||||
|
"new": "cpp",
|
||||||
|
"sstream": "cpp",
|
||||||
|
"stdexcept": "cpp",
|
||||||
|
"streambuf": "cpp",
|
||||||
|
"thread": "cpp",
|
||||||
|
"cinttypes": "cpp",
|
||||||
|
"typeinfo": "cpp"
|
||||||
|
},
|
||||||
|
"todo-tree.filtering.excludeGlobs": [
|
||||||
|
"**/vendor/**",
|
||||||
|
"**/node_modules/**",
|
||||||
|
"**/dist/**",
|
||||||
|
"**/bower_components/**",
|
||||||
|
"**/build/**",
|
||||||
|
"**/.vscode/**",
|
||||||
|
"**/.github/**",
|
||||||
|
"**/_output/**",
|
||||||
|
"**/*.min.*",
|
||||||
|
"**/*.map",
|
||||||
|
"**/ArduinoJson/**"
|
||||||
|
],
|
||||||
|
"cSpell.enableFiletypes": [
|
||||||
|
"ini",
|
||||||
|
"makefile"
|
||||||
|
],
|
||||||
|
"typescript.preferences.preferTypeOnlyAutoImports": true,
|
||||||
|
"sonarlint.pathToCompileCommands": "${workspaceFolder}/compile_commands.json",
|
||||||
|
"sonarlint.connectedMode.project": {
|
||||||
|
"connectionId": "emsesp",
|
||||||
|
"projectKey": "emsesp_EMS-ESP32"
|
||||||
|
}
|
||||||
|
}
|
||||||
18
.vscode/tasks.json
vendored
18
.vscode/tasks.json
vendored
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
|
||||||
// for the documentation about the tasks.json format
|
|
||||||
"version": "2.0.0",
|
|
||||||
"tasks": [
|
|
||||||
{
|
|
||||||
"type": "shell",
|
|
||||||
"label": "build standalone emsesp",
|
|
||||||
"command": "make",
|
|
||||||
"args": [],
|
|
||||||
"problemMatcher": ["$gcc"],
|
|
||||||
"group": {
|
|
||||||
"kind": "build",
|
|
||||||
"isDefault": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
363
CHANGELOG.md
363
CHANGELOG.md
@@ -5,6 +5,365 @@ 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.7.2] 22 March 2025
|
||||||
|
|
||||||
|
## Added
|
||||||
|
|
||||||
|
- change enum_heatingtype for remote control [#2268](https://github.com/emsesp/EMS-ESP32/issues/2268)
|
||||||
|
- system service commands [#2182](https://github.com/emsesp/EMS-ESP32/issues/2182)
|
||||||
|
- read 0x02A5 for thermostat CT200 [#2277](https://github.com/emsesp/EMS-ESP32/issues/2277)
|
||||||
|
- add "duplicate" option to Custom Entities [#2266](https://github.com/emsesp/EMS-ESP32/discussion/2266)
|
||||||
|
- mask bits for bool custom entities
|
||||||
|
- thermostat `reduce threshold` [#2288](https://github.com/emsesp/EMS-ESP32/issues/2288)
|
||||||
|
- thermostat `absent` [#1957](https://github.com/emsesp/EMS-ESP32/issues/1957)
|
||||||
|
- CR11 thermostat [#2295](https://github.com/emsesp/EMS-ESP32/issues/2295)
|
||||||
|
- Show ESP32's CPU temp in Hardware Status
|
||||||
|
- vacation mode for the CR50 [#2403](https://github.com/emsesp/EMS-ESP32/issues/2403)
|
||||||
|
- new Console command "set admin password" to set WebUI admin password
|
||||||
|
- support nested conditions in scheduler [#2451](https://github.com/emsesp/EMS-ESP32/issues/2451)
|
||||||
|
- allow mixed case in scheduler expressions [#2457](https://github.com/emsesp/EMS-ESP32/issues/2457)
|
||||||
|
- Suprapur-o [#2470](https://github.com/emsesp/EMS-ESP32/issues/2470)
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
|
||||||
|
- long numbers of custom entities [#2267](https://github.com/emsesp/EMS-ESP32/issues/2267)
|
||||||
|
- modbus command path to `api/` [#2276](https://github.com/emsesp/EMS-ESP32/issues/2276)
|
||||||
|
- info command for devices without entity-commands [#2274](https://github.com/emsesp/EMS-ESP32/issues/2274)
|
||||||
|
- CW100 settings telegram 0x241 [#2290](https://github.com/emsesp/EMS-ESP32/issues/2290)
|
||||||
|
- modbus signed 8bit values [#2294](https://github.com/emsesp/EMS-ESP32/issues/2294)
|
||||||
|
- thermostat date [#2313](https://github.com/emsesp/EMS-ESP32/issues/2313)
|
||||||
|
- Updated unknown compressor stati "enum_hpactivity" [#2311](https://github.com/emsesp/EMS-ESP32/pull/2311)
|
||||||
|
- Underline Tab headers in WebUI
|
||||||
|
- console unit tests fixed due to changed shell output
|
||||||
|
- tx-queue overflow in some heatpump systems [#2455](https://github.com/emsesp/EMS-ESP32/issues/2455)
|
||||||
|
|
||||||
|
## Changed
|
||||||
|
|
||||||
|
- show operation in pretty telegram between src and dst [#2263](https://github.com/emsesp/EMS-ESP32/discussions/2263)
|
||||||
|
- update eModbus to 1.7.2 [#2254](https://github.com/emsesp/EMS-ESP32/issues/2254)
|
||||||
|
- modbus timeout default to 300 sec, change setting from ms to sec [#2254](https://github.com/emsesp/EMS-ESP32/issues/2254)
|
||||||
|
- update AsyncTCP and ESPAsyncWebServer to latest versions
|
||||||
|
- update Arduino pio platform to 3.10.0 and optimized flash using build flags
|
||||||
|
- Version checker in WebUI improved
|
||||||
|
- rename `remoteseltemp` to `cooltemp` [#2456](https://github.com/emsesp/EMS-ESP32/issues/2456)
|
||||||
|
|
||||||
|
## [3.7.1] 29 November 2024
|
||||||
|
|
||||||
|
## Added
|
||||||
|
|
||||||
|
- include HA "unit_of_meas", "stat_cla" and "dev_cla" attributes for Number sensors [#2149](https://github.com/emsesp/EMS-ESP32/issues/2149)
|
||||||
|
- Bosch CS6800i AW - Silent Mode + Electrical Power Reduction (HP) [#2147](https://github.com/emsesp/EMS-ESP32/issues/2147)
|
||||||
|
- `/api/system/showeralert` and `/api/system/showertimer` [#2182](https://github.com/emsesp/EMS-ESP32/issues/2182)
|
||||||
|
- MX400 [#2198](https://github.com/emsesp/EMS-ESP32/issues/2198)
|
||||||
|
- SM200 values [#2212](https://github.com/emsesp/EMS-ESP32/discussions/2212)
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
|
||||||
|
- Modbus integration in 3.7.0 missing offset [#2148](https://github.com/emsesp/EMS-ESP32/issues/2148)
|
||||||
|
- fix changing TZ in NTPsettings without clearing enable+server, added DST support [#2142](https://github.com/emsesp/EMS-ESP32/issues/2142)
|
||||||
|
- Support MQTT Discovery (AD) with Domoticz [#2177](https://github.com/emsesp/EMS-ESP32/issues/2177)
|
||||||
|
- wwExtra (dhw extra) changed from temperature reading to number
|
||||||
|
- auxheaterstatus [#2192](https://github.com/emsesp/EMS-ESP32/issues/2192)
|
||||||
|
- lastCode character check [#2189](https://github.com/emsesp/EMS-ESP32/issues/2189)
|
||||||
|
- reading too many telegram parts
|
||||||
|
- heatpump cost UOMs [#2188](https://github.com/emsesp/EMS-ESP32/issues/2188)
|
||||||
|
- analog dac output and inputs on dac pins [#2201](https://github.com/emsesp/EMS-ESP32/discussions/2201)
|
||||||
|
- api memory leak [#2216](https://github.com/emsesp/EMS-ESP32/issues/2216)
|
||||||
|
- modbus multiple mixers [#2229](https://github.com/emsesp/EMS-ESP32/issues/2229)
|
||||||
|
- Last Will (LWT) not set on MQTT Connect [#2247](https://github.com/emsesp/EMS-ESP32/issues/2247)
|
||||||
|
|
||||||
|
## Changed
|
||||||
|
|
||||||
|
- name of wwstarts2 [#2217](https://github.com/emsesp/EMS-ESP32/discussions/2217)
|
||||||
|
|
||||||
|
## [3.7.0] 27 October 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 [docs.emsesp.org](https://docs.emsesp.org/).
|
||||||
|
|
||||||
|
## Added
|
||||||
|
|
||||||
|
- some more entities for dhw with SM100 module
|
||||||
|
- thermostat second dhw circuit [#1634](https://github.com/emsesp/EMS-ESP32/issues/1634)
|
||||||
|
- remote thermostat emulation for RC100H, RC200 and FB10 [#1287](https://github.com/emsesp/EMS-ESP32/discussions/1287), [#1602](https://github.com/emsesp/EMS-ESP32/discussions/1602), [#1551](https://github.com/emsesp/EMS-ESP32/discussions/1551)
|
||||||
|
- heatpump dhw stop temperatures [#1624](https://github.com/emsesp/EMS-ESP32/issues/1624)
|
||||||
|
- reset history [#1695](https://github.com/emsesp/EMS-ESP32/issues/1695)
|
||||||
|
- heatpump entities `fan` and `shutdown` [#1690](https://github.com/emsesp/EMS-ESP32/discussions/1690)
|
||||||
|
- mqtt HA-mode 3 for v3.6 compatible HA entities, set on update v3.6->v3.7
|
||||||
|
- HP input states [#1723](https://github.com/emsesp/EMS-ESP32/discussions/1723)
|
||||||
|
- holiday settings for rego 3000 [#1735](https://github.com/emsesp/EMS-ESP32/issues/1735)
|
||||||
|
- Added scripts for OTA (scripts/upload.py and upload_cli.py) [#1738](https://github.com/emsesp/EMS-ESP32/issues/1738)
|
||||||
|
- timeout for remote thermostat emulation [#1680](https://github.com/emsesp/EMS-ESP32/discussions/1680), [#1774](https://github.com/emsesp/EMS-ESP32/issues/1774)
|
||||||
|
- CR120 thermostat as own model() [#1779](https://github.com/emsesp/EMS-ESP32/discussions/1779)
|
||||||
|
- modules - external linkable module library [#1778](https://github.com/emsesp/EMS-ESP32/issues/1778)
|
||||||
|
- scheduler onChange and Conditions [#1806](https://github.com/emsesp/EMS-ESP32/issues/1806)
|
||||||
|
- make remote control timeout editable [#1774](https://github.com/emsesp/EMS-ESP32/issues/1774)
|
||||||
|
- added extra pump characteristics (mode and pressure for EMS+) by @SLTKA [#1802](https://github.com/emsesp/EMS-ESP32/pull/1802)
|
||||||
|
- allow device name to be customized [#1174](https://github.com/emsesp/EMS-ESP32/issues/1174)
|
||||||
|
- Modbus support by @mheyse [#1744](https://github.com/emsesp/EMS-ESP32/issues/1744)
|
||||||
|
- System Message command [#1854](https://github.com/emsesp/EMS-ESP32/issues/1854)
|
||||||
|
- scheduler can use web get/post for values and commands [#1806](https://github.com/emsesp/EMS-ESP32/issues/1806)
|
||||||
|
- RT800 remote emulation [#1867](https://github.com/emsesp/EMS-ESP32/issues/1867)
|
||||||
|
- RC310 cooling parameters [#1857](https://github.com/emsesp/EMS-ESP32/issues/1857)
|
||||||
|
- command `api/device/entities` [#1897](https://github.com/emsesp/EMS-ESP32/issues/1897)
|
||||||
|
- switchprogmode [#1903](https://github.com/emsesp/EMS-ESP32/discussions/1903)
|
||||||
|
- autodetect and download firmware upgrades via the WebUI
|
||||||
|
- command 'show log' that lists out the current weblog buffer, showing last messages.
|
||||||
|
- default web log buffer to 25 lines for ESP32s with no PSRAM
|
||||||
|
- try and determine correct board profile if none is set during boot
|
||||||
|
- auto Scroll in WebLog UI - reduced delay so incoming logs are faster
|
||||||
|
- uploading custom support info, shown to Guest users in Help page [#2054](https://github.com/emsesp/EMS-ESP32/issues/2054)
|
||||||
|
- feature: Dashboard showing all data (favorites, sensors, custom) [#1958](https://github.com/emsesp/EMS-ESP32/issues/1958)
|
||||||
|
- entity for low-temperature boilers pump start temp (pumpOnTemp) #2088 [#2088](https://github.com/emsesp/EMS-ESP32/issues/2088)
|
||||||
|
- internal ESP32 temperature sensor on the S3 [#2077](https://github.com/emsesp/EMS-ESP32/issues/2077)
|
||||||
|
- MQTT status topic (used in connect and last will) set to Retain [#2086](https://github.com/emsesp/EMS-ESP32/discussions/2086)
|
||||||
|
- Czech language [2096](https://github.com/emsesp/EMS-ESP32/issues/2096)
|
||||||
|
- Developer Mode and send EMS Read Commands from WebUI [#2116](https://github.com/emsesp/EMS-ESP32/issues/2116)
|
||||||
|
- Scheduler functions [#2115](https://github.com/emsesp/EMS-ESP32/issues/2115)
|
||||||
|
- Set device custom name from telegram 0x01 [#2073](https://github.com/emsesp/EMS-ESP32/issues/2073)
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
|
||||||
|
- remote thermostat emulation for RC200 on Rego2000/3000 thermostats [#1691](https://github.com/emsesp/EMS-ESP32/discussions/1691)
|
||||||
|
- log shows data for F7/F9 requests
|
||||||
|
- Detection of LittleFS for factory setting wasn't working
|
||||||
|
- Check for bad GPIOs with Ethernet before the ethernet is initialized
|
||||||
|
- Show values with factor 50 on webUI [#2064](https://github.com/emsesp/EMS-ESP32/issues/2064)
|
||||||
|
- Rendering of values between -1 and 0
|
||||||
|
- Value for 32bit times not-set [#2109](https://github.com/emsesp/EMS-ESP32/issues/2109)
|
||||||
|
|
||||||
|
## Changed
|
||||||
|
|
||||||
|
- use flag for BC400 compatible thermostats, manage different mode settings
|
||||||
|
- use factory partition for 16M flash
|
||||||
|
- store digital out states to nvs
|
||||||
|
- Refresh UI - moving settings to one location [#1665](https://github.com/emsesp/EMS-ESP32/issues/1665)
|
||||||
|
- rename DeviceValueTypes, add UINT32 for custom entities
|
||||||
|
- dynamic register dhw circuits for thermostat
|
||||||
|
- removed OTA feature [#1738](https://github.com/emsesp/EMS-ESP32/issues/1738)
|
||||||
|
- added shower min duration [#1801](https://github.com/emsesp/EMS-ESP32/issues/1801)
|
||||||
|
- Include TXT file along with the generated CSV for Device Data export/download
|
||||||
|
- thermostat/remotetemp as command [#1835](https://github.com/emsesp/EMS-ESP32/discussions/1835)
|
||||||
|
- temperaturesensor id notation with underscore [#1794](https://github.com/emsesp/EMS-ESP32/discussions/1794)
|
||||||
|
- Change key-names in JSON to be compliant and consistent [#1860](https://github.com/emsesp/EMS-ESP32/issues/1860)
|
||||||
|
- Updates to webUI [#1920](https://github.com/emsesp/EMS-ESP32/issues/1920)
|
||||||
|
- Correct firmware naming #1933 [#1933](https://github.com/emsesp/EMS-ESP32/issues/1933)
|
||||||
|
- Don't start Serial console if not connected to a Serial port. Will initiate manually after a CTRL-C/CTRL-S
|
||||||
|
- WebLog UI matches color schema of the terminal console correctly
|
||||||
|
- Updated Web libraries, ArduinoJson
|
||||||
|
- Help page doesn't show detailed tech info if the user is not 'admin' role [#2054](https://github.com/emsesp/EMS-ESP32/issues/2054)
|
||||||
|
- removed system command `allvalues` and moved to an action called `export`
|
||||||
|
- Show ems-esp internal devices in device list of system/info
|
||||||
|
- Scheduler and mqtt run async on systems with psram
|
||||||
|
- Show IPv6 address type (local/global/ula) in log
|
||||||
|
|
||||||
|
## [3.6.5] March 23 2024
|
||||||
|
|
||||||
|
## **IMPORTANT! BREAKING CHANGES**
|
||||||
|
|
||||||
|
- The Wifi Tx Power setting in Network Settings will be reset to Auto
|
||||||
|
|
||||||
|
## Added
|
||||||
|
|
||||||
|
- thermostat boost mode and boost time [#1446](https://github.com/emsesp/EMS-ESP32/issues/1446)
|
||||||
|
- heatpump energy meters [#1463](https://github.com/emsesp/EMS-ESP32/issues/1463)
|
||||||
|
- heatpump max power [#1475](https://github.com/emsesp/EMS-ESP32/issues/1475)
|
||||||
|
- checkbox for MQTT-TLS enable [#1474](https://github.com/emsesp/EMS-ESP32/issues/1474)
|
||||||
|
- added SK (Slovak) language. Thanks @misa1515
|
||||||
|
- CPU info [#1497](https://github.com/emsesp/EMS-ESP32/pull/1497)
|
||||||
|
- Show network hostname in Web UI under Network Status
|
||||||
|
- Improved HA Discovery so each section (EMS device, Scheduler, Analog, Temperature, Custom, Shower) have their own section
|
||||||
|
- boiler Bosch C1200W, id 12, [#1536](https://github.com/emsesp/EMS-ESP32/issues/1536)
|
||||||
|
- mixer MM100 telegram 0x2CC [#1554](https://github.com/emsesp/EMS-ESP32/issues/1554)
|
||||||
|
- boiler hpSetDiffPressure [#1563](https://github.com/emsesp/EMS-ESP32/issues/1563)
|
||||||
|
- custom variables [#1423](https://github.com/emsesp/EMS-ESP32/issues/1423)
|
||||||
|
- weather compensation [#1642](https://github.com/emsesp/EMS-ESP32/issues/1642)
|
||||||
|
- env and partitions for DevKitC-1-N32R8 [#1635](https://github.com/emsesp/EMS-ESP32/discussions/1635)
|
||||||
|
- command `restart partitionname` and button long press to start with other partition [#1657](https://github.com/emsesp/EMS-ESP32/issues/1657)
|
||||||
|
- command `set service <mqtt|ota|ntp|ap> <enable|disable>` [#1663](https://github.com/emsesp/EMS-ESP32/issues/1663)
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
|
||||||
|
- exhaust temperature for some boilers
|
||||||
|
- add back boil2hyst [#1477](https://github.com/emsesp/EMS-ESP32/issues/1477)
|
||||||
|
- subscribed MQTT topics not detecting changes by EMS-ESP [#1494](https://github.com/emsesp/EMS-ESP32/issues/1494)
|
||||||
|
- changed HA name and grouping to be consistent [#1528](https://github.com/emsesp/EMS-ESP32/issues/1528)
|
||||||
|
- MQTT autodiscovery in Domoticz not working [#1360](https://github.com/emsesp/EMS-ESP32/issues/1528)
|
||||||
|
- dhw comfort for new ems+, [#1495](https://github.com/emsesp/EMS-ESP32/issues/1495)
|
||||||
|
- added writeable icon to Web's Custom Entity page for each entity shown in the table
|
||||||
|
- Wifi Tx Power not adjusted [#1614](https://github.com/emsesp/EMS-ESP32/issues/1614)
|
||||||
|
- MQTT discovery of custom entity doesn't consider type of data [#1587](https://github.com/emsesp/EMS-ESP32/issues/1587)
|
||||||
|
- WiFi TxPower wasn't correctly used. Added an 'Auto' setting, which is the default.
|
||||||
|
- dns w/wo IPv6 [#1644](https://github.com/emsesp/EMS-ESP32/issues/1644)
|
||||||
|
|
||||||
|
## Changed
|
||||||
|
|
||||||
|
- HA don't set entity_category to Diagnostic/Configuration for EMS entities [#1459](https://github.com/emsesp/EMS-ESP32/discussions/1459)
|
||||||
|
- upgraded ArduinoJson to 7.0.0 #1538 and then 7.0.2
|
||||||
|
- small changes to the API for analog and temperature sensors
|
||||||
|
- Length of mqtt Broker adress [#1619](https://github.com/emsesp/EMS-ESP32/issues/1619)
|
||||||
|
- C++ optimizations - see <https://github.com/emsesp/EMS-ESP32/pull/1615>
|
||||||
|
- Send MQTT heartbeat immediately after connection [#1628](https://github.com/emsesp/EMS-ESP32/issues/1628)
|
||||||
|
- 16MB partitions with second nvs, larger FS, Coredump, optional factory partition
|
||||||
|
- stop fetching empty telegrams after 5 min
|
||||||
|
|
||||||
|
## [3.6.4] November 24 2023
|
||||||
|
|
||||||
|
## **IMPORTANT! BREAKING CHANGES**
|
||||||
|
|
||||||
|
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
|
# [3.5.0] February 6 2023
|
||||||
|
|
||||||
## **IMPORTANT! BREAKING CHANGES**
|
## **IMPORTANT! BREAKING CHANGES**
|
||||||
@@ -85,7 +444,7 @@ 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
|
||||||
|
|
||||||
@@ -112,7 +471,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
- 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
|
||||||
|
|||||||
@@ -1,70 +1,56 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
# [3.6.0]
|
For more details go to [docs.emsesp.org](https://docs.emsesp.org/).
|
||||||
|
|
||||||
## **IMPORTANT! BREAKING CHANGES**
|
## [3.7.3]
|
||||||
|
|
||||||
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
|
## Added
|
||||||
|
|
||||||
- Workaround for better Domoticz MQTT intergration? [#904](https://github.com/emsesp/EMS-ESP32/issues/904)
|
- analogsensor types: NTC and RGB-Led
|
||||||
- Show MAC address without connecting to network enhancement [#933](https://github.com/emsesp/EMS-ESP32/issues/933)
|
- Flag for HMC310 [#2465](https://github.com/emsesp/EMS-ESP32/issues/2465)
|
||||||
- Warn user in WebUI of unsaved changes [#911](https://github.com/emsesp/EMS-ESP32/issues/911)
|
- boiler auxheatersource [#2489](https://github.com/emsesp/EMS-ESP32/discussions/2489)
|
||||||
- Detect old Tado thermostat, device-id 0x19, no entities
|
- thermostat last error for RC100/300 [#2501](https://github.com/emsesp/EMS-ESP32/issues/2501)
|
||||||
- Some more HM200 entities [#500](https://github.com/emsesp/EMS-ESP32/issues/500)
|
- boiler 0xC6 telegram [#1963](https://github.com/emsesp/EMS-ESP32/issues/1963)
|
||||||
- Added Scheduler [#701](https://github.com/emsesp/EMS-ESP32/issues/701)
|
- CS6800i changes [#2448](https://github.com/emsesp/EMS-ESP32/issues/2448), [#2449](https://github.com/emsesp/EMS-ESP32/issues/2449)
|
||||||
- Added Custom Entities read/write from EMS bus
|
- charging pump [#2544](https://github.com/emsesp/EMS-ESP32/issues/2544)
|
||||||
- Build S3 binary with github actions
|
- hybrid CSH5800iG [#2569](https://github.com/emsesp/EMS-ESP32/issues/2569)
|
||||||
- Greenstar HIU [#1158](https://github.com/emsesp/EMS-ESP32/issues/1158)
|
- add EMS Device details to Home Assistant MQTT Discovery
|
||||||
- AM200 code 10 [#1161](https://github.com/emsesp/EMS-ESP32/issues/1161)
|
- disinfection command [#2601](https://github.com/emsesp/EMS-ESP32/issues/2601)
|
||||||
- Ventilation device (Logavent HRV176) [#1172](https://github.com/emsesp/EMS-ESP32/issues/1172)
|
- added new board profile for upcoming BBQKees E32V2.2
|
||||||
- Turn ETH off on wifi connect [#1167](https://github.com/emsesp/EMS-ESP32/issues/1167)
|
- set differential pressure entity in Mixer device
|
||||||
- Support for multiple EMS-ESPs with HA [#1196](https://github.com/emsesp/EMS-ESP32/issues/1196)
|
- set set climate action cooling/heating in HA [#2583](https://github.com/emsesp/EMS-ESP32/issues/2583)
|
||||||
- Italian translation [#1199](https://github.com/emsesp/EMS-ESP32/issues/1199)
|
- Internal sensors of E32V2_2
|
||||||
- Turkish language support [#1076](https://github.com/emsesp/EMS-ESP32/issues/1076)
|
- FW200 display options [#2610](https://github.com/emsesp/EMS-ESP32/discussions/2610)
|
||||||
- Buderus GB182 - HC1 mode change not work bug [#1193](https://github.com/emsesp/EMS-ESP32/issues/1193)
|
- CR11 mode settings OFF/MANUAL depends on selTemp [#2437](https://github.com/emsesp/EMS-ESP32/issues/2437)
|
||||||
- Minimal flow temperature enhancement [#1192](https://github.com/emsesp/EMS-ESP32/issues/1192)
|
- Fuse settings for BBQKees boards
|
||||||
- Roomtemperature Switching Difference enhancement [#1191](https://github.com/emsesp/EMS-ESP32/issues/1191)
|
- Analogsensors for pulse output [#2624](https://github.com/emsesp/EMS-ESP32/discussions/2624)
|
||||||
- Dew Point Temperature Difference enhancement [#1190](https://github.com/emsesp/EMS-ESP32/issues/1190)
|
- Analogsensors frequency input [#2631](https://github.com/emsesp/EMS-ESP32/discussions/2631)
|
||||||
- Control of heating circuit mode enhancement [#1187](https://github.com/emsesp/EMS-ESP32/issues/1187)
|
- SRC plus thermostats [#2636](https://github.com/emsesp/EMS-ESP32/issues/2636)
|
||||||
- Warn user in WebUI of unsaved changes enhancement [#911](https://github.com/emsesp/EMS-ESP32/issues/911)
|
- Greenstar 2000 [#2645](https://github.com/emsesp/EMS-ESP32/issues/2645)
|
||||||
- Create safebuild app to fit into factory partition to give ESP32 more flash memory enhancement [#608](https://github.com/emsesp/EMS-ESP32/issues/608)
|
- RC3xx `dhw modetype` [#2659](https://github.com/emsesp/EMS-ESP32/discussions/2659)
|
||||||
- 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
|
## Fixed
|
||||||
|
|
||||||
- HA-discovery for analog sensor commands [#1035](https://github.com/emsesp/EMS-ESP32/issues/1035)
|
- dhw/switchtime [#2490](https://github.com/emsesp/EMS-ESP32/issues/2490)
|
||||||
- Enum order of RC3x nofrost mode
|
- switch to secure mqtt [#2492](https://github.com/emsesp/EMS-ESP32/issues/2492)
|
||||||
- Heartbeat interval
|
- update link buttons [#2497](https://github.com/emsesp/EMS-ESP32/issues/2497)
|
||||||
- Exhaust temperature always zero on GB125/MC110/RC310 bug [#1147](https://github.com/emsesp/EMS-ESP32/issues/1147)
|
- refresh scheduler states [#2502](https://github.com/emsesp/EMS-ESP32/discussions/2502)
|
||||||
- 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)
|
- also rebuild HA config on mqtt connect for scheduler, custom and shower
|
||||||
- NTP: cant apply changed timezone [#1182](https://github.com/emsesp/EMS-ESP32/issues/1182)
|
- FB100 controls the hc, not the master [#2510](https://github.com/emsesp/EMS-ESP32/issues/2510)
|
||||||
- Missing Status of VS1 for Buderus SM200 enhancement [#1034](https://github.com/emsesp/EMS-ESP32/issues/1034)
|
- IPM DHW module, [#2524](https://github.com/emsesp/EMS-ESP32/issues/2524)
|
||||||
- Allowed gpios for S3
|
- charge optimization [#2543](https://github.com/emsesp/EMS-ESP32/issues/2543)
|
||||||
|
- shower active state retained, shows correctly in HA
|
||||||
|
- MQTT Command Topic with slashes [#2571](https://github.com/emsesp/EMS-ESP32/issues/2571)
|
||||||
|
- Add pulsed water meter input to V1.3 gateway with Lilygo S3 [#2550](https://github.com/emsesp/EMS-ESP32/issues/2550)
|
||||||
|
- fix missing long 10-second press of Button to perform a factory reset
|
||||||
|
- fix wwMaxPower on Junkers ZBS14 [#2609](https://github.com/emsesp/EMS-ESP32/issues/2609)
|
||||||
|
- ventilation bypass state from telegram 0x55C [#1197](https://github.com/emsesp/EMS-ESP32/issues/1197)
|
||||||
|
- set selflowtemp for ems+ boilers [#2641](https://github.com/emsesp/EMS-ESP32/discussions/2641)
|
||||||
|
|
||||||
## Changed
|
## Changed
|
||||||
|
|
||||||
- Optional upgrade to platform-espressif32 6.3.0 (after 5.3.0) [#862](https://github.com/emsesp/EMS-ESP32/issues/862)
|
- show console log with ISO date/time [#2533](https://github.com/emsesp/EMS-ESP32/discussions/2533)
|
||||||
- Use byte 3 for detection RC30 active heatingcircuit [#786](https://github.com/emsesp/EMS-ESP32/issues/786)
|
- remove ESP32 CPU temperature
|
||||||
- Write repeated selflowtemp if tx-queue is empty without verify [#954](https://github.com/emsesp/EMS-ESP32/issues/954)
|
- updated core libraries like AsyncTCP, AsyncWebServer and Modbus
|
||||||
- HA discovery recreate after disconnect by device [#1067](https://github.com/emsesp/EMS-ESP32/issues/1067)
|
- remove command `scan deep`
|
||||||
- File upload: check flash size (overflow) instead of filesize
|
- ignore repeated `forceheatingoff` commands [#2641](https://github.com/emsesp/EMS-ESP32/discussions/2641)
|
||||||
- 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
|
|
||||||
|
|||||||
@@ -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://docs.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.
|
||||||
|
|
||||||
|
|||||||
85
Makefile
85
Makefile
@@ -2,8 +2,37 @@
|
|||||||
# GNUMakefile for EMS-ESP
|
# GNUMakefile for EMS-ESP
|
||||||
#
|
#
|
||||||
|
|
||||||
NUMJOBS=${NUMJOBS:-" -j4 "}
|
_mkfile_path := $(abspath $(lastword $(MAKEFILE_LIST)))
|
||||||
MAKEFLAGS+="j "
|
I := $(patsubst %/,%,$(dir $(_mkfile_path)))
|
||||||
|
|
||||||
|
ifneq ($(words $(MAKECMDGOALS)),1)
|
||||||
|
.DEFAULT_GOAL = all
|
||||||
|
%:
|
||||||
|
@$(MAKE) $@ --no-print-directory -rRf $(firstword $(MAKEFILE_LIST))
|
||||||
|
else
|
||||||
|
ifndef ECHO
|
||||||
|
T := $(shell $(MAKE) $(MAKECMDGOALS) --no-print-directory \
|
||||||
|
-nrRf $(firstword $(MAKEFILE_LIST)) \
|
||||||
|
ECHO="COUNTTHIS" | grep -c "COUNTTHIS")
|
||||||
|
N := x
|
||||||
|
C = $(words $N)$(eval N := x $N)
|
||||||
|
ECHO = python3 $(I)/scripts/echo_progress.py --stepno=$C --nsteps=$T
|
||||||
|
endif
|
||||||
|
|
||||||
|
# determine number of parallel compiles based on OS
|
||||||
|
UNAME_S := $(shell uname -s)
|
||||||
|
ifeq ($(UNAME_S),Linux)
|
||||||
|
EXTRA_CPPFLAGS = -D LINUX
|
||||||
|
JOBS ?= $(shell nproc)
|
||||||
|
endif
|
||||||
|
ifeq ($(UNAME_S),Darwin)
|
||||||
|
EXTRA_CPPFLAGS = -D OSX -Wno-tautological-constant-out-of-range-compare
|
||||||
|
JOBS ?= $(shell sysctl -n hw.ncpu)
|
||||||
|
endif
|
||||||
|
MAKEFLAGS += -j $(JOBS) -l $(JOBS)
|
||||||
|
|
||||||
|
# $(info Number of jobs: $(JOBS))
|
||||||
|
|
||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
# Project Structure
|
# Project Structure
|
||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
@@ -13,36 +42,29 @@ MAKEFLAGS+="j "
|
|||||||
# INCLUDES is a list of directories containing header files
|
# INCLUDES is a list of directories containing header files
|
||||||
# LIBRARIES is a list of directories containing libraries, this must be the top level containing include and lib
|
# LIBRARIES is a list of directories containing libraries, this must be the top level containing include and lib
|
||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
|
|
||||||
#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 lib/semver lib/espMqttClient/src lib/espMqttClient/src/*
|
SOURCES := src/core src/devices src/web src/test lib_standalone lib/semver lib/espMqttClient/src lib/espMqttClient/src/* lib/ArduinoJson/src lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src lib/PButton
|
||||||
INCLUDES := src 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
|
INCLUDES := src/core src/devices src/web src/test lib/* lib_standalone lib/semver lib/espMqttClient/src lib/espMqttClient/src/Transport lib/ArduinoJson/src lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src lib/uuid-telnet/src lib/uuid-syslog/src
|
||||||
LIBRARIES :=
|
LIBRARIES :=
|
||||||
|
|
||||||
CPPCHECK = cppcheck
|
CPPCHECK = cppcheck
|
||||||
# CHECKFLAGS = -q --force --std=c++17
|
CHECKFLAGS = -q --force --std=gnu++17
|
||||||
CHECKFLAGS = -q --force --std=c++11
|
|
||||||
|
|
||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
# Languages Standard
|
# Languages Standard
|
||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
C_STANDARD := -std=c17
|
C_STANDARD := -std=c17
|
||||||
# CXX_STANDARD := -std=c++17
|
CXX_STANDARD := -std=gnu++17
|
||||||
CXX_STANDARD := -std=gnu++11
|
|
||||||
|
|
||||||
# C_STANDARD := -std=c11
|
|
||||||
# CXX_STANDARD := -std=c++11
|
|
||||||
|
|
||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
# Defined Symbols
|
# Defined Symbols
|
||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
DEFINES += -DARDUINOJSON_ENABLE_STD_STRING=1 -DARDUINOJSON_ENABLE_PROGMEM=1 -DARDUINOJSON_ENABLE_ARDUINO_STRING -DARDUINOJSON_USE_DOUBLE=0
|
DEFINES += -DARDUINOJSON_ENABLE -DARDUINOJSON_ENABLE_ARDUINO_STRING -DARDUINOJSON_USE_DOUBLE=0
|
||||||
DEFINES += -DEMSESP_DEBUG -DEMSESP_STANDALONE -DEMSESP_TEST -D__linux__ -DEMC_RX_BUFFER_SIZE=1500
|
DEFINES += -DEMSESP_STANDALONE -DEMSESP_TEST -DEMSESP_DEBUG -DEMC_RX_BUFFER_SIZE=1500
|
||||||
DEFINES += $(ARGS)
|
DEFINES += $(ARGS)
|
||||||
|
|
||||||
DEFAULTS = -DEMSESP_DEFAULT_LOCALE=\"en\" -DEMSESP_DEFAULT_TX_MODE=8 -DEMSESP_DEFAULT_VERSION=\"3.6.0-dev\" -DEMSESP_DEFAULT_BOARD_PROFILE=\"S32\"
|
DEFAULTS = -DEMSESP_DEFAULT_LOCALE=\"en\" -DEMSESP_DEFAULT_TX_MODE=8 -DEMSESP_DEFAULT_VERSION=\"3.7.3-dev\" -DEMSESP_DEFAULT_BOARD_PROFILE=\"S32S3\"
|
||||||
|
|
||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
# Sources & Files
|
# Sources & Files
|
||||||
@@ -76,14 +98,16 @@ CXX := /usr/bin/g++
|
|||||||
# LDFLAGS Linker Flags
|
# LDFLAGS Linker Flags
|
||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
CPPFLAGS += $(DEFINES) $(DEFAULTS) $(INCLUDE)
|
CPPFLAGS += $(DEFINES) $(DEFAULTS) $(INCLUDE)
|
||||||
CPPFLAGS += -ggdb
|
CPPFLAGS += -ggdb -g3 -MMD
|
||||||
CPPFLAGS += -g3
|
CPPFLAGS += -flto=auto -fno-lto
|
||||||
CPPFLAGS += -Os
|
CPPFLAGS += -Wall -Wextra -Werror -Wswitch-enum
|
||||||
|
CPPFLAGS += -Wno-unused-parameter -Wno-missing-braces -Wno-vla-cxx-extension
|
||||||
|
|
||||||
|
CPPFLAGS += $(EXTRA_CPPFLAGS)
|
||||||
|
|
||||||
CFLAGS += $(CPPFLAGS)
|
CFLAGS += $(CPPFLAGS)
|
||||||
CFLAGS += -Wall -Wextra -Werror -Wswitch-enum -Wno-unused-parameter -Wno-inconsistent-missing-override -Wno-missing-braces -Wno-unused-lambda-capture
|
CXXFLAGS += $(CPPFLAGS)
|
||||||
|
LDFLAGS =
|
||||||
CXXFLAGS += $(CFLAGS) -MMD
|
|
||||||
|
|
||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
# Compiler & Linker Commands
|
# Compiler & Linker Commands
|
||||||
@@ -124,23 +148,27 @@ COMPILE.cpp = $(CXX) $(CXX_STANDARD) $(CXXFLAGS) $(DEPFLAGS) -c $< -o $@
|
|||||||
.SILENT: $(OUTPUT)
|
.SILENT: $(OUTPUT)
|
||||||
|
|
||||||
all: $(OUTPUT)
|
all: $(OUTPUT)
|
||||||
|
@$(ECHO) Build complete.
|
||||||
|
|
||||||
$(OUTPUT): $(OBJS)
|
$(OUTPUT): $(OBJS)
|
||||||
@mkdir -p $(@D)
|
@mkdir -p $(@D)
|
||||||
|
@$(ECHO) Linking $@
|
||||||
$(LINK.o)
|
$(LINK.o)
|
||||||
$(SYMBOLS.out)
|
$(SYMBOLS.out)
|
||||||
|
|
||||||
$(BUILD)/%.o: %.c
|
$(BUILD)/%.o: %.c
|
||||||
@mkdir -p $(@D)
|
@mkdir -p $(@D)
|
||||||
$(COMPILE.c)
|
@$(ECHO) Compiling $@
|
||||||
|
@$(COMPILE.c)
|
||||||
|
|
||||||
$(BUILD)/%.o: %.cpp
|
$(BUILD)/%.o: %.cpp
|
||||||
@mkdir -p $(@D)
|
@mkdir -p $(@D)
|
||||||
$(COMPILE.cpp)
|
@$(ECHO) Compiling $@
|
||||||
|
@$(COMPILE.cpp)
|
||||||
|
|
||||||
$(BUILD)/%.o: %.s
|
$(BUILD)/%.o: %.s
|
||||||
@mkdir -p $(@D)
|
@mkdir -p $(@D)
|
||||||
$(COMPILE.s)
|
@$(COMPILE.s)
|
||||||
|
|
||||||
cppcheck: $(SOURCES)
|
cppcheck: $(SOURCES)
|
||||||
$(CPPCHECK) $(CHECKFLAGS) $^
|
$(CPPCHECK) $(CHECKFLAGS) $^
|
||||||
@@ -149,6 +177,7 @@ run: $(OUTPUT)
|
|||||||
@$<
|
@$<
|
||||||
|
|
||||||
.PHONY: clean
|
.PHONY: clean
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
@$(RM) -rf $(BUILD) $(OUTPUT)
|
@$(RM) -rf $(BUILD) $(OUTPUT)
|
||||||
|
|
||||||
@@ -156,4 +185,6 @@ help:
|
|||||||
@echo available targets: all run clean
|
@echo available targets: all run clean
|
||||||
@echo $(OUTPUT)
|
@echo $(OUTPUT)
|
||||||
|
|
||||||
-include $(DEPS)
|
-include $(DEPS)
|
||||||
|
|
||||||
|
endif
|
||||||
|
|||||||
103
README.md
103
README.md
@@ -1,10 +1,36 @@
|
|||||||
# 
|
<div align="center">
|
||||||
|
<p align="center">
|
||||||
|
<a href="#">
|
||||||
|
<img src="https://raw.githubusercontent.com/emsesp/EMS-ESP32/dev/media/favicon/android-chrome-512x512.png" height="100px" />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 align="center">EMS-ESP</h1>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://emsesp.org">
|
||||||
|
<img src="https://img.shields.io/badge/Website-0077b5?style=for-the-badge&logo=googlehome&logoColor=white" alt="Website" />
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/emsesp/EMS-ESP32/blob/dev/CONTRIBUTING.md">
|
||||||
|
<img src="https://img.shields.io/badge/Contribute-ff4785?style=for-the-badge&logo=git&logoColor=white" alt="Contribute" />
|
||||||
|
</a>
|
||||||
|
<a href="https://docs.emsesp.org">
|
||||||
|
<img src="https://img.shields.io/badge/Documentation-0077b5?style=for-the-badge&logo=googledocs&logoColor=white" alt="Guides" />
|
||||||
|
</a>
|
||||||
|
<a href="https://discord.gg/3J3GgnzpyT">
|
||||||
|
<img src="https://img.shields.io/badge/Discord-7289da?style=for-the-badge&logo=discord&logoColor=white" alt="Discord" />
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/emsesp/EMS-ESP32/blob/main/CHANGELOG.md">
|
||||||
|
<img src="https://img.shields.io/badge/Changelog-6c5ce7?style=for-the-badge&logo=git&logoColor=white" alt="Changelog" />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
[](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)
|
||||||
|
|
||||||
@@ -12,44 +38,59 @@
|
|||||||
[](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)
|
||||||
|
|
||||||
**EMS-ESP** is an open-source firmware for the Espressif ESP32 microcontroller that communicates with **EMS** (Energy Management System) based equipment from manufacturers like Bosch, Buderus, Nefit, Junkers, Worcester and Sieger. It requires a small gateway circuit to interface with the EMS bus which can be purchased from <https://bbqkees-electronics.nl> or custom built.
|
**EMS-ESP** is an open-source firmware for the Espressif ESP32 microcontroller to communicate with **EMS** (Energy Management System) compatible equipment from manufacturers such as Bosch, Buderus, Nefit, Junkers, Worcester, Sieger, elm.leblanc and iVT.
|
||||||
|
|
||||||
## **Features**
|
It requires a small circuit to interface with the EMS bus which can be purchased from <https://bbqkees-electronics.nl> or custom built.
|
||||||
|
|
||||||
- A multi-user, multi-language secure web interface to change settings and monitor incoming data
|
## 📦 **Key Features**
|
||||||
- A console, accessible via Serial and Telnet for more advanced monitoring
|
|
||||||
- Native support for Home Assistant, Domoticz and openHAB via [MQTT Discovery](https://www.home-assistant.io/docs/mqtt/discovery/)
|
|
||||||
- Can run standalone as an independent WiFi Access Point or join an existing WiFi network
|
|
||||||
- Easy first-time configuration via a web Captive Portal
|
|
||||||
- Support for more than [110+ EMS devices](https://emsesp.github.io/docs/All-Devices/) (boilers, thermostats, solar modules, mixer modules, heat pumps, gateways, switches, heat sources)
|
|
||||||
|
|
||||||
## **Documentation**
|
- 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
|
||||||
|
|
||||||
For the complete documentation on how to install, configure and get support visit the [EMS-ESP Wiki](https://emsesp.github.io/docs).
|
## 🚀 **Installing**
|
||||||
|
|
||||||
## **Support**
|
Head over to [download.emsesp.org](https://download.emsesp.org) for instructions on how to install EMS-ESP. There is also further details on which boards are supported in [this section](https://docs.emsesp.org/Installing/) of the documentation.
|
||||||
|
|
||||||
|
## 📋 **Documentation**
|
||||||
|
|
||||||
|
Visit [emsesp.org](https://docs.emsesp.org) for more details on how to install and configure EMS-ESP. There is also a collection of Frequently Asked Questions and Troubleshooting tips with example customizations from the community.
|
||||||
|
|
||||||
|
## 💬 **Getting Support**
|
||||||
|
|
||||||
To chat with the community reach out on our [Discord Server](https://discord.gg/3J3GgnzpyT).
|
To chat with the community reach out on our [Discord Server](https://discord.gg/3J3GgnzpyT).
|
||||||
|
|
||||||
If you like **EMS-ESP**, please give it a star, or fork it and contribute or offer a small donation!
|
If you find an issue or have a request, see [here](https://docs.emsesp.org/Support/) on how to submit a bug report or feature request.
|
||||||
|
|
||||||
## **Demo**
|
## 🎥 **Live Demo**
|
||||||
|
|
||||||
For a live demo of the Web UI click [here](https://ems-esp.derbyshire.nl) and log in with any username/password.
|
For a live demo go to [demo.emsesp.org](https://demo.emsesp.org). Pick a language from the sign on page and log in with any username or password. Note not all features are operational as it's based on static data.
|
||||||
|
|
||||||
## **Contributors ✨**
|
## 💖 **Contributors**
|
||||||
|
|
||||||
EMS-ESP is a project owned and maintained by [proddy](https://github.com/proddy) and [MichaelDvP](https://github.com/MichaelDvP).
|
EMS-ESP is a project created by [proddy](https://github.com/proddy) and owned and maintained by both [proddy](https://github.com/proddy) and [MichaelDvP](https://github.com/MichaelDvP) with support from [BBQKees Electronics](https://bbqkees-electronics.nl).
|
||||||
|
|
||||||
## **Libraries used**
|
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.
|
||||||
|
|
||||||
- [esp8266-react](https://github.com/rjwats/esp8266-react) by @rjwats for the framework that provides the core of the Web UI
|
## 📢 **Libraries used**
|
||||||
- [uuid-\*](https://github.com/nomis/mcu-uuid-console) from @nomis. The console, syslog, telnet and logging are based off these open source libraries
|
|
||||||
- [ArduinoJson](https://github.com/bblanchon/ArduinoJson) for all the JSON
|
|
||||||
- [espMqttClient](https://github.com/bertmelis/espMqttClient) for the MQTT client, with custom modifications from @MichaelDvP and @proddy
|
|
||||||
- ESPAsyncWebServer and AsyncTCP for the Web server and TCP backends, with custom modifications for performance
|
|
||||||
|
|
||||||
## **License**
|
- [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](https://github.com/ESP32Async/ESPAsyncWebServer) and [AsyncTCP](https://github.com/ESP32Async/AsyncTCP) for the Web server and TCP backends
|
||||||
|
|
||||||
|
## 📜 **License**
|
||||||
|
|
||||||
This program is licensed under GPL-3.0
|
This program is licensed under GPL-3.0
|
||||||
|
|
||||||
@@ -59,14 +100,14 @@ This program is licensed under GPL-3.0
|
|||||||
|
|
||||||
| | |
|
| | |
|
||||||
| ---------------------------------- | -------------------------------- |
|
| ---------------------------------- | -------------------------------- |
|
||||||
| <img src="media/web_settings.png"> | <img src="media/web_status.png"> |
|
|  |  |
|
||||||
| <img src="media/web_devices.png"> | <img src="media/web_mqtt.png"> |
|
|  |  |
|
||||||
| <img src="media/web_edit.png"> | <img src="media/web_log.png"> |
|
|  |  |
|
||||||
|
|
||||||
### Telnet Console
|
### Telnet Console
|
||||||
|
|
||||||
<img src="media/console0.png" width=80% height=80%>
|

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

|
||||||
|
|||||||
45
boards/c3_mini_4M.json
Normal file
45
boards/c3_mini_4M.json
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"build": {
|
||||||
|
"arduino": {
|
||||||
|
"ldscript": "esp32c3_out.ld"
|
||||||
|
},
|
||||||
|
"core": "esp32",
|
||||||
|
"extra_flags": [
|
||||||
|
"-DTASMOTA_SDK",
|
||||||
|
"-DARDUINO_LOLIN_C3_MINI",
|
||||||
|
"-DARDUINO_USB_MODE=1",
|
||||||
|
"-DARDUINO_USB_CDC_ON_BOOT=1"
|
||||||
|
],
|
||||||
|
"f_cpu": "160000000L",
|
||||||
|
"f_flash": "80000000L",
|
||||||
|
"flash_mode": "qio",
|
||||||
|
"hwids": [
|
||||||
|
[
|
||||||
|
"0X303A",
|
||||||
|
"0x1001"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"mcu": "esp32c3",
|
||||||
|
"variant": "lolin_c3_mini"
|
||||||
|
},
|
||||||
|
"connectivity": [
|
||||||
|
"wifi"
|
||||||
|
],
|
||||||
|
"debug": {
|
||||||
|
"openocd_target": "esp32c3.cfg"
|
||||||
|
},
|
||||||
|
"frameworks": [
|
||||||
|
"arduino",
|
||||||
|
"espidf"
|
||||||
|
],
|
||||||
|
"name": "WEMOS LOLIN C3 Mini",
|
||||||
|
"upload": {
|
||||||
|
"flash_size": "4MB",
|
||||||
|
"maximum_ram_size": 327680,
|
||||||
|
"maximum_size": 4194304,
|
||||||
|
"require_upload_port": true,
|
||||||
|
"speed": 460800
|
||||||
|
},
|
||||||
|
"url": "https://www.wemos.cc/en/latest/c3/c3_mini.html",
|
||||||
|
"vendor": "WEMOS"
|
||||||
|
}
|
||||||
34
boards/esp32dev.json
Normal file
34
boards/esp32dev.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"build": {
|
||||||
|
"core": "esp32",
|
||||||
|
"f_cpu": "240000000L",
|
||||||
|
"f_flash": "40000000L",
|
||||||
|
"flash_mode": "dio",
|
||||||
|
"mcu": "esp32",
|
||||||
|
"variant": "esp32"
|
||||||
|
},
|
||||||
|
"connectivity": [
|
||||||
|
"wifi",
|
||||||
|
"ethernet"
|
||||||
|
],
|
||||||
|
"debug": {
|
||||||
|
"openocd_board": "esp32.cfg"
|
||||||
|
},
|
||||||
|
"frameworks": [
|
||||||
|
"arduino",
|
||||||
|
"espidf"
|
||||||
|
],
|
||||||
|
"name": "Espressif ESP32 Dev Module",
|
||||||
|
"upload": {
|
||||||
|
"flash_size": "16MB",
|
||||||
|
"maximum_ram_size": 327680,
|
||||||
|
"maximum_size": 16777216,
|
||||||
|
"require_upload_port": true,
|
||||||
|
"speed": 460800
|
||||||
|
},
|
||||||
|
"download": {
|
||||||
|
"speed": 230400
|
||||||
|
},
|
||||||
|
"url": "https://en.wikipedia.org/wiki/ESP32",
|
||||||
|
"vendor": "Espressif"
|
||||||
|
}
|
||||||
47
boards/s2_4M_P.json
Normal file
47
boards/s2_4M_P.json
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"build": {
|
||||||
|
"arduino": {
|
||||||
|
"ldscript": "esp32s2_out.ld"
|
||||||
|
},
|
||||||
|
"core": "esp32",
|
||||||
|
"extra_flags": [
|
||||||
|
"-DBOARD_HAS_PSRAM",
|
||||||
|
"-DTASMOTA_SDK",
|
||||||
|
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||||
|
"-DARDUINO_USB_MODE=0"
|
||||||
|
],
|
||||||
|
"f_cpu": "240000000L",
|
||||||
|
"f_flash": "80000000L",
|
||||||
|
"flash_mode": "dio",
|
||||||
|
"hwids": [
|
||||||
|
[
|
||||||
|
"0X303A",
|
||||||
|
"0x80C2"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"mcu": "esp32s2",
|
||||||
|
"variant": "lolin_s2_mini"
|
||||||
|
},
|
||||||
|
"connectivity": [
|
||||||
|
"wifi"
|
||||||
|
],
|
||||||
|
"debug": {
|
||||||
|
"openocd_target": "esp32s2.cfg"
|
||||||
|
},
|
||||||
|
"frameworks": [
|
||||||
|
"arduino",
|
||||||
|
"espidf"
|
||||||
|
],
|
||||||
|
"name": "WEMOS LOLIN S2 Mini",
|
||||||
|
"upload": {
|
||||||
|
"flash_size": "4MB",
|
||||||
|
"maximum_ram_size": 327680,
|
||||||
|
"maximum_size": 4194304,
|
||||||
|
"use_1200bps_touch": true,
|
||||||
|
"wait_for_upload_port": true,
|
||||||
|
"require_upload_port": true,
|
||||||
|
"speed": 921600
|
||||||
|
},
|
||||||
|
"url": "https://www.wemos.cc/en/latest/s2/s2_mini.html",
|
||||||
|
"vendor": "WEMOS"
|
||||||
|
}
|
||||||
44
boards/s3_16M_P.json
Normal file
44
boards/s3_16M_P.json
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"build": {
|
||||||
|
"arduino": {
|
||||||
|
"ldscript": "esp32s3_out.ld",
|
||||||
|
"memory_type": "qio_opi"
|
||||||
|
},
|
||||||
|
"core": "esp32",
|
||||||
|
"extra_flags": [
|
||||||
|
"-DBOARD_HAS_PSRAM",
|
||||||
|
"-DARDUINO_USB_MODE=1",
|
||||||
|
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||||
|
"-DARDUINO_RUNNING_CORE=1",
|
||||||
|
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
||||||
|
],
|
||||||
|
"f_cpu": "240000000L",
|
||||||
|
"f_flash": "80000000L",
|
||||||
|
"flash_mode": "qio",
|
||||||
|
"mcu": "esp32s3",
|
||||||
|
"variant": "esp32s3"
|
||||||
|
},
|
||||||
|
"connectivity": [
|
||||||
|
"wifi"
|
||||||
|
],
|
||||||
|
"debug": {
|
||||||
|
"openocd_target": "esp32s3.cfg"
|
||||||
|
},
|
||||||
|
"frameworks": [
|
||||||
|
"arduino",
|
||||||
|
"espidf"
|
||||||
|
],
|
||||||
|
"name": "Espressif ESP32-S3 16M Flash OPI PSRAM, 4608KB Code/OTA, 2MB FS",
|
||||||
|
"upload": {
|
||||||
|
"flash_size": "16MB",
|
||||||
|
"maximum_ram_size": 327680,
|
||||||
|
"maximum_size": 16777216,
|
||||||
|
"require_upload_port": true,
|
||||||
|
"speed": 460800
|
||||||
|
},
|
||||||
|
"download": {
|
||||||
|
"speed": 230400
|
||||||
|
},
|
||||||
|
"url": "https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/hw-reference/esp32s3/",
|
||||||
|
"vendor": "Espressif"
|
||||||
|
}
|
||||||
37
boards/s3_32M_P.json
Normal file
37
boards/s3_32M_P.json
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"build": {
|
||||||
|
"arduino":{
|
||||||
|
"memory_type": "opi_opi"
|
||||||
|
},
|
||||||
|
"core": "esp32",
|
||||||
|
"extra_flags": "-DBOARD_HAS_PSRAM",
|
||||||
|
"f_cpu": "240000000L",
|
||||||
|
"f_flash": "80000000L",
|
||||||
|
"flash_mode": "opi",
|
||||||
|
"mcu": "esp32s3",
|
||||||
|
"variant": "esp32s3"
|
||||||
|
},
|
||||||
|
"connectivity": [
|
||||||
|
"wifi"
|
||||||
|
],
|
||||||
|
"debug": {
|
||||||
|
"openocd_target": "esp32s3.cfg"
|
||||||
|
},
|
||||||
|
"frameworks": [
|
||||||
|
"arduino",
|
||||||
|
"espidf"
|
||||||
|
],
|
||||||
|
"name": "Espressif ESP32-S3 32M Flash OPI PSRAM, 4608KB Code/OTA, 2MB FS",
|
||||||
|
"upload": {
|
||||||
|
"flash_size": "32MB",
|
||||||
|
"maximum_ram_size": 327680,
|
||||||
|
"maximum_size": 16777216,
|
||||||
|
"require_upload_port": true,
|
||||||
|
"speed": 460800
|
||||||
|
},
|
||||||
|
"download": {
|
||||||
|
"speed": 230400
|
||||||
|
},
|
||||||
|
"url": "https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/hw-reference/esp32s3/",
|
||||||
|
"vendor": "Espressif"
|
||||||
|
}
|
||||||
35
boards/s_16M.json
Normal file
35
boards/s_16M.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"build": {
|
||||||
|
"core": "esp32",
|
||||||
|
"extra_flags": "-DTASMOTA_SDK",
|
||||||
|
"f_cpu": "240000000L",
|
||||||
|
"f_flash": "40000000L",
|
||||||
|
"flash_mode": "dio",
|
||||||
|
"mcu": "esp32",
|
||||||
|
"variant": "esp32"
|
||||||
|
},
|
||||||
|
"connectivity": [
|
||||||
|
"wifi",
|
||||||
|
"ethernet"
|
||||||
|
],
|
||||||
|
"debug": {
|
||||||
|
"openocd_target": "esp32.cfg"
|
||||||
|
},
|
||||||
|
"frameworks": [
|
||||||
|
"arduino",
|
||||||
|
"espidf"
|
||||||
|
],
|
||||||
|
"name": "Espressif ESP32 16M Flash, 4608KB Code/OTA, 2MB FS",
|
||||||
|
"upload": {
|
||||||
|
"flash_size": "16MB",
|
||||||
|
"maximum_ram_size": 327680,
|
||||||
|
"maximum_size": 16777216,
|
||||||
|
"require_upload_port": true,
|
||||||
|
"speed": 460800
|
||||||
|
},
|
||||||
|
"download": {
|
||||||
|
"speed": 230400
|
||||||
|
},
|
||||||
|
"url": "https://en.wikipedia.org/wiki/ESP32",
|
||||||
|
"vendor": "Espressif"
|
||||||
|
}
|
||||||
35
boards/s_16M_P.json
Normal file
35
boards/s_16M_P.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"build": {
|
||||||
|
"core": "esp32",
|
||||||
|
"extra_flags": "-DBOARD_HAS_PSRAM",
|
||||||
|
"f_cpu": "240000000L",
|
||||||
|
"f_flash": "80000000L",
|
||||||
|
"flash_mode": "dio",
|
||||||
|
"mcu": "esp32",
|
||||||
|
"variant": "esp32"
|
||||||
|
},
|
||||||
|
"connectivity": [
|
||||||
|
"wifi",
|
||||||
|
"ethernet"
|
||||||
|
],
|
||||||
|
"debug": {
|
||||||
|
"openocd_target": "esp32.cfg"
|
||||||
|
},
|
||||||
|
"frameworks": [
|
||||||
|
"arduino",
|
||||||
|
"espidf"
|
||||||
|
],
|
||||||
|
"name": "Espressif ESP32 16M Flash DIO PSRAM, 4608KB Code/OTA, 2MB FS",
|
||||||
|
"upload": {
|
||||||
|
"flash_size": "16MB",
|
||||||
|
"maximum_ram_size": 327680,
|
||||||
|
"maximum_size": 16777216,
|
||||||
|
"require_upload_port": true,
|
||||||
|
"speed": 460800
|
||||||
|
},
|
||||||
|
"download": {
|
||||||
|
"speed": 230400
|
||||||
|
},
|
||||||
|
"url": "https://en.wikipedia.org/wiki/ESP32",
|
||||||
|
"vendor": "Espressif"
|
||||||
|
}
|
||||||
34
boards/s_4M.json
Normal file
34
boards/s_4M.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"build": {
|
||||||
|
"core": "esp32",
|
||||||
|
"extra_flags": "-DTASMOTA_SDK",
|
||||||
|
"f_cpu": "240000000L",
|
||||||
|
"f_flash": "40000000L",
|
||||||
|
"flash_mode": "dio",
|
||||||
|
"mcu": "esp32",
|
||||||
|
"variant": "esp32"
|
||||||
|
},
|
||||||
|
"connectivity": [
|
||||||
|
"wifi"
|
||||||
|
],
|
||||||
|
"debug": {
|
||||||
|
"openocd_target": "esp32.cfg"
|
||||||
|
},
|
||||||
|
"frameworks": [
|
||||||
|
"arduino",
|
||||||
|
"espidf"
|
||||||
|
],
|
||||||
|
"name": "Tasmota ESP32 4M Flash, 4608KB Code/OTA, 2MB FS",
|
||||||
|
"upload": {
|
||||||
|
"flash_size": "4MB",
|
||||||
|
"maximum_ram_size": 327680,
|
||||||
|
"maximum_size": 4194304,
|
||||||
|
"require_upload_port": true,
|
||||||
|
"speed": 460800
|
||||||
|
},
|
||||||
|
"download": {
|
||||||
|
"speed": 230400
|
||||||
|
},
|
||||||
|
"url": "https://en.wikipedia.org/wiki/ESP32",
|
||||||
|
"vendor": "Espressif"
|
||||||
|
}
|
||||||
48
boards/seeed_xiao_esp32c6.json
Normal file
48
boards/seeed_xiao_esp32c6.json
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"build": {
|
||||||
|
"core": "esp32",
|
||||||
|
"extra_flags": [
|
||||||
|
"-DARDUINO_XIAO_ESP32C6",
|
||||||
|
"-DARDUINO_USB_MODE=1",
|
||||||
|
"-DARDUINO_USB_CDC_ON_BOOT=1"
|
||||||
|
],
|
||||||
|
"f_cpu": "160000000L",
|
||||||
|
"f_flash": "80000000L",
|
||||||
|
"flash_mode": "qio",
|
||||||
|
"hwids": [
|
||||||
|
[
|
||||||
|
"0x2886",
|
||||||
|
"0x0046"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"0x303a",
|
||||||
|
"0x1001"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"mcu": "esp32c6",
|
||||||
|
"variant": "XIAO_ESP32C6"
|
||||||
|
},
|
||||||
|
"connectivity": [
|
||||||
|
"wifi",
|
||||||
|
"bluetooth",
|
||||||
|
"zigbee",
|
||||||
|
"thread"
|
||||||
|
],
|
||||||
|
"debug": {
|
||||||
|
"openocd_target": "esp32c6.cfg"
|
||||||
|
},
|
||||||
|
"frameworks": [
|
||||||
|
"arduino",
|
||||||
|
"espidf"
|
||||||
|
],
|
||||||
|
"name": "Seeed Studio XIAO ESP32C6",
|
||||||
|
"upload": {
|
||||||
|
"flash_size": "4MB",
|
||||||
|
"maximum_ram_size": 327680,
|
||||||
|
"maximum_size": 4194304,
|
||||||
|
"require_upload_port": true,
|
||||||
|
"speed": 460800
|
||||||
|
},
|
||||||
|
"url": "https://wiki.seeedstudio.com/XIAO_ESP32C6_Getting_Started/",
|
||||||
|
"vendor": "Seeed Studio"
|
||||||
|
}
|
||||||
38
cspell.json
Normal file
38
cspell.json
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"$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",
|
||||||
|
"**/*.md",
|
||||||
|
"**/*.py",
|
||||||
|
"locale_translations.h",
|
||||||
|
"TZ.tsx",
|
||||||
|
"**/*.txt",
|
||||||
|
"build/**",
|
||||||
|
"**/i18n/**",
|
||||||
|
"/project-words.txt",
|
||||||
|
"Makefile",
|
||||||
|
"**/*.ini",
|
||||||
|
"**/*.json",
|
||||||
|
"src/core/modbus_entity_parameters.hpp",
|
||||||
|
"sdkconfig.*",
|
||||||
|
"managed_components/**",
|
||||||
|
"pnpm-*.yaml"
|
||||||
|
]
|
||||||
|
}
|
||||||
85
data/pre_load.json
Normal file
85
data/pre_load.json
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
{
|
||||||
|
"type": "settings",
|
||||||
|
"Network": {
|
||||||
|
"ssid": "my_wifi_ssid",
|
||||||
|
"bssid": "",
|
||||||
|
"password": "my_wifi_password",
|
||||||
|
"hostname": "ems-esp"
|
||||||
|
},
|
||||||
|
"AP": {
|
||||||
|
"provision_mode": 2,
|
||||||
|
"ssid": "ems-esp",
|
||||||
|
"password": "ems-esp-neo",
|
||||||
|
"channel": 1,
|
||||||
|
"ssid_hidden": false,
|
||||||
|
"max_clients": 4,
|
||||||
|
"local_ip": "192.168.4.1",
|
||||||
|
"gateway_ip": "192.168.4.1",
|
||||||
|
"subnet_mask": "255.255.255.0"
|
||||||
|
},
|
||||||
|
"MQTT": {
|
||||||
|
"enableTLS": false,
|
||||||
|
"rootCA": "",
|
||||||
|
"enabled": false,
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": 1883,
|
||||||
|
"base": "ems-esp",
|
||||||
|
"username": "username",
|
||||||
|
"password": "password",
|
||||||
|
"client_id": "ems-esp",
|
||||||
|
"entity_format": 1,
|
||||||
|
"publish_time_boiler": 10,
|
||||||
|
"publish_time_thermostat": 10,
|
||||||
|
"publish_time_solar": 10,
|
||||||
|
"publish_time_mixer": 10,
|
||||||
|
"publish_time_water": 10,
|
||||||
|
"publish_time_other": 60,
|
||||||
|
"publish_time_sensor": 10,
|
||||||
|
"publish_time_heartbeat": 60,
|
||||||
|
"mqtt_qos": 0,
|
||||||
|
"mqtt_retain": false,
|
||||||
|
"ha_enabled": false,
|
||||||
|
"nested_format": 1,
|
||||||
|
"discovery_prefix": "homeassistant",
|
||||||
|
"discovery_type": 0,
|
||||||
|
"publish_single": false,
|
||||||
|
"publish_single2cmd": false,
|
||||||
|
"send_response": false
|
||||||
|
},
|
||||||
|
"NTP": {
|
||||||
|
"enabled": true,
|
||||||
|
"server": "time.google.com",
|
||||||
|
"tz_label": "Europe/Amsterdam",
|
||||||
|
"tz_format": "CET-1CEST,M3.5.0,M10.5.0/3"
|
||||||
|
},
|
||||||
|
"Security": {
|
||||||
|
"jwt_secret": "ems-esp-neo",
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"username": "admin",
|
||||||
|
"password": "admin",
|
||||||
|
"admin": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "guest",
|
||||||
|
"password": "guest",
|
||||||
|
"admin": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Settings": {
|
||||||
|
"board_profile": "S3",
|
||||||
|
"locale": "en",
|
||||||
|
"tx_mode": 1,
|
||||||
|
"ems_bus_id": 11,
|
||||||
|
"boiler_heatingoff": false,
|
||||||
|
"hide_led": true,
|
||||||
|
"telnet_enabled": true,
|
||||||
|
"notoken_api": false,
|
||||||
|
"analog_enabled": true,
|
||||||
|
"fahrenheit": false,
|
||||||
|
"bool_format": 1,
|
||||||
|
"bool_dashboard": 1,
|
||||||
|
"enum_format": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
6209
docs/Modbus-Entity-Registers.md
Normal file
6209
docs/Modbus-Entity-Registers.md
Normal file
File diff suppressed because it is too large
Load Diff
5776
docs/dump_entities.csv
Normal file
5776
docs/dump_entities.csv
Normal file
File diff suppressed because it is too large
Load Diff
231
docs/dump_telegrams.csv
Normal file
231
docs/dump_telegrams.csv
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
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,HydrTemp,
|
||||||
|
0x23,JunkersSetMixer,fetched
|
||||||
|
0x27,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,
|
||||||
|
0xC0,RCErrorMessage,
|
||||||
|
0xC2,UBAErrorMessage3,
|
||||||
|
0xC6,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,JunkersDisp,fetched
|
||||||
|
0x012E,HPEnergy1,
|
||||||
|
0x013B,HPEnergy2,
|
||||||
|
0x0165,JunkersSet,
|
||||||
|
0x0166,JunkersSet,
|
||||||
|
0x0167,JunkersSet,
|
||||||
|
0x0168,JunkersSet,
|
||||||
|
0x016E,Absent,fetched
|
||||||
|
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
|
||||||
|
0x0241,RC300Settings,fetched
|
||||||
|
0x0267,RC300Floordry,
|
||||||
|
0x0269,RC300Holiday,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,HPPressure,fetched
|
||||||
|
0x02CD,MMPLUSConfigMessage,fetched
|
||||||
|
0x02CE,RC300Set2,
|
||||||
|
0x02D0,RC300Set2,
|
||||||
|
0x02D2,RC300Set2,
|
||||||
|
0x02D6,HPPump2,fetched
|
||||||
|
0x02D7,MMPLUSStatusMessage,
|
||||||
|
0x02E0,UBASetPoints,
|
||||||
|
0x02F5,RC300WWmode,fetched
|
||||||
|
0x02F6,RC300WW2mode,fetched
|
||||||
|
0x0313,MMPLUSConfigMessage_WWC,fetched
|
||||||
|
0x031B,RC300WWtemp,fetched
|
||||||
|
0x031D,RC300WWmode2,
|
||||||
|
0x031E,RC300WWmode2,
|
||||||
|
0x0331,MMPLUSStatusMessage_WWC,
|
||||||
|
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
|
||||||
|
0x043F,CRHolidays,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
|
||||||
|
0x055C,VentilationSet,fetched
|
||||||
|
0x056B,VentilationMode,fetched
|
||||||
|
0x0583,VentilationMonitor,
|
||||||
|
0x0585,Blowerspeed,
|
||||||
|
0x0587,Bypass,
|
||||||
|
0x05BA,HpPoolStatus,fetched
|
||||||
|
0x05D9,Airquality,
|
||||||
|
0x0772,HIUSettings,
|
||||||
|
0x0779,HIUMonitor,
|
||||||
|
0x07A5,SM100wwCirc,fetched
|
||||||
|
0x07A6,SM100wwParam,fetched
|
||||||
|
0x07AA,SM100wwStatus,
|
||||||
|
0x07AB,SM100wwCommand,
|
||||||
|
0x07AC,SM100wwParam1,
|
||||||
|
0x07AD,SM100ValveStatus,
|
||||||
|
0x07AE,SM100wwKeepWarm,fetched
|
||||||
|
0x07D6,SM100wwTemperature,
|
||||||
|
0x07E0,SM100wwStatus2,fetched
|
||||||
|
0x0935,EM100SetMessage,fetched
|
||||||
|
0x0936,EM100OutMessage,
|
||||||
|
0x0937,EM100TempMessage,
|
||||||
|
0x0938,EM100InputMessage,
|
||||||
|
0x0939,EM100MonitorMessage,
|
||||||
|
0x093A,EM100ConfigMessage,
|
||||||
|
0x0998,HPSettings,fetched
|
||||||
|
0x0999,HPFunctionTest,fetched
|
||||||
|
0x099A,HPStarts,
|
||||||
|
0x099B,HPFlowTemp,
|
||||||
|
0x099C,HPComp,
|
||||||
|
0x09A0,HPTemperature,
|
||||||
|
4005
dump_entities.csv
4005
dump_entities.csv
File diff suppressed because it is too large
Load Diff
@@ -1,6 +0,0 @@
|
|||||||
# Name, Type, SubType, Offset, Size, Flags
|
|
||||||
nvs, data, nvs, 0x9000, 0x5000,
|
|
||||||
otadata, data, ota, , 0x2000,
|
|
||||||
app0, app, ota_0, , 0x7F0000,
|
|
||||||
app1, app, ota_1, , 0x7F0000,
|
|
||||||
spiffs, data, spiffs, , 64K,
|
|
||||||
|
@@ -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=false
|
|
||||||
|
|
||||||
; MQTT settings
|
; MQTT settings
|
||||||
-D FACTORY_MQTT_ENABLED=false
|
-D FACTORY_MQTT_ENABLED=false
|
||||||
-D FACTORY_MQTT_HOST=\"\"
|
-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,12 +0,0 @@
|
|||||||
node_modules/
|
|
||||||
build/
|
|
||||||
dist/
|
|
||||||
.yarn/
|
|
||||||
|
|
||||||
.prettierrc
|
|
||||||
.eslintrc*
|
|
||||||
env.d.ts
|
|
||||||
progmem-generator.js
|
|
||||||
unpack.ts
|
|
||||||
vite.config.ts
|
|
||||||
package.json
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
{
|
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
"es6": true
|
|
||||||
},
|
|
||||||
"extends": [
|
|
||||||
"eslint:recommended",
|
|
||||||
"airbnb/hooks",
|
|
||||||
"airbnb-typescript",
|
|
||||||
"plugin:react/recommended",
|
|
||||||
"plugin:react/jsx-runtime",
|
|
||||||
"plugin:@typescript-eslint/recommended",
|
|
||||||
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
|
||||||
"plugin:prettier/recommended",
|
|
||||||
"plugin:import/recommended"
|
|
||||||
],
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaFeatures": {
|
|
||||||
"jsx": true
|
|
||||||
},
|
|
||||||
"ecmaVersion": "latest",
|
|
||||||
"sourceType": "module",
|
|
||||||
"tsconfigRootDir": ".",
|
|
||||||
"project": ["tsconfig.json"]
|
|
||||||
},
|
|
||||||
"plugins": ["react", "@typescript-eslint", "autofix", "react-hooks"],
|
|
||||||
"settings": {
|
|
||||||
"import/resolver": {
|
|
||||||
"typescript": {
|
|
||||||
"project": "./tsconfig.json"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"react": {
|
|
||||||
"version": "18.x"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"rules": {
|
|
||||||
"object-shorthand": "error",
|
|
||||||
"no-console": "warn",
|
|
||||||
"@typescript-eslint/consistent-type-definitions": ["off", "type"],
|
|
||||||
"@typescript-eslint/explicit-function-return-type": "off",
|
|
||||||
"@typescript-eslint/no-unsafe-call": "off",
|
|
||||||
"@typescript-eslint/no-unsafe-enum-comparison": "off",
|
|
||||||
"@typescript-eslint/no-unsafe-assignment": "off",
|
|
||||||
"@typescript-eslint/no-unsafe-return": "off",
|
|
||||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
|
||||||
"@typescript-eslint/naming-convention": "off",
|
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
|
||||||
"@typescript-eslint/no-unsafe-argument": "off",
|
|
||||||
"@typescript-eslint/restrict-plus-operands": "off",
|
|
||||||
"@typescript-eslint/no-unused-expressions": "off",
|
|
||||||
"@typescript-eslint/no-implied-eval": "off",
|
|
||||||
"@typescript-eslint/no-misused-promises": "off",
|
|
||||||
"arrow-body-style": ["error", "as-needed"],
|
|
||||||
"react-hooks/exhaustive-deps": "warn",
|
|
||||||
"@typescript-eslint/consistent-type-imports": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"prefer": "type-imports"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"import/order": [
|
|
||||||
"warn",
|
|
||||||
{
|
|
||||||
"groups": ["builtin", "external", "parent", "sibling", "index", "object", "type"],
|
|
||||||
"pathGroups": [
|
|
||||||
{
|
|
||||||
"pattern": "@/**/**",
|
|
||||||
"group": "parent",
|
|
||||||
"position": "before"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"alphabetize": { "order": "asc" }
|
|
||||||
}
|
|
||||||
],
|
|
||||||
// "autofix/no-unused-vars": [
|
|
||||||
// "error",
|
|
||||||
// {
|
|
||||||
// "argsIgnorePattern": "^_",
|
|
||||||
// "ignoreRestSiblings": true,
|
|
||||||
// "destructuredArrayIgnorePattern": "^_"
|
|
||||||
// }
|
|
||||||
// ],
|
|
||||||
"react/self-closing-comp": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"component": true,
|
|
||||||
"html": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"@typescript-eslint/ban-types": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"extendDefaults": true,
|
|
||||||
"types": {
|
|
||||||
"{}": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"prettier/prettier": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"endOfLine": "auto"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
7
interface/.gitignore
vendored
7
interface/.gitignore
vendored
@@ -1,7 +0,0 @@
|
|||||||
.pnp.*
|
|
||||||
.yarn/*
|
|
||||||
!.yarn/patches
|
|
||||||
!.yarn/plugins
|
|
||||||
!.yarn/releases
|
|
||||||
!.yarn/sdks
|
|
||||||
!.yarn/versions
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
build/
|
build/
|
||||||
dist/
|
dist/
|
||||||
|
src/i18n/*
|
||||||
|
|
||||||
.prettierrc
|
.prettierrc
|
||||||
.yarn/
|
.typesafe-i18n.json
|
||||||
.typesafe-i18n.json
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"adapter": "react",
|
"adapter": "react",
|
||||||
"baseLocale": "pl",
|
"baseLocale": "pl",
|
||||||
"$schema": "https://unpkg.com/typesafe-i18n@5.26.0/schema/typesafe-i18n.json"
|
"$schema": "https://unpkg.com/typesafe-i18n@5.26.2/schema/typesafe-i18n.json"
|
||||||
}
|
}
|
||||||
File diff suppressed because one or more lines are too long
873
interface/.yarn/releases/yarn-3.4.1.cjs
vendored
873
interface/.yarn/releases/yarn-3.4.1.cjs
vendored
File diff suppressed because one or more lines are too long
20
interface/.yarn/sdks/eslint/bin/eslint.js
vendored
20
interface/.yarn/sdks/eslint/bin/eslint.js
vendored
@@ -1,20 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
const {existsSync} = require(`fs`);
|
|
||||||
const {createRequire} = require(`module`);
|
|
||||||
const {resolve} = require(`path`);
|
|
||||||
|
|
||||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
|
||||||
|
|
||||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
|
||||||
const absRequire = createRequire(absPnpApiPath);
|
|
||||||
|
|
||||||
if (existsSync(absPnpApiPath)) {
|
|
||||||
if (!process.versions.pnp) {
|
|
||||||
// Setup the environment to be able to require eslint/bin/eslint.js
|
|
||||||
require(absPnpApiPath).setup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Defer to the real eslint/bin/eslint.js your application uses
|
|
||||||
module.exports = absRequire(`eslint/bin/eslint.js`);
|
|
||||||
20
interface/.yarn/sdks/eslint/lib/api.js
vendored
20
interface/.yarn/sdks/eslint/lib/api.js
vendored
@@ -1,20 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
const {existsSync} = require(`fs`);
|
|
||||||
const {createRequire} = require(`module`);
|
|
||||||
const {resolve} = require(`path`);
|
|
||||||
|
|
||||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
|
||||||
|
|
||||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
|
||||||
const absRequire = createRequire(absPnpApiPath);
|
|
||||||
|
|
||||||
if (existsSync(absPnpApiPath)) {
|
|
||||||
if (!process.versions.pnp) {
|
|
||||||
// Setup the environment to be able to require eslint
|
|
||||||
require(absPnpApiPath).setup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Defer to the real eslint your application uses
|
|
||||||
module.exports = absRequire(`eslint`);
|
|
||||||
6
interface/.yarn/sdks/eslint/package.json
vendored
6
interface/.yarn/sdks/eslint/package.json
vendored
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "eslint",
|
|
||||||
"version": "8.36.0-sdk",
|
|
||||||
"main": "./lib/api.js",
|
|
||||||
"type": "commonjs"
|
|
||||||
}
|
|
||||||
5
interface/.yarn/sdks/integrations.yml
vendored
5
interface/.yarn/sdks/integrations.yml
vendored
@@ -1,5 +0,0 @@
|
|||||||
# This file is automatically generated by @yarnpkg/sdks.
|
|
||||||
# Manual changes might be lost!
|
|
||||||
|
|
||||||
integrations:
|
|
||||||
- vscode
|
|
||||||
20
interface/.yarn/sdks/prettier/index.js
vendored
20
interface/.yarn/sdks/prettier/index.js
vendored
@@ -1,20 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
const {existsSync} = require(`fs`);
|
|
||||||
const {createRequire} = require(`module`);
|
|
||||||
const {resolve} = require(`path`);
|
|
||||||
|
|
||||||
const relPnpApiPath = "../../../.pnp.cjs";
|
|
||||||
|
|
||||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
|
||||||
const absRequire = createRequire(absPnpApiPath);
|
|
||||||
|
|
||||||
if (existsSync(absPnpApiPath)) {
|
|
||||||
if (!process.versions.pnp) {
|
|
||||||
// Setup the environment to be able to require prettier/index.js
|
|
||||||
require(absPnpApiPath).setup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Defer to the real prettier/index.js your application uses
|
|
||||||
module.exports = absRequire(`prettier/index.js`);
|
|
||||||
6
interface/.yarn/sdks/prettier/package.json
vendored
6
interface/.yarn/sdks/prettier/package.json
vendored
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "prettier",
|
|
||||||
"version": "2.8.7-sdk",
|
|
||||||
"main": "./index.js",
|
|
||||||
"type": "commonjs"
|
|
||||||
}
|
|
||||||
20
interface/.yarn/sdks/typescript/bin/tsc
vendored
20
interface/.yarn/sdks/typescript/bin/tsc
vendored
@@ -1,20 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
const {existsSync} = require(`fs`);
|
|
||||||
const {createRequire} = require(`module`);
|
|
||||||
const {resolve} = require(`path`);
|
|
||||||
|
|
||||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
|
||||||
|
|
||||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
|
||||||
const absRequire = createRequire(absPnpApiPath);
|
|
||||||
|
|
||||||
if (existsSync(absPnpApiPath)) {
|
|
||||||
if (!process.versions.pnp) {
|
|
||||||
// Setup the environment to be able to require typescript/bin/tsc
|
|
||||||
require(absPnpApiPath).setup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Defer to the real typescript/bin/tsc your application uses
|
|
||||||
module.exports = absRequire(`typescript/bin/tsc`);
|
|
||||||
20
interface/.yarn/sdks/typescript/bin/tsserver
vendored
20
interface/.yarn/sdks/typescript/bin/tsserver
vendored
@@ -1,20 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
const {existsSync} = require(`fs`);
|
|
||||||
const {createRequire} = require(`module`);
|
|
||||||
const {resolve} = require(`path`);
|
|
||||||
|
|
||||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
|
||||||
|
|
||||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
|
||||||
const absRequire = createRequire(absPnpApiPath);
|
|
||||||
|
|
||||||
if (existsSync(absPnpApiPath)) {
|
|
||||||
if (!process.versions.pnp) {
|
|
||||||
// Setup the environment to be able to require typescript/bin/tsserver
|
|
||||||
require(absPnpApiPath).setup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Defer to the real typescript/bin/tsserver your application uses
|
|
||||||
module.exports = absRequire(`typescript/bin/tsserver`);
|
|
||||||
20
interface/.yarn/sdks/typescript/lib/tsc.js
vendored
20
interface/.yarn/sdks/typescript/lib/tsc.js
vendored
@@ -1,20 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
const {existsSync} = require(`fs`);
|
|
||||||
const {createRequire} = require(`module`);
|
|
||||||
const {resolve} = require(`path`);
|
|
||||||
|
|
||||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
|
||||||
|
|
||||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
|
||||||
const absRequire = createRequire(absPnpApiPath);
|
|
||||||
|
|
||||||
if (existsSync(absPnpApiPath)) {
|
|
||||||
if (!process.versions.pnp) {
|
|
||||||
// Setup the environment to be able to require typescript/lib/tsc.js
|
|
||||||
require(absPnpApiPath).setup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Defer to the real typescript/lib/tsc.js your application uses
|
|
||||||
module.exports = absRequire(`typescript/lib/tsc.js`);
|
|
||||||
223
interface/.yarn/sdks/typescript/lib/tsserver.js
vendored
223
interface/.yarn/sdks/typescript/lib/tsserver.js
vendored
@@ -1,223 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
const {existsSync} = require(`fs`);
|
|
||||||
const {createRequire} = require(`module`);
|
|
||||||
const {resolve} = require(`path`);
|
|
||||||
|
|
||||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
|
||||||
|
|
||||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
|
||||||
const absRequire = createRequire(absPnpApiPath);
|
|
||||||
|
|
||||||
const moduleWrapper = tsserver => {
|
|
||||||
if (!process.versions.pnp) {
|
|
||||||
return tsserver;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {isAbsolute} = require(`path`);
|
|
||||||
const pnpApi = require(`pnpapi`);
|
|
||||||
|
|
||||||
const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//);
|
|
||||||
const isPortal = str => str.startsWith("portal:/");
|
|
||||||
const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`);
|
|
||||||
|
|
||||||
const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => {
|
|
||||||
return `${locator.name}@${locator.reference}`;
|
|
||||||
}));
|
|
||||||
|
|
||||||
// VSCode sends the zip paths to TS using the "zip://" prefix, that TS
|
|
||||||
// doesn't understand. This layer makes sure to remove the protocol
|
|
||||||
// before forwarding it to TS, and to add it back on all returned paths.
|
|
||||||
|
|
||||||
function toEditorPath(str) {
|
|
||||||
// We add the `zip:` prefix to both `.zip/` paths and virtual paths
|
|
||||||
if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) {
|
|
||||||
// We also take the opportunity to turn virtual paths into physical ones;
|
|
||||||
// this makes it much easier to work with workspaces that list peer
|
|
||||||
// dependencies, since otherwise Ctrl+Click would bring us to the virtual
|
|
||||||
// file instances instead of the real ones.
|
|
||||||
//
|
|
||||||
// We only do this to modules owned by the the dependency tree roots.
|
|
||||||
// This avoids breaking the resolution when jumping inside a vendor
|
|
||||||
// with peer dep (otherwise jumping into react-dom would show resolution
|
|
||||||
// errors on react).
|
|
||||||
//
|
|
||||||
const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str;
|
|
||||||
if (resolved) {
|
|
||||||
const locator = pnpApi.findPackageLocator(resolved);
|
|
||||||
if (locator && (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))) {
|
|
||||||
str = resolved;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
str = normalize(str);
|
|
||||||
|
|
||||||
if (str.match(/\.zip\//)) {
|
|
||||||
switch (hostInfo) {
|
|
||||||
// Absolute VSCode `Uri.fsPath`s need to start with a slash.
|
|
||||||
// VSCode only adds it automatically for supported schemes,
|
|
||||||
// so we have to do it manually for the `zip` scheme.
|
|
||||||
// The path needs to start with a caret otherwise VSCode doesn't handle the protocol
|
|
||||||
//
|
|
||||||
// Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910
|
|
||||||
//
|
|
||||||
// 2021-10-08: VSCode changed the format in 1.61.
|
|
||||||
// Before | ^zip:/c:/foo/bar.zip/package.json
|
|
||||||
// After | ^/zip//c:/foo/bar.zip/package.json
|
|
||||||
//
|
|
||||||
// 2022-04-06: VSCode changed the format in 1.66.
|
|
||||||
// Before | ^/zip//c:/foo/bar.zip/package.json
|
|
||||||
// After | ^/zip/c:/foo/bar.zip/package.json
|
|
||||||
//
|
|
||||||
// 2022-05-06: VSCode changed the format in 1.68
|
|
||||||
// Before | ^/zip/c:/foo/bar.zip/package.json
|
|
||||||
// After | ^/zip//c:/foo/bar.zip/package.json
|
|
||||||
//
|
|
||||||
case `vscode <1.61`: {
|
|
||||||
str = `^zip:${str}`;
|
|
||||||
} break;
|
|
||||||
|
|
||||||
case `vscode <1.66`: {
|
|
||||||
str = `^/zip/${str}`;
|
|
||||||
} break;
|
|
||||||
|
|
||||||
case `vscode <1.68`: {
|
|
||||||
str = `^/zip${str}`;
|
|
||||||
} break;
|
|
||||||
|
|
||||||
case `vscode`: {
|
|
||||||
str = `^/zip/${str}`;
|
|
||||||
} break;
|
|
||||||
|
|
||||||
// To make "go to definition" work,
|
|
||||||
// We have to resolve the actual file system path from virtual path
|
|
||||||
// and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip)
|
|
||||||
case `coc-nvim`: {
|
|
||||||
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
|
||||||
str = resolve(`zipfile:${str}`);
|
|
||||||
} break;
|
|
||||||
|
|
||||||
// Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server)
|
|
||||||
// We have to resolve the actual file system path from virtual path,
|
|
||||||
// everything else is up to neovim
|
|
||||||
case `neovim`: {
|
|
||||||
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
|
||||||
str = `zipfile://${str}`;
|
|
||||||
} break;
|
|
||||||
|
|
||||||
default: {
|
|
||||||
str = `zip:${str}`;
|
|
||||||
} break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fromEditorPath(str) {
|
|
||||||
switch (hostInfo) {
|
|
||||||
case `coc-nvim`: {
|
|
||||||
str = str.replace(/\.zip::/, `.zip/`);
|
|
||||||
// The path for coc-nvim is in format of /<pwd>/zipfile:/<pwd>/.yarn/...
|
|
||||||
// So in order to convert it back, we use .* to match all the thing
|
|
||||||
// before `zipfile:`
|
|
||||||
return process.platform === `win32`
|
|
||||||
? str.replace(/^.*zipfile:\//, ``)
|
|
||||||
: str.replace(/^.*zipfile:/, ``);
|
|
||||||
} break;
|
|
||||||
|
|
||||||
case `neovim`: {
|
|
||||||
str = str.replace(/\.zip::/, `.zip/`);
|
|
||||||
// The path for neovim is in format of zipfile:///<pwd>/.yarn/...
|
|
||||||
return str.replace(/^zipfile:\/\//, ``);
|
|
||||||
} break;
|
|
||||||
|
|
||||||
case `vscode`:
|
|
||||||
default: {
|
|
||||||
return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`)
|
|
||||||
} break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force enable 'allowLocalPluginLoads'
|
|
||||||
// TypeScript tries to resolve plugins using a path relative to itself
|
|
||||||
// which doesn't work when using the global cache
|
|
||||||
// https://github.com/microsoft/TypeScript/blob/1b57a0395e0bff191581c9606aab92832001de62/src/server/project.ts#L2238
|
|
||||||
// VSCode doesn't want to enable 'allowLocalPluginLoads' due to security concerns but
|
|
||||||
// TypeScript already does local loads and if this code is running the user trusts the workspace
|
|
||||||
// https://github.com/microsoft/vscode/issues/45856
|
|
||||||
const ConfiguredProject = tsserver.server.ConfiguredProject;
|
|
||||||
const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype;
|
|
||||||
ConfiguredProject.prototype.enablePluginsWithOptions = function() {
|
|
||||||
this.projectService.allowLocalPluginLoads = true;
|
|
||||||
return originalEnablePluginsWithOptions.apply(this, arguments);
|
|
||||||
};
|
|
||||||
|
|
||||||
// And here is the point where we hijack the VSCode <-> TS communications
|
|
||||||
// by adding ourselves in the middle. We locate everything that looks
|
|
||||||
// like an absolute path of ours and normalize it.
|
|
||||||
|
|
||||||
const Session = tsserver.server.Session;
|
|
||||||
const {onMessage: originalOnMessage, send: originalSend} = Session.prototype;
|
|
||||||
let hostInfo = `unknown`;
|
|
||||||
|
|
||||||
Object.assign(Session.prototype, {
|
|
||||||
onMessage(/** @type {string | object} */ message) {
|
|
||||||
const isStringMessage = typeof message === 'string';
|
|
||||||
const parsedMessage = isStringMessage ? JSON.parse(message) : message;
|
|
||||||
|
|
||||||
if (
|
|
||||||
parsedMessage != null &&
|
|
||||||
typeof parsedMessage === `object` &&
|
|
||||||
parsedMessage.arguments &&
|
|
||||||
typeof parsedMessage.arguments.hostInfo === `string`
|
|
||||||
) {
|
|
||||||
hostInfo = parsedMessage.arguments.hostInfo;
|
|
||||||
if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) {
|
|
||||||
const [, major, minor] = (process.env.VSCODE_IPC_HOOK.match(
|
|
||||||
// The RegExp from https://semver.org/ but without the caret at the start
|
|
||||||
/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
|
|
||||||
) ?? []).map(Number)
|
|
||||||
|
|
||||||
if (major === 1) {
|
|
||||||
if (minor < 61) {
|
|
||||||
hostInfo += ` <1.61`;
|
|
||||||
} else if (minor < 66) {
|
|
||||||
hostInfo += ` <1.66`;
|
|
||||||
} else if (minor < 68) {
|
|
||||||
hostInfo += ` <1.68`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => {
|
|
||||||
return typeof value === 'string' ? fromEditorPath(value) : value;
|
|
||||||
});
|
|
||||||
|
|
||||||
return originalOnMessage.call(
|
|
||||||
this,
|
|
||||||
isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
send(/** @type {any} */ msg) {
|
|
||||||
return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => {
|
|
||||||
return typeof value === `string` ? toEditorPath(value) : value;
|
|
||||||
})));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return tsserver;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (existsSync(absPnpApiPath)) {
|
|
||||||
if (!process.versions.pnp) {
|
|
||||||
// Setup the environment to be able to require typescript/lib/tsserver.js
|
|
||||||
require(absPnpApiPath).setup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Defer to the real typescript/lib/tsserver.js your application uses
|
|
||||||
module.exports = moduleWrapper(absRequire(`typescript/lib/tsserver.js`));
|
|
||||||
@@ -1,223 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
const {existsSync} = require(`fs`);
|
|
||||||
const {createRequire} = require(`module`);
|
|
||||||
const {resolve} = require(`path`);
|
|
||||||
|
|
||||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
|
||||||
|
|
||||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
|
||||||
const absRequire = createRequire(absPnpApiPath);
|
|
||||||
|
|
||||||
const moduleWrapper = tsserver => {
|
|
||||||
if (!process.versions.pnp) {
|
|
||||||
return tsserver;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {isAbsolute} = require(`path`);
|
|
||||||
const pnpApi = require(`pnpapi`);
|
|
||||||
|
|
||||||
const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//);
|
|
||||||
const isPortal = str => str.startsWith("portal:/");
|
|
||||||
const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`);
|
|
||||||
|
|
||||||
const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => {
|
|
||||||
return `${locator.name}@${locator.reference}`;
|
|
||||||
}));
|
|
||||||
|
|
||||||
// VSCode sends the zip paths to TS using the "zip://" prefix, that TS
|
|
||||||
// doesn't understand. This layer makes sure to remove the protocol
|
|
||||||
// before forwarding it to TS, and to add it back on all returned paths.
|
|
||||||
|
|
||||||
function toEditorPath(str) {
|
|
||||||
// We add the `zip:` prefix to both `.zip/` paths and virtual paths
|
|
||||||
if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) {
|
|
||||||
// We also take the opportunity to turn virtual paths into physical ones;
|
|
||||||
// this makes it much easier to work with workspaces that list peer
|
|
||||||
// dependencies, since otherwise Ctrl+Click would bring us to the virtual
|
|
||||||
// file instances instead of the real ones.
|
|
||||||
//
|
|
||||||
// We only do this to modules owned by the the dependency tree roots.
|
|
||||||
// This avoids breaking the resolution when jumping inside a vendor
|
|
||||||
// with peer dep (otherwise jumping into react-dom would show resolution
|
|
||||||
// errors on react).
|
|
||||||
//
|
|
||||||
const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str;
|
|
||||||
if (resolved) {
|
|
||||||
const locator = pnpApi.findPackageLocator(resolved);
|
|
||||||
if (locator && (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))) {
|
|
||||||
str = resolved;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
str = normalize(str);
|
|
||||||
|
|
||||||
if (str.match(/\.zip\//)) {
|
|
||||||
switch (hostInfo) {
|
|
||||||
// Absolute VSCode `Uri.fsPath`s need to start with a slash.
|
|
||||||
// VSCode only adds it automatically for supported schemes,
|
|
||||||
// so we have to do it manually for the `zip` scheme.
|
|
||||||
// The path needs to start with a caret otherwise VSCode doesn't handle the protocol
|
|
||||||
//
|
|
||||||
// Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910
|
|
||||||
//
|
|
||||||
// 2021-10-08: VSCode changed the format in 1.61.
|
|
||||||
// Before | ^zip:/c:/foo/bar.zip/package.json
|
|
||||||
// After | ^/zip//c:/foo/bar.zip/package.json
|
|
||||||
//
|
|
||||||
// 2022-04-06: VSCode changed the format in 1.66.
|
|
||||||
// Before | ^/zip//c:/foo/bar.zip/package.json
|
|
||||||
// After | ^/zip/c:/foo/bar.zip/package.json
|
|
||||||
//
|
|
||||||
// 2022-05-06: VSCode changed the format in 1.68
|
|
||||||
// Before | ^/zip/c:/foo/bar.zip/package.json
|
|
||||||
// After | ^/zip//c:/foo/bar.zip/package.json
|
|
||||||
//
|
|
||||||
case `vscode <1.61`: {
|
|
||||||
str = `^zip:${str}`;
|
|
||||||
} break;
|
|
||||||
|
|
||||||
case `vscode <1.66`: {
|
|
||||||
str = `^/zip/${str}`;
|
|
||||||
} break;
|
|
||||||
|
|
||||||
case `vscode <1.68`: {
|
|
||||||
str = `^/zip${str}`;
|
|
||||||
} break;
|
|
||||||
|
|
||||||
case `vscode`: {
|
|
||||||
str = `^/zip/${str}`;
|
|
||||||
} break;
|
|
||||||
|
|
||||||
// To make "go to definition" work,
|
|
||||||
// We have to resolve the actual file system path from virtual path
|
|
||||||
// and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip)
|
|
||||||
case `coc-nvim`: {
|
|
||||||
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
|
||||||
str = resolve(`zipfile:${str}`);
|
|
||||||
} break;
|
|
||||||
|
|
||||||
// Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server)
|
|
||||||
// We have to resolve the actual file system path from virtual path,
|
|
||||||
// everything else is up to neovim
|
|
||||||
case `neovim`: {
|
|
||||||
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
|
||||||
str = `zipfile://${str}`;
|
|
||||||
} break;
|
|
||||||
|
|
||||||
default: {
|
|
||||||
str = `zip:${str}`;
|
|
||||||
} break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fromEditorPath(str) {
|
|
||||||
switch (hostInfo) {
|
|
||||||
case `coc-nvim`: {
|
|
||||||
str = str.replace(/\.zip::/, `.zip/`);
|
|
||||||
// The path for coc-nvim is in format of /<pwd>/zipfile:/<pwd>/.yarn/...
|
|
||||||
// So in order to convert it back, we use .* to match all the thing
|
|
||||||
// before `zipfile:`
|
|
||||||
return process.platform === `win32`
|
|
||||||
? str.replace(/^.*zipfile:\//, ``)
|
|
||||||
: str.replace(/^.*zipfile:/, ``);
|
|
||||||
} break;
|
|
||||||
|
|
||||||
case `neovim`: {
|
|
||||||
str = str.replace(/\.zip::/, `.zip/`);
|
|
||||||
// The path for neovim is in format of zipfile:///<pwd>/.yarn/...
|
|
||||||
return str.replace(/^zipfile:\/\//, ``);
|
|
||||||
} break;
|
|
||||||
|
|
||||||
case `vscode`:
|
|
||||||
default: {
|
|
||||||
return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`)
|
|
||||||
} break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force enable 'allowLocalPluginLoads'
|
|
||||||
// TypeScript tries to resolve plugins using a path relative to itself
|
|
||||||
// which doesn't work when using the global cache
|
|
||||||
// https://github.com/microsoft/TypeScript/blob/1b57a0395e0bff191581c9606aab92832001de62/src/server/project.ts#L2238
|
|
||||||
// VSCode doesn't want to enable 'allowLocalPluginLoads' due to security concerns but
|
|
||||||
// TypeScript already does local loads and if this code is running the user trusts the workspace
|
|
||||||
// https://github.com/microsoft/vscode/issues/45856
|
|
||||||
const ConfiguredProject = tsserver.server.ConfiguredProject;
|
|
||||||
const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype;
|
|
||||||
ConfiguredProject.prototype.enablePluginsWithOptions = function() {
|
|
||||||
this.projectService.allowLocalPluginLoads = true;
|
|
||||||
return originalEnablePluginsWithOptions.apply(this, arguments);
|
|
||||||
};
|
|
||||||
|
|
||||||
// And here is the point where we hijack the VSCode <-> TS communications
|
|
||||||
// by adding ourselves in the middle. We locate everything that looks
|
|
||||||
// like an absolute path of ours and normalize it.
|
|
||||||
|
|
||||||
const Session = tsserver.server.Session;
|
|
||||||
const {onMessage: originalOnMessage, send: originalSend} = Session.prototype;
|
|
||||||
let hostInfo = `unknown`;
|
|
||||||
|
|
||||||
Object.assign(Session.prototype, {
|
|
||||||
onMessage(/** @type {string | object} */ message) {
|
|
||||||
const isStringMessage = typeof message === 'string';
|
|
||||||
const parsedMessage = isStringMessage ? JSON.parse(message) : message;
|
|
||||||
|
|
||||||
if (
|
|
||||||
parsedMessage != null &&
|
|
||||||
typeof parsedMessage === `object` &&
|
|
||||||
parsedMessage.arguments &&
|
|
||||||
typeof parsedMessage.arguments.hostInfo === `string`
|
|
||||||
) {
|
|
||||||
hostInfo = parsedMessage.arguments.hostInfo;
|
|
||||||
if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) {
|
|
||||||
const [, major, minor] = (process.env.VSCODE_IPC_HOOK.match(
|
|
||||||
// The RegExp from https://semver.org/ but without the caret at the start
|
|
||||||
/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
|
|
||||||
) ?? []).map(Number)
|
|
||||||
|
|
||||||
if (major === 1) {
|
|
||||||
if (minor < 61) {
|
|
||||||
hostInfo += ` <1.61`;
|
|
||||||
} else if (minor < 66) {
|
|
||||||
hostInfo += ` <1.66`;
|
|
||||||
} else if (minor < 68) {
|
|
||||||
hostInfo += ` <1.68`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => {
|
|
||||||
return typeof value === 'string' ? fromEditorPath(value) : value;
|
|
||||||
});
|
|
||||||
|
|
||||||
return originalOnMessage.call(
|
|
||||||
this,
|
|
||||||
isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
send(/** @type {any} */ msg) {
|
|
||||||
return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => {
|
|
||||||
return typeof value === `string` ? toEditorPath(value) : value;
|
|
||||||
})));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return tsserver;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (existsSync(absPnpApiPath)) {
|
|
||||||
if (!process.versions.pnp) {
|
|
||||||
// Setup the environment to be able to require typescript/lib/tsserverlibrary.js
|
|
||||||
require(absPnpApiPath).setup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Defer to the real typescript/lib/tsserverlibrary.js your application uses
|
|
||||||
module.exports = moduleWrapper(absRequire(`typescript/lib/tsserverlibrary.js`));
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
const {existsSync} = require(`fs`);
|
|
||||||
const {createRequire} = require(`module`);
|
|
||||||
const {resolve} = require(`path`);
|
|
||||||
|
|
||||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
|
||||||
|
|
||||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
|
||||||
const absRequire = createRequire(absPnpApiPath);
|
|
||||||
|
|
||||||
if (existsSync(absPnpApiPath)) {
|
|
||||||
if (!process.versions.pnp) {
|
|
||||||
// Setup the environment to be able to require typescript/lib/typescript.js
|
|
||||||
require(absPnpApiPath).setup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Defer to the real typescript/lib/typescript.js your application uses
|
|
||||||
module.exports = absRequire(`typescript/lib/typescript.js`);
|
|
||||||
6
interface/.yarn/sdks/typescript/package.json
vendored
6
interface/.yarn/sdks/typescript/package.json
vendored
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "typescript",
|
|
||||||
"version": "5.0.2-sdk",
|
|
||||||
"main": "./lib/typescript.js",
|
|
||||||
"type": "commonjs"
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
plugins:
|
|
||||||
- path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs
|
|
||||||
spec: '@yarnpkg/plugin-typescript'
|
|
||||||
|
|
||||||
yarnPath: .yarn/releases/yarn-3.4.1.cjs
|
|
||||||
|
|
||||||
# uing pnp
|
|
||||||
# nodeLinker: pnp
|
|
||||||
|
|
||||||
# use these if not using PnP and have node_modules
|
|
||||||
nodeLinker: node-modules
|
|
||||||
compressionLevel: 0
|
|
||||||
nmMode: hardlinks-local
|
|
||||||
enableGlobalCache: true
|
|
||||||
44
interface/eslint.config.js
Normal file
44
interface/eslint.config.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
// @ts-check
|
||||||
|
import eslint from '@eslint/js';
|
||||||
|
import prettierConfig from 'eslint-config-prettier';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
eslint.configs.recommended,
|
||||||
|
...tseslint.configs.recommendedTypeChecked,
|
||||||
|
prettierConfig,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
project: true,
|
||||||
|
tsconfigRootDir: import.meta.dirname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ignores: [
|
||||||
|
'dist/*',
|
||||||
|
'*.mjs',
|
||||||
|
'build/*',
|
||||||
|
'*.js',
|
||||||
|
'**/*.cjs',
|
||||||
|
'**/unpack.ts',
|
||||||
|
'i18n*.*'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-unsafe-enum-comparison': 'off',
|
||||||
|
'@typescript-eslint/no-unused-expressions': 'off',
|
||||||
|
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||||
|
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||||
|
'@typescript-eslint/no-unsafe-call': 'off',
|
||||||
|
'@typescript-eslint/no-misused-promises': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
checksVoidReturn: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -1,79 +1,67 @@
|
|||||||
{
|
{
|
||||||
"name": "EMS-ESP",
|
"name": "EMS-ESP",
|
||||||
"version": "3.6.0",
|
"version": "3.7.2",
|
||||||
"description": "build EMS-ESP WebUI",
|
"description": "EMS-ESP WebUI",
|
||||||
"homepage": "https://emsesp.github.io/docs",
|
"homepage": "https://emsesp.org",
|
||||||
"author": "proddy",
|
"author": "proddy, emsesp.org",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"preinstall": "npx only-allow pnpm",
|
||||||
|
"dev": "vite dev",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"build-hosted": "vite build --mode hosted",
|
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"preview-standalone": "npm-run-all -p preview typesafe-i18n mock-api",
|
"build-hosted": "typesafe-i18n && vite build --mode hosted",
|
||||||
"mock-api": "nodemon --watch ../mock-api ../mock-api/server.js",
|
"preview-standalone": "typesafe-i18n --no-watch && vite build && concurrently -c \"auto\" \"pnpm:mock-rest\" \"vite preview\"",
|
||||||
"standalone": "npm-run-all -p dev typesafe-i18n mock-api",
|
"mock-rest": "bun --watch ../mock-api/restServer.ts",
|
||||||
"typesafe-i18n": "typesafe-i18n",
|
"standalone": "concurrently -c \"auto\" \"typesafe-i18n\" \"pnpm:mock-rest\" \"vite\"",
|
||||||
"format": "prettier --write '**/*.{ts,tsx,js,css,json,md}'",
|
"typesafe-i18n": "typesafe-i18n --no-watch",
|
||||||
"lint": "eslint . --cache --fix"
|
"webUI": "node progmem-generator.js",
|
||||||
|
"format": "prettier -l -w '**/*.{ts,tsx,js,css,json,md}'",
|
||||||
|
"lint": "eslint . --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alova/adapter-xhr": "^1.0.1",
|
"@alova/adapter-xhr": "2.2.1",
|
||||||
"@emotion/react": "^11.11.1",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.14.1",
|
||||||
"@mui/icons-material": "^5.14.3",
|
"@mui/icons-material": "^7.3.4",
|
||||||
"@mui/material": "^5.14.4",
|
"@mui/material": "^7.3.4",
|
||||||
"@preact/compat": "^17.1.2",
|
"@table-library/react-table-library": "4.1.15",
|
||||||
"@prefresh/vite": "^2.4.1",
|
"alova": "3.3.4",
|
||||||
"@table-library/react-table-library": "4.1.7",
|
|
||||||
"@types/lodash-es": "^4.17.8",
|
|
||||||
"@types/node": "^20.4.9",
|
|
||||||
"@types/react": "^18.2.20",
|
|
||||||
"@types/react-dom": "^18.2.7",
|
|
||||||
"@types/react-router-dom": "^5.3.3",
|
|
||||||
"alova": "^2.10.0",
|
|
||||||
"async-validator": "^4.2.5",
|
"async-validator": "^4.2.5",
|
||||||
"history": "^5.3.0",
|
"formidable": "^3.5.4",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^4.0.0",
|
||||||
"lodash-es": "^4.17.21",
|
"magic-string": "^0.30.19",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^3.0.1",
|
||||||
"preact": "^10.16.0",
|
"preact": "^10.27.2",
|
||||||
"react": "latest",
|
"react": "^19.2.0",
|
||||||
"react-dom": "latest",
|
"react-dom": "^19.2.0",
|
||||||
"react-dropzone": "^14.2.3",
|
"react-icons": "^5.5.0",
|
||||||
"react-icons": "^4.10.1",
|
"react-router": "^7.9.4",
|
||||||
"react-router-dom": "^6.15.0",
|
"react-toastify": "^11.0.5",
|
||||||
"react-toastify": "^9.1.3",
|
"typesafe-i18n": "^5.26.2",
|
||||||
"sockette": "^2.0.6",
|
"typescript": "^5.9.3"
|
||||||
"typesafe-i18n": "^5.26.0",
|
|
||||||
"typescript": "^5.1.6"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.22.10",
|
"@babel/core": "^7.28.4",
|
||||||
"@preact/preset-vite": "^2.5.0",
|
"@eslint/js": "^9.37.0",
|
||||||
"@types/babel__core": "^7",
|
"@preact/compat": "^18.3.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.3.0",
|
"@preact/preset-vite": "^2.10.2",
|
||||||
"@typescript-eslint/parser": "^6.3.0",
|
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
||||||
"eslint": "^8.47.0",
|
"@types/node": "^24.7.2",
|
||||||
"eslint-config-airbnb": "^19.0.4",
|
"@types/react": "^19.2.2",
|
||||||
"eslint-config-airbnb-typescript": "^17.1.0",
|
"@types/react-dom": "^19.2.2",
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"concurrently": "^9.2.1",
|
||||||
"eslint-import-resolver-typescript": "^3.6.0",
|
"eslint": "^9.37.0",
|
||||||
"eslint-plugin-autofix": "^1.1.0",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-import": "^2.28.0",
|
"prettier": "^3.6.2",
|
||||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
"rollup-plugin-visualizer": "^6.0.4",
|
||||||
"eslint-plugin-prettier": "alpha",
|
"terser": "^5.44.0",
|
||||||
"eslint-plugin-react": "^7.33.1",
|
"typescript-eslint": "^8.46.1",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"vite": "^7.1.10",
|
||||||
"nodemon": "^3.0.1",
|
"vite-plugin-imagemin": "^0.6.1",
|
||||||
"npm-run-all": "^4.1.5",
|
"vite-tsconfig-paths": "^5.1.4"
|
||||||
"prettier": "^3.0.1",
|
|
||||||
"rollup-plugin-visualizer": "^5.9.2",
|
|
||||||
"terser": "^5.19.2",
|
|
||||||
"vite": "^4.4.9",
|
|
||||||
"vite-plugin-svgr": "^3.2.0",
|
|
||||||
"vite-tsconfig-paths": "^4.2.0"
|
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@3.4.1"
|
"packageManager": "pnpm@10.18.3"
|
||||||
}
|
}
|
||||||
|
|||||||
6057
interface/pnpm-lock.yaml
generated
Normal file
6057
interface/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
8
interface/pnpm-workspace.yaml
Normal file
8
interface/pnpm-workspace.yaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
onlyBuiltDependencies:
|
||||||
|
- cwebp-bin
|
||||||
|
- esbuild
|
||||||
|
- gifsicle
|
||||||
|
- jpegtran-bin
|
||||||
|
- mozjpeg
|
||||||
|
- optipng-bin
|
||||||
|
- pngquant-bin
|
||||||
@@ -1,10 +1,33 @@
|
|||||||
const { readdirSync, existsSync, unlinkSync, readFileSync, createWriteStream } = require('fs');
|
import crypto from 'crypto';
|
||||||
const { resolve, relative, sep } = require('path');
|
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 INDENT = ' ';
|
||||||
|
const outputPath = '../src/ESP32React/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) => {
|
||||||
@@ -18,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);
|
||||||
@@ -29,90 +48,68 @@ function cleanAndOpen(path) {
|
|||||||
return createWriteStream(path, { flags: 'w+' });
|
return createWriteStream(path, { flags: 'w+' });
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ProgmemGenerator({ outputPath = './WWWData.h', bytesPerLine = 20 }) {
|
const writeFile = (relativeFilePath, buffer) => {
|
||||||
return {
|
const variable = 'ESP_REACT_DATA_' + fileInfo.length;
|
||||||
name: 'ProgmemGenerator',
|
const mimeType = mime.lookup(relativeFilePath);
|
||||||
writeBundle: () => {
|
var size = 0;
|
||||||
console.log('Generating ' + outputPath);
|
writeStream.write('const uint8_t ' + variable + '[] = {');
|
||||||
const includes = ARDUINO_INCLUDES;
|
// const zipBuffer = zlib.brotliCompressSync(buffer, { quality: 1 });
|
||||||
const indent = INDENT;
|
const zipBuffer = zlib.gzipSync(buffer, { level: 9 });
|
||||||
const fileInfo = [];
|
|
||||||
const writeStream = cleanAndOpen(resolve(outputPath));
|
|
||||||
|
|
||||||
try {
|
// create sha
|
||||||
const writeIncludes = () => {
|
const hashSum = crypto.createHash('sha256');
|
||||||
writeStream.write(includes);
|
hashSum.update(zipBuffer);
|
||||||
};
|
const hash = hashSum.digest('hex');
|
||||||
|
|
||||||
const writeFile = (relativeFilePath, buffer) => {
|
zipBuffer.forEach((b) => {
|
||||||
const variable = 'ESP_REACT_DATA_' + fileInfo.length;
|
if (!(size % bytesPerLine)) {
|
||||||
const mimeType = mime.lookup(relativeFilePath);
|
writeStream.write('\n');
|
||||||
var size = 0;
|
writeStream.write(indent);
|
||||||
writeStream.write('const uint8_t ' + variable + '[] = {');
|
|
||||||
// const zipBuffer = zlib.brotliCompressSync(buffer, { quality: 1 });
|
|
||||||
const zipBuffer = zlib.gzipSync(buffer);
|
|
||||||
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 = () => {
|
|
||||||
// process static files
|
|
||||||
const buildPath = resolve('build');
|
|
||||||
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 = () =>
|
|
||||||
`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 = () => {
|
|
||||||
writeStream.write(generateWWWClass());
|
|
||||||
};
|
|
||||||
|
|
||||||
writeIncludes();
|
|
||||||
writeFiles();
|
|
||||||
writeWWWClass();
|
|
||||||
|
|
||||||
writeStream.on('finish', () => {
|
|
||||||
// callback();
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
writeStream.end();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
writeStream.write('0x' + ('00' + b.toString(16).toUpperCase()).slice(-2) + ',');
|
||||||
|
size++;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (size % bytesPerLine) {
|
||||||
|
writeStream.write('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
writeStream.write('};\n\n');
|
||||||
|
|
||||||
|
fileInfo.push({
|
||||||
|
uri: '/' + relativeFilePath.replace(sep, '/'),
|
||||||
|
mimeType,
|
||||||
|
variable,
|
||||||
|
size,
|
||||||
|
hash
|
||||||
|
});
|
||||||
|
|
||||||
|
// console.log(relativeFilePath + ' (size ' + size + ' bytes)');
|
||||||
|
totalSize += size;
|
||||||
|
};
|
||||||
|
|
||||||
|
// start
|
||||||
|
console.log('Generating ' + outputPath + ' from ' + sourcePath);
|
||||||
|
const includes = ARDUINO_INCLUDES;
|
||||||
|
const indent = INDENT;
|
||||||
|
const fileInfo = [];
|
||||||
|
const writeStream = cleanAndOpen(resolve(outputPath));
|
||||||
|
|
||||||
|
// includes
|
||||||
|
writeStream.write(includes);
|
||||||
|
|
||||||
|
// process static files
|
||||||
|
const buildPath = resolve(sourcePath);
|
||||||
|
for (const filePath of getFilesSync(buildPath)) {
|
||||||
|
const readStream = readFileSync(filePath);
|
||||||
|
const relativeFilePath = relative(buildPath, filePath);
|
||||||
|
writeFile(relativeFilePath, readStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add class
|
||||||
|
writeStream.write(generateWWWClass());
|
||||||
|
|
||||||
|
// end
|
||||||
|
writeStream.end();
|
||||||
|
|
||||||
|
console.log('Total size: ' + totalSize / 1000 + ' KB');
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Uses font-size 400 (normal) only and Latin (plus extra unicode chars) to keep flash memory to a minimum
|
* Uses font-weight 400 (normal) only, no bold, and Latin with a few extra unicode chars.
|
||||||
|
* This is to keep flash memory to a minimum
|
||||||
* View fonts on https://fonts.google.com/
|
* 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
|
* Download woff2 using e.g. https://fonts.googleapis.com/css2?family=Lato or https://fonts.googleapis.com/css2?family=Roboto
|
||||||
*/
|
*/
|
||||||
@@ -12,7 +13,9 @@
|
|||||||
local('Roboto'),
|
local('Roboto'),
|
||||||
local('Roboto-Regular'),
|
local('Roboto-Regular'),
|
||||||
url(../fonts/re.woff2) format('woff2');
|
url(../fonts/re.woff2) format('woff2');
|
||||||
unicode-range: U+0000-00FF, U+0104-0107, U+0118-0119, U+011E-011F, U+0130-0131, U+0141-0144, U+0152-0153, U+015A-015B,
|
unicode-range:
|
||||||
U+015E-015F, U+0179-017C, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193,
|
U+0000-00FF, U+0104-0107, U+0118-0119, U+011E-011F, U+0130-0131, U+0141-0144,
|
||||||
U+2212, U+2215, U+FEFF, U+FFFD;
|
U+0152-0153, U+015A-015B, U+015E-015F, U+0179-017C, U+02BB-02BC, U+02C6, U+02DA,
|
||||||
|
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
|
||||||
|
U+FEFF, U+FFFD;
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
@@ -1,48 +1,62 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { ToastContainer, Slide } from 'react-toastify';
|
import { ToastContainer, Zoom } from 'react-toastify';
|
||||||
|
|
||||||
import 'react-toastify/dist/ReactToastify.min.css';
|
|
||||||
|
|
||||||
import { localStorageDetector } from 'typesafe-i18n/detectors';
|
|
||||||
import { FeaturesLoader } from './contexts/features';
|
|
||||||
import type { FC } from 'react';
|
|
||||||
import AppRouting from 'AppRouting';
|
import AppRouting from 'AppRouting';
|
||||||
import CustomTheme from 'CustomTheme';
|
import CustomTheme from 'CustomTheme';
|
||||||
|
|
||||||
import TypesafeI18n from 'i18n/i18n-react';
|
import TypesafeI18n from 'i18n/i18n-react';
|
||||||
import { detectLocale } from 'i18n/i18n-util';
|
import type { Locales } from 'i18n/i18n-types';
|
||||||
import { loadLocaleAsync } from 'i18n/i18n-util.async';
|
import { loadLocaleAsync } from 'i18n/i18n-util.async';
|
||||||
|
import { detectLocale, navigatorDetector } from 'typesafe-i18n/detectors';
|
||||||
|
|
||||||
const detectedLocale = detectLocale(localStorageDetector);
|
const availableLocales = [
|
||||||
|
'de',
|
||||||
|
'en',
|
||||||
|
'it',
|
||||||
|
'fr',
|
||||||
|
'nl',
|
||||||
|
'no',
|
||||||
|
'pl',
|
||||||
|
'sk',
|
||||||
|
'sv',
|
||||||
|
'tr',
|
||||||
|
'cz'
|
||||||
|
];
|
||||||
|
|
||||||
const App: FC = () => {
|
const App = () => {
|
||||||
const [wasLoaded, setWasLoaded] = useState(false);
|
const [wasLoaded, setWasLoaded] = useState(false);
|
||||||
|
const [locale, setLocale] = useState<Locales>('en');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void loadLocaleAsync(detectedLocale).then(() => setWasLoaded(true));
|
// determine locale, take from session if set other default to browser language
|
||||||
|
const browserLocale = detectLocale('en', availableLocales, navigatorDetector);
|
||||||
|
const newLocale = (localStorage.getItem('lang') || browserLocale) as Locales;
|
||||||
|
localStorage.setItem('lang', newLocale);
|
||||||
|
setLocale(newLocale);
|
||||||
|
void loadLocaleAsync(newLocale).then(() => setWasLoaded(true));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!wasLoaded) return null;
|
if (!wasLoaded) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TypesafeI18n locale={detectedLocale}>
|
<TypesafeI18n locale={locale}>
|
||||||
<CustomTheme>
|
<CustomTheme>
|
||||||
<FeaturesLoader>
|
<AppRouting />
|
||||||
<AppRouting />
|
|
||||||
</FeaturesLoader>
|
|
||||||
<ToastContainer
|
<ToastContainer
|
||||||
position="bottom-left"
|
position="bottom-left"
|
||||||
autoClose={3000}
|
autoClose={3000}
|
||||||
hideProgressBar={false}
|
hideProgressBar={false}
|
||||||
newestOnTop={false}
|
newestOnTop={false}
|
||||||
closeOnClick={true}
|
closeOnClick
|
||||||
rtl={false}
|
rtl={false}
|
||||||
pauseOnFocusLoss={false}
|
pauseOnFocusLoss
|
||||||
draggable={false}
|
draggable={false}
|
||||||
pauseOnHover={false}
|
pauseOnHover={false}
|
||||||
transition={Slide}
|
transition={Zoom}
|
||||||
closeButton={false}
|
closeButton={false}
|
||||||
theme="light"
|
theme="dark"
|
||||||
|
toastStyle={{
|
||||||
|
border: '1px solid #177ac9'
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</CustomTheme>
|
</CustomTheme>
|
||||||
</TypesafeI18n>
|
</TypesafeI18n>
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
import { useContext, useEffect } from 'react';
|
import { useContext, useEffect } from 'react';
|
||||||
|
import { Navigate, Route, Routes } from 'react-router';
|
||||||
import { Route, Routes, Navigate, useLocation } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import type { FC } from 'react';
|
|
||||||
|
|
||||||
import AuthenticatedRouting from 'AuthenticatedRouting';
|
import AuthenticatedRouting from 'AuthenticatedRouting';
|
||||||
import SignIn from 'SignIn';
|
import SignIn from 'SignIn';
|
||||||
import { RequireAuthenticated, RequireUnauthenticated } from 'components';
|
import { RequireAuthenticated, RequireUnauthenticated } from 'components';
|
||||||
|
|
||||||
import { Authentication, AuthenticationContext } from 'contexts/authentication';
|
import { Authentication, AuthenticationContext } from 'contexts/authentication';
|
||||||
import { useI18nContext } from 'i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
|
||||||
@@ -17,7 +13,7 @@ interface SecurityRedirectProps {
|
|||||||
signOut?: boolean;
|
signOut?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RootRedirect: FC<SecurityRedirectProps> = ({ message, signOut }) => {
|
const RootRedirect = ({ message, signOut }: SecurityRedirectProps) => {
|
||||||
const authenticationContext = useContext(AuthenticationContext);
|
const authenticationContext = useContext(AuthenticationContext);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
signOut && authenticationContext.signOut(false);
|
signOut && authenticationContext.signOut(false);
|
||||||
@@ -26,29 +22,20 @@ const RootRedirect: FC<SecurityRedirectProps> = ({ message, signOut }) => {
|
|||||||
return <Navigate to="/" />;
|
return <Navigate to="/" />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RemoveTrailingSlashes = () => {
|
const AppRouting = () => {
|
||||||
const location = useLocation();
|
|
||||||
return (
|
|
||||||
location.pathname.match('/.*/$') && (
|
|
||||||
<Navigate
|
|
||||||
to={{
|
|
||||||
pathname: location.pathname.replace(/\/+$/, ''),
|
|
||||||
search: location.search
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const AppRouting: FC = () => {
|
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Authentication>
|
<Authentication>
|
||||||
<RemoveTrailingSlashes />
|
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/unauthorized" element={<RootRedirect message={LL.PLEASE_SIGNIN()} signOut />} />
|
<Route
|
||||||
<Route path="/fileUpdated" element={<RootRedirect message={LL.UPLOAD_SUCCESSFUL()} />} />
|
path="/unauthorized"
|
||||||
|
element={<RootRedirect message={LL.PLEASE_SIGNIN()} signOut />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/fileUpdated"
|
||||||
|
element={<RootRedirect message={LL.UPLOAD_SUCCESSFUL()} />}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/"
|
path="/"
|
||||||
element={
|
element={
|
||||||
|
|||||||
@@ -1,64 +1,76 @@
|
|||||||
import { Navigate, Routes, Route } from 'react-router-dom';
|
import { useContext } from 'react';
|
||||||
import Dashboard from './project/Dashboard';
|
import { Navigate, Route, Routes } from 'react-router';
|
||||||
import Help from './project/Help';
|
|
||||||
import Settings from './project/Settings';
|
|
||||||
import type { FC } from 'react';
|
|
||||||
|
|
||||||
import { Layout, RequireAdmin } from 'components';
|
import CustomEntities from 'app/main/CustomEntities';
|
||||||
import AccessPoint from 'framework/ap/AccessPoint';
|
import Customizations from 'app/main/Customizations';
|
||||||
import Mqtt from 'framework/mqtt/Mqtt';
|
import Dashboard from 'app/main/Dashboard';
|
||||||
import NetworkConnection from 'framework/network/NetworkConnection';
|
import Devices from 'app/main/Devices';
|
||||||
import NetworkTime from 'framework/ntp/NetworkTime';
|
import Help from 'app/main/Help';
|
||||||
import Security from 'framework/security/Security';
|
import Modules from 'app/main/Modules';
|
||||||
import System from 'framework/system/System';
|
import Scheduler from 'app/main/Scheduler';
|
||||||
|
import Sensors from 'app/main/Sensors';
|
||||||
|
import APSettings from 'app/settings/APSettings';
|
||||||
|
import ApplicationSettings from 'app/settings/ApplicationSettings';
|
||||||
|
import DownloadUpload from 'app/settings/DownloadUpload';
|
||||||
|
import MqttSettings from 'app/settings/MqttSettings';
|
||||||
|
import NTPSettings from 'app/settings/NTPSettings';
|
||||||
|
import Settings from 'app/settings/Settings';
|
||||||
|
import Network from 'app/settings/network/Network';
|
||||||
|
import Security from 'app/settings/security/Security';
|
||||||
|
import APStatus from 'app/status/APStatus';
|
||||||
|
import Activity from 'app/status/Activity';
|
||||||
|
import HardwareStatus from 'app/status/HardwareStatus';
|
||||||
|
import MqttStatus from 'app/status/MqttStatus';
|
||||||
|
import NTPStatus from 'app/status/NTPStatus';
|
||||||
|
import NetworkStatus from 'app/status/NetworkStatus';
|
||||||
|
import Status from 'app/status/Status';
|
||||||
|
import SystemLog from 'app/status/SystemLog';
|
||||||
|
import Version from 'app/status/Version';
|
||||||
|
import { Layout } from 'components';
|
||||||
|
import { AuthenticatedContext } from 'contexts/authentication';
|
||||||
|
|
||||||
const AuthenticatedRouting: FC = () => (
|
const AuthenticatedRouting = () => {
|
||||||
// const location = useLocation();
|
const { me } = useContext(AuthenticatedContext);
|
||||||
// const navigate = useNavigate();
|
return (
|
||||||
// const handleApiResponseError = useCallback(
|
<Layout>
|
||||||
// (error: AxiosError) => {
|
<Routes>
|
||||||
// if (error.response && error.response.status === 401) {
|
<Route path="/dashboard/*" element={<Dashboard />} />
|
||||||
// AuthenticationApi.storeLoginRedirect(location);
|
<Route path="/devices/*" element={<Devices />} />
|
||||||
// navigate('/unauthorized');
|
<Route path="/sensors/*" element={<Sensors />} />
|
||||||
// }
|
<Route path="/status/*" element={<Status />} />
|
||||||
// return Promise.reject(error);
|
<Route path="/help/*" element={<Help />} />
|
||||||
// },
|
<Route path="/*" element={<Navigate to="/" />} />
|
||||||
// [location, navigate]
|
|
||||||
// );
|
|
||||||
// useEffect(() => {
|
|
||||||
// const axiosHandlerId = AXIOS.interceptors.response.use((response) => response, handleApiResponseError);
|
|
||||||
// return () => AXIOS.interceptors.response.eject(axiosHandlerId);
|
|
||||||
// }, [handleApiResponseError]);
|
|
||||||
|
|
||||||
<Layout>
|
<Route path="/status/hardwarestatus/*" element={<HardwareStatus />} />
|
||||||
<Routes>
|
<Route path="/status/activity" element={<Activity />} />
|
||||||
<Route path="/dashboard/*" element={<Dashboard />} />
|
<Route path="/status/log" element={<SystemLog />} />
|
||||||
<Route
|
<Route path="/status/mqtt" element={<MqttStatus />} />
|
||||||
path="/settings/*"
|
<Route path="/status/ntp" element={<NTPStatus />} />
|
||||||
element={
|
<Route path="/status/ap" element={<APStatus />} />
|
||||||
<RequireAdmin>
|
<Route path="/status/network" element={<NetworkStatus />} />
|
||||||
<Settings />
|
<Route path="/status/version" element={<Version />} />
|
||||||
</RequireAdmin>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route path="/help/*" element={<Help />} />
|
|
||||||
|
|
||||||
<Route path="/network/*" element={<NetworkConnection />} />
|
{me.admin && (
|
||||||
<Route path="/ap/*" element={<AccessPoint />} />
|
<>
|
||||||
<Route path="/ntp/*" element={<NetworkTime />} />
|
<Route path="/settings" element={<Settings />} />
|
||||||
<Route path="/mqtt/*" element={<Mqtt />} />
|
<Route path="/settings/application" element={<ApplicationSettings />} />
|
||||||
<Route
|
<Route path="/settings/mqtt" element={<MqttSettings />} />
|
||||||
path="/security/*"
|
<Route path="/settings/ntp" element={<NTPSettings />} />
|
||||||
element={
|
<Route path="/settings/ap" element={<APSettings />} />
|
||||||
<RequireAdmin>
|
<Route path="/settings/modules" element={<Modules />} />
|
||||||
<Security />
|
<Route path="/settings/downloadUpload" element={<DownloadUpload />} />
|
||||||
</RequireAdmin>
|
|
||||||
}
|
<Route path="/settings/network/*" element={<Network />} />
|
||||||
/>
|
<Route path="/settings/security/*" element={<Security />} />
|
||||||
<Route path="/system/*" element={<System />} />
|
|
||||||
<Route path="/*" element={<Navigate to="/" />} />
|
<Route path="/customizations" element={<Customizations />} />
|
||||||
</Routes>
|
<Route path="/scheduler" element={<Scheduler />} />
|
||||||
</Layout>
|
<Route path="/customentities" element={<CustomEntities />} />
|
||||||
);
|
</>
|
||||||
|
)}
|
||||||
|
</Routes>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default AuthenticatedRouting;
|
export default AuthenticatedRouting;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { CssBaseline } from '@mui/material';
|
|
||||||
import { createTheme, responsiveFontSizes, ThemeProvider } from '@mui/material/styles';
|
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
|
|
||||||
|
import { CssBaseline, ThemeProvider, responsiveFontSizes } from '@mui/material';
|
||||||
|
import { createTheme } from '@mui/material/styles';
|
||||||
|
|
||||||
import type { RequiredChildrenProps } from 'utils';
|
import type { RequiredChildrenProps } from 'utils';
|
||||||
|
|
||||||
export const dialogStyle = {
|
export const dialogStyle = {
|
||||||
@@ -10,8 +11,7 @@ export const dialogStyle = {
|
|||||||
borderColor: '#565656',
|
borderColor: '#565656',
|
||||||
borderStyle: 'solid',
|
borderStyle: 'solid',
|
||||||
borderWidth: '1px'
|
borderWidth: '1px'
|
||||||
},
|
}
|
||||||
backdropFilter: 'blur(1px)'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const theme = responsiveFontSizes(
|
const theme = responsiveFontSizes(
|
||||||
@@ -26,6 +26,9 @@ const theme = responsiveFontSizes(
|
|||||||
},
|
},
|
||||||
info: {
|
info: {
|
||||||
main: '#607d8b' // blueGrey[500]
|
main: '#607d8b' // blueGrey[500]
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
disabled: '#eee' // white
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,40 +1,28 @@
|
|||||||
import ForwardIcon from '@mui/icons-material/Forward';
|
|
||||||
import { Box, Paper, Typography, MenuItem, TextField, Button } from '@mui/material';
|
|
||||||
import { useRequest } from 'alova';
|
|
||||||
import { useContext, useState } from 'react';
|
import { useContext, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { FeaturesContext } from './contexts/features';
|
|
||||||
|
import ForwardIcon from '@mui/icons-material/Forward';
|
||||||
|
import { Box, Button, Paper, Typography } from '@mui/material';
|
||||||
|
|
||||||
|
import * as AuthenticationApi from 'components/routing/authentication';
|
||||||
|
import { useRequest } from 'alova/client';
|
||||||
import type { ValidateFieldsError } from 'async-validator';
|
import type { ValidateFieldsError } from 'async-validator';
|
||||||
|
import {
|
||||||
import type { Locales } from 'i18n/i18n-types';
|
LanguageSelector,
|
||||||
import type { ChangeEventHandler, FC } from 'react';
|
ValidatedPasswordField,
|
||||||
import type { SignInRequest } from 'types';
|
ValidatedTextField
|
||||||
import * as AuthenticationApi from 'api/authentication';
|
} from 'components';
|
||||||
import { PROJECT_NAME } from 'api/env';
|
|
||||||
|
|
||||||
import { ValidatedPasswordField, ValidatedTextField } from 'components';
|
|
||||||
import { AuthenticationContext } from 'contexts/authentication';
|
import { AuthenticationContext } from 'contexts/authentication';
|
||||||
|
import { PROJECT_NAME } from 'env';
|
||||||
import { ReactComponent as DEflag } from 'i18n/DE.svg';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
import { ReactComponent as FRflag } from 'i18n/FR.svg';
|
import type { SignInRequest } from 'types';
|
||||||
import { ReactComponent as GBflag } from 'i18n/GB.svg';
|
|
||||||
import { ReactComponent as ITflag } from 'i18n/IT.svg';
|
|
||||||
import { ReactComponent as NLflag } from 'i18n/NL.svg';
|
|
||||||
import { ReactComponent as NOflag } from 'i18n/NO.svg';
|
|
||||||
import { ReactComponent as PLflag } from 'i18n/PL.svg';
|
|
||||||
import { ReactComponent as SVflag } from 'i18n/SV.svg';
|
|
||||||
import { ReactComponent as TRflag } from 'i18n/TR.svg';
|
|
||||||
import { I18nContext } from 'i18n/i18n-react';
|
|
||||||
import { loadLocaleAsync } from 'i18n/i18n-util.async';
|
|
||||||
import { onEnterCallback, updateValue } from 'utils';
|
import { onEnterCallback, updateValue } from 'utils';
|
||||||
import { SIGN_IN_REQUEST_VALIDATOR, validate } from 'validators';
|
import { SIGN_IN_REQUEST_VALIDATOR, validate } from 'validators';
|
||||||
|
|
||||||
const SignIn: FC = () => {
|
const SignIn = () => {
|
||||||
const authenticationContext = useContext(AuthenticationContext);
|
const authenticationContext = useContext(AuthenticationContext);
|
||||||
|
|
||||||
const { LL, setLocale, locale } = useContext(I18nContext);
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
const { features } = useContext(FeaturesContext);
|
|
||||||
|
|
||||||
const [signInRequest, setSignInRequest] = useState<SignInRequest>({
|
const [signInRequest, setSignInRequest] = useState<SignInRequest>({
|
||||||
username: '',
|
username: '',
|
||||||
@@ -43,11 +31,12 @@ 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, onSuccess } = useRequest((request: SignInRequest) => AuthenticationApi.signIn(request), {
|
const { send: callSignIn } = useRequest(
|
||||||
immediate: false
|
(request: SignInRequest) => AuthenticationApi.signIn(request),
|
||||||
});
|
{
|
||||||
|
immediate: false
|
||||||
onSuccess((response) => {
|
}
|
||||||
|
).onSuccess((response) => {
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
authenticationContext.signIn(response.data.access_token);
|
authenticationContext.signIn(response.data.access_token);
|
||||||
}
|
}
|
||||||
@@ -56,7 +45,7 @@ const SignIn: FC = () => {
|
|||||||
const updateLoginRequestValue = updateValue(setSignInRequest);
|
const updateLoginRequestValue = updateValue(setSignInRequest);
|
||||||
|
|
||||||
const signIn = async () => {
|
const signIn = async () => {
|
||||||
await callSignIn(signInRequest).catch((event) => {
|
await callSignIn(signInRequest).catch((event: Error) => {
|
||||||
if (event.message === 'Unauthorized') {
|
if (event.message === 'Unauthorized') {
|
||||||
toast.warning(LL.INVALID_LOGIN());
|
toast.warning(LL.INVALID_LOGIN());
|
||||||
} else {
|
} else {
|
||||||
@@ -74,21 +63,14 @@ const SignIn: FC = () => {
|
|||||||
try {
|
try {
|
||||||
await validate(SIGN_IN_REQUEST_VALIDATOR, signInRequest);
|
await validate(SIGN_IN_REQUEST_VALIDATOR, signInRequest);
|
||||||
await signIn();
|
await signIn();
|
||||||
} catch (errors: any) {
|
} catch (error) {
|
||||||
setFieldErrors(errors);
|
setFieldErrors(error as ValidateFieldsError);
|
||||||
setProcessing(false);
|
setProcessing(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitOnEnter = onEnterCallback(signIn);
|
const submitOnEnter = onEnterCallback(signIn);
|
||||||
|
|
||||||
const onLocaleSelected: ChangeEventHandler<HTMLInputElement> = async ({ target }) => {
|
|
||||||
const loc = target.value as Locales;
|
|
||||||
localStorage.setItem('lang', loc);
|
|
||||||
await loadLocaleAsync(loc);
|
|
||||||
setLocale(loc);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
display="flex"
|
display="flex"
|
||||||
@@ -111,46 +93,8 @@ const SignIn: FC = () => {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Typography variant="h4">{PROJECT_NAME}</Typography>
|
<Typography variant="h4">{PROJECT_NAME}</Typography>
|
||||||
<Typography variant="subtitle2">{features.version}</Typography>
|
|
||||||
|
|
||||||
<TextField name="locale" variant="outlined" value={locale} onChange={onLocaleSelected} size="small" select>
|
<LanguageSelector />
|
||||||
<MenuItem key="de" value="de">
|
|
||||||
<DEflag style={{ width: 16, verticalAlign: 'middle' }} />
|
|
||||||
DE
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem key="en" value="en">
|
|
||||||
<GBflag style={{ width: 16, verticalAlign: 'middle' }} />
|
|
||||||
EN
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem key="fr" value="fr">
|
|
||||||
<FRflag style={{ width: 16, verticalAlign: 'middle' }} />
|
|
||||||
FR
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem key="it" value="it">
|
|
||||||
<ITflag style={{ width: 16, verticalAlign: 'middle' }} />
|
|
||||||
IT
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem key="nl" value="nl">
|
|
||||||
<NLflag style={{ width: 16, verticalAlign: 'middle' }} />
|
|
||||||
NL
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem key="no" value="no">
|
|
||||||
<NOflag style={{ width: 16, verticalAlign: 'middle' }} />
|
|
||||||
NO
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem key="pl" value="pl">
|
|
||||||
<PLflag style={{ width: 16, verticalAlign: 'middle' }} />
|
|
||||||
PL
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem key="sv" value="sv">
|
|
||||||
<SVflag style={{ width: 16, verticalAlign: 'middle' }} />
|
|
||||||
SV
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem key="tr" value="tr">
|
|
||||||
<TRflag style={{ width: 16, verticalAlign: 'middle' }} />
|
|
||||||
TR
|
|
||||||
</MenuItem>
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
<Box display="flex" flexDirection="column" alignItems="center">
|
<Box display="flex" flexDirection="column" alignItems="center">
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
@@ -165,6 +109,12 @@ const SignIn: FC = () => {
|
|||||||
onChange={updateLoginRequestValue}
|
onChange={updateLoginRequestValue}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
slotProps={{
|
||||||
|
input: {
|
||||||
|
autoCapitalize: 'none',
|
||||||
|
autoCorrect: 'off'
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<ValidatedPasswordField
|
<ValidatedPasswordField
|
||||||
fieldErrors={fieldErrors}
|
fieldErrors={fieldErrors}
|
||||||
@@ -181,7 +131,13 @@ const SignIn: FC = () => {
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Button variant="contained" color="primary" sx={{ mt: 2 }} onClick={validateAndSignIn} disabled={processing}>
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
sx={{ mt: 2 }}
|
||||||
|
onClick={validateAndSignIn}
|
||||||
|
disabled={processing}
|
||||||
|
>
|
||||||
<ForwardIcon sx={{ mr: 1 }} />
|
<ForwardIcon sx={{ mr: 1 }} />
|
||||||
{LL.SIGN_IN()}
|
{LL.SIGN_IN()}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import type { APSettingsType, APStatusType } from 'types';
|
||||||
|
|
||||||
import { alovaInstance } from './endpoints';
|
import { alovaInstance } from './endpoints';
|
||||||
|
|
||||||
import type { APSettings, APStatus } from 'types';
|
export const readAPStatus = () => alovaInstance.Get<APStatusType>('/rest/apStatus');
|
||||||
|
export const readAPSettings = () =>
|
||||||
export const readAPStatus = () => alovaInstance.Get<APStatus>('/rest/apStatus');
|
alovaInstance.Get<APSettingsType>('/rest/apSettings');
|
||||||
export const readAPSettings = () => alovaInstance.Get<APSettings>('/rest/apSettings');
|
export const updateAPSettings = (data: APSettingsType) =>
|
||||||
export const updateAPSettings = (data: APSettings) => alovaInstance.Post<APSettings>('/rest/apSettings', data);
|
alovaInstance.Post<APSettingsType>('/rest/apSettings', data);
|
||||||
|
|||||||
152
interface/src/api/app.ts
Normal file
152
interface/src/api/app.ts
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import { alovaInstance } from 'api/endpoints';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
APIcall,
|
||||||
|
Action,
|
||||||
|
Activity,
|
||||||
|
CoreData,
|
||||||
|
DashboardData,
|
||||||
|
DeviceData,
|
||||||
|
DeviceEntity,
|
||||||
|
Entities,
|
||||||
|
EntityItem,
|
||||||
|
ModuleItem,
|
||||||
|
Modules,
|
||||||
|
Schedule,
|
||||||
|
ScheduleItem,
|
||||||
|
SensorData,
|
||||||
|
Settings,
|
||||||
|
WriteAnalogSensor,
|
||||||
|
WriteTemperatureSensor
|
||||||
|
} from '../app/main/types';
|
||||||
|
|
||||||
|
// Dashboard
|
||||||
|
export const readDashboard = () =>
|
||||||
|
alovaInstance.Get<DashboardData>('/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,
|
||||||
|
o_hide: ei.hide
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
export const writeCustomEntities = (data: Entities) =>
|
||||||
|
alovaInstance.Post('/rest/customEntities', data);
|
||||||
@@ -1,33 +1,36 @@
|
|||||||
import { xhrRequestAdapter } from '@alova/adapter-xhr';
|
import { type AlovaXHRResponse, xhrRequestAdapter } from '@alova/adapter-xhr';
|
||||||
import { createAlova } from 'alova';
|
import { createAlova } from 'alova';
|
||||||
import ReactHook from 'alova/react';
|
import ReactHook from 'alova/react';
|
||||||
import { unpack } from '../api/unpack';
|
|
||||||
|
import { unpack } from './unpack';
|
||||||
|
|
||||||
export const ACCESS_TOKEN = 'access_token';
|
export const ACCESS_TOKEN = 'access_token';
|
||||||
|
|
||||||
const host = window.location.host;
|
|
||||||
export const WEB_SOCKET_ROOT = 'ws://' + host + '/ws/';
|
|
||||||
export const EVENT_SOURCE_ROOT = 'http://' + host + '/es/';
|
|
||||||
|
|
||||||
export const alovaInstance = createAlova({
|
export const alovaInstance = createAlova({
|
||||||
statesHook: ReactHook,
|
statesHook: ReactHook,
|
||||||
timeout: 3000, // 3 seconds but throwing a timeout error
|
// timeout: 3000, // 3 seconds before throwing a timeout error, default is 0 = none
|
||||||
localCache: null,
|
cacheFor: null, // disable cache
|
||||||
// localCache: {
|
// cacheFor: {
|
||||||
// GET: {
|
// GET: {
|
||||||
// mode: 'placeholder', // see https://alova.js.org/learning/response-cache/#cache-replaceholder-mode
|
// mode: 'memory',
|
||||||
// expire: 2000
|
// expire: 60 * 10 * 1000 // 60 seconds in cache
|
||||||
// }
|
// }
|
||||||
// },
|
// },
|
||||||
requestAdapter: xhrRequestAdapter(),
|
requestAdapter: xhrRequestAdapter(),
|
||||||
beforeRequest(method) {
|
beforeRequest(method) {
|
||||||
if (localStorage.getItem(ACCESS_TOKEN)) {
|
if (localStorage.getItem(ACCESS_TOKEN)) {
|
||||||
method.config.headers.Authorization = 'Bearer ' + localStorage.getItem(ACCESS_TOKEN);
|
method.config.headers.Authorization =
|
||||||
|
'Bearer ' + localStorage.getItem(ACCESS_TOKEN);
|
||||||
}
|
}
|
||||||
|
// for simulating very slow networks
|
||||||
|
// return new Promise((resolve) => {
|
||||||
|
// const random = 3000 + Math.random() * 2000;
|
||||||
|
// setTimeout(resolve, Math.floor(random));
|
||||||
|
// });
|
||||||
},
|
},
|
||||||
|
|
||||||
responded: {
|
responded: {
|
||||||
onSuccess: async (response) => {
|
onSuccess: async (response: AlovaXHRResponse) => {
|
||||||
// if (response.status === 202) {
|
// if (response.status === 202) {
|
||||||
// throw new Error('Wait'); // wifi scan in progress
|
// throw new Error('Wait'); // wifi scan in progress
|
||||||
// } else
|
// } else
|
||||||
@@ -38,9 +41,9 @@ export const alovaInstance = createAlova({
|
|||||||
} else if (response.status >= 400) {
|
} else if (response.status >= 400) {
|
||||||
throw new Error(response.statusText);
|
throw new Error(response.statusText);
|
||||||
}
|
}
|
||||||
const data = await response.data;
|
const data: ArrayBuffer = (await response.data) as ArrayBuffer;
|
||||||
if (response.data instanceof ArrayBuffer) {
|
if (response.data instanceof ArrayBuffer) {
|
||||||
return unpack(data);
|
return unpack(data) as ArrayBuffer;
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@@ -54,7 +57,10 @@ export const alovaInstance = createAlova({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const alovaInstanceGH = createAlova({
|
export const alovaInstanceGH = createAlova({
|
||||||
baseURL: 'https://api.github.com/repos/emsesp/EMS-ESP32/releases',
|
baseURL:
|
||||||
|
process.env.NODE_ENV === 'development'
|
||||||
|
? '/gh'
|
||||||
|
: 'https://api.github.com/repos/emsesp/EMS-ESP32/releases',
|
||||||
statesHook: ReactHook,
|
statesHook: ReactHook,
|
||||||
requestAdapter: xhrRequestAdapter()
|
requestAdapter: xhrRequestAdapter()
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
import { alovaInstance } from './endpoints';
|
|
||||||
|
|
||||||
import type { Features } from 'types';
|
|
||||||
|
|
||||||
export const readFeatures = () => alovaInstance.Get<Features>('/rest/features');
|
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
import { alovaInstance } from './endpoints';
|
import type { MqttSettingsType, MqttStatusType } from 'types';
|
||||||
import type { MqttSettings, MqttStatus } from 'types';
|
|
||||||
|
|
||||||
export const readMqttStatus = () => alovaInstance.Get<MqttStatus>('/rest/mqttStatus');
|
import { alovaInstance } from './endpoints';
|
||||||
export const readMqttSettings = () => alovaInstance.Get<MqttSettings>('/rest/mqttSettings');
|
|
||||||
export const updateMqttSettings = (data: MqttSettings) => alovaInstance.Post<MqttSettings>('/rest/mqttSettings', data);
|
export const readMqttStatus = () =>
|
||||||
|
alovaInstance.Get<MqttStatusType>('/rest/mqttStatus');
|
||||||
|
export const readMqttSettings = () =>
|
||||||
|
alovaInstance.Get<MqttSettingsType>('/rest/mqttSettings');
|
||||||
|
export const updateMqttSettings = (data: MqttSettingsType) =>
|
||||||
|
alovaInstance.Post<MqttSettingsType>('/rest/mqttSettings', data);
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
|
import type { NetworkSettingsType, NetworkStatusType, WiFiNetworkList } from 'types';
|
||||||
|
|
||||||
import { alovaInstance } from './endpoints';
|
import { alovaInstance } from './endpoints';
|
||||||
|
|
||||||
import type { WiFiNetworkList, NetworkSettings, NetworkStatus } from 'types';
|
export const readNetworkStatus = () =>
|
||||||
|
alovaInstance.Get<NetworkStatusType>('/rest/networkStatus');
|
||||||
export const readNetworkStatus = () => alovaInstance.Get<NetworkStatus>('/rest/networkStatus');
|
|
||||||
export const scanNetworks = () => alovaInstance.Get('/rest/scanNetworks');
|
export const scanNetworks = () => alovaInstance.Get('/rest/scanNetworks');
|
||||||
export const listNetworks = () =>
|
export const listNetworks = () =>
|
||||||
alovaInstance.Get<WiFiNetworkList>('/rest/listNetworks', {
|
alovaInstance.Get<WiFiNetworkList>('/rest/listNetworks', {
|
||||||
name: 'listNetworks',
|
timeout: 20000 // 20 seconds
|
||||||
timeout: 20000 // timeout 20 seconds
|
|
||||||
});
|
});
|
||||||
export const readNetworkSettings = () =>
|
export const readNetworkSettings = () =>
|
||||||
alovaInstance.Get<NetworkSettings>('/rest/networkSettings', { name: 'networkSettings' });
|
alovaInstance.Get<NetworkSettingsType>('/rest/networkSettings');
|
||||||
export const updateNetworkSettings = (wifiSettings: NetworkSettings) =>
|
export const updateNetworkSettings = (wifiSettings: NetworkSettingsType) =>
|
||||||
alovaInstance.Post<NetworkSettings>('/rest/networkSettings', wifiSettings);
|
alovaInstance.Post<NetworkSettingsType>('/rest/networkSettings', wifiSettings);
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
|
import type { NTPSettingsType, NTPStatusType, Time } from 'types';
|
||||||
|
|
||||||
import { alovaInstance } from './endpoints';
|
import { alovaInstance } from './endpoints';
|
||||||
import type { NTPSettings, NTPStatus, Time } from 'types';
|
|
||||||
|
|
||||||
export const readNTPStatus = () => alovaInstance.Get<NTPStatus>('/rest/ntpStatus');
|
export const readNTPStatus = () =>
|
||||||
|
alovaInstance.Get<NTPStatusType>('/rest/ntpStatus');
|
||||||
|
|
||||||
export const readNTPSettings = () =>
|
export const readNTPSettings = () =>
|
||||||
alovaInstance.Get<NTPSettings>('/rest/ntpSettings', {
|
alovaInstance.Get<NTPSettingsType>('/rest/ntpSettings', {});
|
||||||
name: 'ntpSettings'
|
export const updateNTPSettings = (data: NTPSettingsType) =>
|
||||||
});
|
alovaInstance.Post<NTPSettingsType>('/rest/ntpSettings', data);
|
||||||
export const updateNTPSettings = (data: NTPSettings) => alovaInstance.Post<NTPSettings>('/rest/ntpSettings', data);
|
|
||||||
|
|
||||||
export const updateTime = (data: Time) => alovaInstance.Post<Time>('/rest/time', data);
|
export const updateTime = (data: Time) =>
|
||||||
|
alovaInstance.Post<Time>('/rest/time', data);
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
import type { SecuritySettingsType, Token } from 'types';
|
||||||
|
|
||||||
import { alovaInstance } from './endpoints';
|
import { alovaInstance } from './endpoints';
|
||||||
|
|
||||||
import type { SecuritySettings, Token } from 'types';
|
export const readSecuritySettings = () =>
|
||||||
|
alovaInstance.Get<SecuritySettingsType>('/rest/securitySettings');
|
||||||
|
|
||||||
export const readSecuritySettings = () => alovaInstance.Get<SecuritySettings>('/rest/securitySettings');
|
export const updateSecuritySettings = (securitySettings: SecuritySettingsType) =>
|
||||||
|
|
||||||
export const updateSecuritySettings = (securitySettings: SecuritySettings) =>
|
|
||||||
alovaInstance.Post('/rest/securitySettings', securitySettings);
|
alovaInstance.Post('/rest/securitySettings', securitySettings);
|
||||||
|
|
||||||
export const generateToken = (username?: string) =>
|
export const generateToken = (username?: string) =>
|
||||||
|
|||||||
@@ -1,34 +1,38 @@
|
|||||||
|
import type { LogSettings, SystemStatus } from 'types';
|
||||||
|
|
||||||
import { alovaInstance, alovaInstanceGH } from './endpoints';
|
import { alovaInstance, alovaInstanceGH } from './endpoints';
|
||||||
import type { OTASettings, SystemStatus, LogSettings } from 'types';
|
|
||||||
|
|
||||||
// SystemStatus - also used to ping in Restart monitor for pinging
|
// systemStatus - also used to ping in System Monitor for pinging
|
||||||
export const readSystemStatus = () => alovaInstance.Get<SystemStatus>('/rest/systemStatus');
|
export const readSystemStatus = () =>
|
||||||
|
alovaInstance.Get<SystemStatus>('/rest/systemStatus');
|
||||||
// commands
|
|
||||||
export const restart = () => alovaInstance.Post('/rest/restart');
|
|
||||||
export const partition = () => alovaInstance.Post('/rest/partition');
|
|
||||||
export const factoryReset = () => alovaInstance.Post('/rest/factoryReset');
|
|
||||||
|
|
||||||
// OTA
|
|
||||||
export const readOTASettings = () => alovaInstance.Get<OTASettings>(`/rest/otaSettings`);
|
|
||||||
export const updateOTASettings = (data: any) => alovaInstance.Post('/rest/otaSettings', data);
|
|
||||||
|
|
||||||
// SystemLog
|
// SystemLog
|
||||||
export const readLogSettings = () => alovaInstance.Get<LogSettings>(`/rest/logSettings`);
|
export const readLogSettings = () =>
|
||||||
export const updateLogSettings = (data: any) => alovaInstance.Post('/rest/logSettings', data);
|
alovaInstance.Get<LogSettings>(`/rest/logSettings`);
|
||||||
export const fetchLog = () => alovaInstance.Post('/rest/fetchLog');
|
export const updateLogSettings = (data: LogSettings) =>
|
||||||
|
alovaInstance.Post('/rest/logSettings', data);
|
||||||
|
export const fetchLogES = () => alovaInstance.Get('/es/log');
|
||||||
|
|
||||||
// Get versions from github
|
// Get versions from GitHub
|
||||||
|
// cache for 10 minutes to stop getting the IP blocked by GitHub
|
||||||
export const getStableVersion = () =>
|
export const getStableVersion = () =>
|
||||||
alovaInstanceGH.Get('latest', {
|
alovaInstanceGH.Get('latest', {
|
||||||
transformData(response: any) {
|
cacheFor: 60 * 10 * 1000,
|
||||||
return response.data.name.substring(1);
|
transform(response: { data: { name: string; published_at: string } }) {
|
||||||
|
return {
|
||||||
|
name: response.data.name.substring(1),
|
||||||
|
published_at: response.data.published_at
|
||||||
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
export const getDevVersion = () =>
|
export const getDevVersion = () =>
|
||||||
alovaInstanceGH.Get('tags/latest', {
|
alovaInstanceGH.Get('tags/latest', {
|
||||||
transformData(response: any) {
|
cacheFor: 60 * 10 * 1000,
|
||||||
return response.data.name.split(/\s+/).splice(-1)[0].substring(1);
|
transform(response: { data: { name: string; published_at: string } }) {
|
||||||
|
return {
|
||||||
|
name: response.data.name.split(/\s+/).splice(-1)[0].substring(1),
|
||||||
|
published_at: response.data.published_at
|
||||||
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -36,7 +40,6 @@ export const uploadFile = (file: File) => {
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
return alovaInstance.Post('/rest/uploadFile', formData, {
|
return alovaInstance.Post('/rest/uploadFile', formData, {
|
||||||
timeout: 60000, // override timeout for uploading firmware - 1 minute
|
timeout: 60000 // override timeout for uploading firmware - 1 minute
|
||||||
enableUpload: true
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ try {
|
|||||||
export class Unpackr {
|
export class Unpackr {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
if (options) {
|
if (options) {
|
||||||
if (options.useRecords === false && options.mapsAsObjects === undefined) options.mapsAsObjects = true;
|
if (options.useRecords === false && options.mapsAsObjects === undefined)
|
||||||
|
options.mapsAsObjects = true;
|
||||||
if (options.sequential && options.trusted !== false) {
|
if (options.sequential && options.trusted !== false) {
|
||||||
options.trusted = true;
|
options.trusted = true;
|
||||||
if (!options.structures && options.useRecords != false) {
|
if (!options.structures && options.useRecords != false) {
|
||||||
@@ -46,7 +47,8 @@ export class Unpackr {
|
|||||||
if (!options.maxSharedStructures) options.maxSharedStructures = 0;
|
if (!options.maxSharedStructures) options.maxSharedStructures = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (options.structures) options.structures.sharedLength = options.structures.length;
|
if (options.structures)
|
||||||
|
options.structures.sharedLength = options.structures.length;
|
||||||
else if (options.getStructures) {
|
else if (options.getStructures) {
|
||||||
(options.structures = []).uninitialized = true; // this is what we use to denote an uninitialized structures
|
(options.structures = []).uninitialized = true; // this is what we use to denote an uninitialized structures
|
||||||
options.structures.sharedLength = 0;
|
options.structures.sharedLength = 0;
|
||||||
@@ -63,11 +65,14 @@ export class Unpackr {
|
|||||||
// re-entrant execution, save the state and restore it after we do this unpack
|
// re-entrant execution, save the state and restore it after we do this unpack
|
||||||
return saveState(() => {
|
return saveState(() => {
|
||||||
clearSource();
|
clearSource();
|
||||||
return this ? this.unpack(source, options) : Unpackr.prototype.unpack.call(defaultOptions, source, options);
|
return this
|
||||||
|
? this.unpack(source, options)
|
||||||
|
: Unpackr.prototype.unpack.call(defaultOptions, source, options);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!source.buffer && source.constructor === ArrayBuffer)
|
if (!source.buffer && source.constructor === ArrayBuffer)
|
||||||
source = typeof Buffer !== 'undefined' ? Buffer.from(source) : new Uint8Array(source);
|
source =
|
||||||
|
typeof Buffer !== 'undefined' ? Buffer.from(source) : new Uint8Array(source);
|
||||||
if (typeof options === 'object') {
|
if (typeof options === 'object') {
|
||||||
srcEnd = options.end || source.length;
|
srcEnd = options.end || source.length;
|
||||||
position = options.start || 0;
|
position = options.start || 0;
|
||||||
@@ -86,14 +91,21 @@ export class Unpackr {
|
|||||||
// new ones
|
// new ones
|
||||||
try {
|
try {
|
||||||
dataView =
|
dataView =
|
||||||
source.dataView || (source.dataView = new DataView(source.buffer, source.byteOffset, source.byteLength));
|
source.dataView ||
|
||||||
|
(source.dataView = new DataView(
|
||||||
|
source.buffer,
|
||||||
|
source.byteOffset,
|
||||||
|
source.byteLength
|
||||||
|
));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// if it doesn't have a buffer, maybe it is the wrong type of object
|
// if it doesn't have a buffer, maybe it is the wrong type of object
|
||||||
src = null;
|
src = null;
|
||||||
if (source instanceof Uint8Array) throw error;
|
if (source instanceof Uint8Array) throw error;
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Source must be a Uint8Array or Buffer but was a ' +
|
'Source must be a Uint8Array or Buffer but was a ' +
|
||||||
(source && typeof source == 'object' ? source.constructor.name : typeof source)
|
(source && typeof source == 'object'
|
||||||
|
? source.constructor.name
|
||||||
|
: typeof source)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (this instanceof Unpackr) {
|
if (this instanceof Unpackr) {
|
||||||
@@ -117,7 +129,9 @@ export class Unpackr {
|
|||||||
try {
|
try {
|
||||||
sequentialMode = true;
|
sequentialMode = true;
|
||||||
const size = source.length;
|
const size = source.length;
|
||||||
const value = this ? this.unpack(source, size) : defaultUnpackr.unpack(source, size);
|
const value = this
|
||||||
|
? this.unpack(source, size)
|
||||||
|
: defaultUnpackr.unpack(source, size);
|
||||||
if (forEach) {
|
if (forEach) {
|
||||||
if (forEach(value) === false) return;
|
if (forEach(value) === false) return;
|
||||||
while (position < size) {
|
while (position < size) {
|
||||||
@@ -145,9 +159,11 @@ export class Unpackr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_mergeStructures(loadedStructures, existingStructures) {
|
_mergeStructures(loadedStructures, existingStructures) {
|
||||||
if (onLoadedStructures) loadedStructures = onLoadedStructures.call(this, loadedStructures);
|
if (onLoadedStructures)
|
||||||
|
loadedStructures = onLoadedStructures.call(this, loadedStructures);
|
||||||
loadedStructures = loadedStructures || [];
|
loadedStructures = loadedStructures || [];
|
||||||
if (Object.isFrozen(loadedStructures)) loadedStructures = loadedStructures.map((structure) => structure.slice(0));
|
if (Object.isFrozen(loadedStructures))
|
||||||
|
loadedStructures = loadedStructures.map((structure) => structure.slice(0));
|
||||||
for (let i = 0, l = loadedStructures.length; i < l; i++) {
|
for (let i = 0, l = loadedStructures.length; i < l; i++) {
|
||||||
const structure = loadedStructures[i];
|
const structure = loadedStructures[i];
|
||||||
if (structure) {
|
if (structure) {
|
||||||
@@ -162,7 +178,8 @@ export class Unpackr {
|
|||||||
const existing = existingStructures[id];
|
const existing = existingStructures[id];
|
||||||
if (existing) {
|
if (existing) {
|
||||||
if (structure)
|
if (structure)
|
||||||
(loadedStructures.restoreStructures || (loadedStructures.restoreStructures = []))[id] = structure;
|
(loadedStructures.restoreStructures ||
|
||||||
|
(loadedStructures.restoreStructures = []))[id] = structure;
|
||||||
loadedStructures[id] = existing;
|
loadedStructures[id] = existing;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,10 +198,16 @@ export function checkedRead(options: any) {
|
|||||||
try {
|
try {
|
||||||
if (!currentUnpackr.trusted && !sequentialMode) {
|
if (!currentUnpackr.trusted && !sequentialMode) {
|
||||||
const sharedLength = currentStructures.sharedLength || 0;
|
const sharedLength = currentStructures.sharedLength || 0;
|
||||||
if (sharedLength < currentStructures.length) currentStructures.length = sharedLength;
|
if (sharedLength < currentStructures.length)
|
||||||
|
currentStructures.length = sharedLength;
|
||||||
}
|
}
|
||||||
let result;
|
let result;
|
||||||
if (currentUnpackr.randomAccessStructure && src[position] < 0x40 && src[position] >= 0x20 && readStruct) {
|
if (
|
||||||
|
currentUnpackr.randomAccessStructure &&
|
||||||
|
src[position] < 0x40 &&
|
||||||
|
src[position] >= 0x20 &&
|
||||||
|
readStruct
|
||||||
|
) {
|
||||||
result = readStruct(src, position, srcEnd, currentUnpackr);
|
result = readStruct(src, position, srcEnd, currentUnpackr);
|
||||||
src = null; // dispose of this so that recursive unpack calls don't save state
|
src = null; // dispose of this so that recursive unpack calls don't save state
|
||||||
if (!(options && options.lazy) && result) result = result.toJSON();
|
if (!(options && options.lazy) && result) result = result.toJSON();
|
||||||
@@ -198,7 +221,8 @@ export function checkedRead(options: any) {
|
|||||||
|
|
||||||
if (position == srcEnd) {
|
if (position == srcEnd) {
|
||||||
// finished reading this source, cleanup references
|
// finished reading this source, cleanup references
|
||||||
if (currentStructures && currentStructures.restoreStructures) restoreStructures();
|
if (currentStructures && currentStructures.restoreStructures)
|
||||||
|
restoreStructures();
|
||||||
currentStructures = null;
|
currentStructures = null;
|
||||||
src = null;
|
src = null;
|
||||||
if (referenceMap) referenceMap = null;
|
if (referenceMap) referenceMap = null;
|
||||||
@@ -208,10 +232,9 @@ export function checkedRead(options: any) {
|
|||||||
} else if (!sequentialMode) {
|
} else if (!sequentialMode) {
|
||||||
let jsonView;
|
let jsonView;
|
||||||
try {
|
try {
|
||||||
jsonView = JSON.stringify(result, (_, value) => (typeof value === 'bigint' ? `${value}n` : value)).slice(
|
jsonView = JSON.stringify(result, (_, value) =>
|
||||||
0,
|
typeof value === 'bigint' ? `${value}n` : value
|
||||||
100
|
).slice(0, 100);
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
jsonView = '(JSON view not available ' + error + ')';
|
jsonView = '(JSON view not available ' + error + ')';
|
||||||
}
|
}
|
||||||
@@ -220,9 +243,14 @@ export function checkedRead(options: any) {
|
|||||||
// else more to read, but we are reading sequentially, so don't clear source yet
|
// else more to read, but we are reading sequentially, so don't clear source yet
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (currentStructures && currentStructures.restoreStructures) restoreStructures();
|
if (currentStructures && currentStructures.restoreStructures)
|
||||||
|
restoreStructures();
|
||||||
clearSource();
|
clearSource();
|
||||||
if (error instanceof RangeError || error.message.startsWith('Unexpected end of buffer') || position > srcEnd) {
|
if (
|
||||||
|
error instanceof RangeError ||
|
||||||
|
error.message.startsWith('Unexpected end of buffer') ||
|
||||||
|
position > srcEnd
|
||||||
|
) {
|
||||||
error.incomplete = true;
|
error.incomplete = true;
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
@@ -243,7 +271,8 @@ export function read() {
|
|||||||
if (token < 0x40) return token;
|
if (token < 0x40) return token;
|
||||||
else {
|
else {
|
||||||
const structure =
|
const structure =
|
||||||
currentStructures[token & 0x3f] || (currentUnpackr.getStructures && loadStructures()[token & 0x3f]);
|
currentStructures[token & 0x3f] ||
|
||||||
|
(currentUnpackr.getStructures && loadStructures()[token & 0x3f]);
|
||||||
if (structure) {
|
if (structure) {
|
||||||
if (!structure.read) {
|
if (!structure.read) {
|
||||||
structure.read = createStructureReader(structure, token & 0x3f);
|
structure.read = createStructureReader(structure, token & 0x3f);
|
||||||
@@ -282,7 +311,10 @@ export function read() {
|
|||||||
// fixstr
|
// fixstr
|
||||||
const length = token - 0xa0;
|
const length = token - 0xa0;
|
||||||
if (srcStringEnd >= position) {
|
if (srcStringEnd >= position) {
|
||||||
return srcString.slice(position - srcStringStart, (position += length) - srcStringStart);
|
return srcString.slice(
|
||||||
|
position - srcStringStart,
|
||||||
|
(position += length) - srcStringStart
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (srcStringEnd == 0 && srcEnd < 140) {
|
if (srcStringEnd == 0 && srcEnd < 140) {
|
||||||
// for small blocks, avoiding the overhead of the extract call is helpful
|
// for small blocks, avoiding the overhead of the extract call is helpful
|
||||||
@@ -298,8 +330,16 @@ export function read() {
|
|||||||
case 0xc1:
|
case 0xc1:
|
||||||
if (bundledStrings) {
|
if (bundledStrings) {
|
||||||
value = read(); // followed by the length of the string in characters (not bytes!)
|
value = read(); // followed by the length of the string in characters (not bytes!)
|
||||||
if (value > 0) return bundledStrings[1].slice(bundledStrings.position1, (bundledStrings.position1 += value));
|
if (value > 0)
|
||||||
else return bundledStrings[0].slice(bundledStrings.position0, (bundledStrings.position0 -= value));
|
return bundledStrings[1].slice(
|
||||||
|
bundledStrings.position1,
|
||||||
|
(bundledStrings.position1 += value)
|
||||||
|
);
|
||||||
|
else
|
||||||
|
return bundledStrings[0].slice(
|
||||||
|
bundledStrings.position0,
|
||||||
|
(bundledStrings.position0 -= value)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return C1; // "never-used", return special object to denote that
|
return C1; // "never-used", return special object to denote that
|
||||||
case 0xc2:
|
case 0xc2:
|
||||||
@@ -338,7 +378,8 @@ export function read() {
|
|||||||
value = dataView.getFloat32(position);
|
value = dataView.getFloat32(position);
|
||||||
if (currentUnpackr.useFloat32 > 2) {
|
if (currentUnpackr.useFloat32 > 2) {
|
||||||
// this does rounding of numbers that were encoded in 32-bit float to nearest significant decimal digit that could be preserved
|
// this does rounding of numbers that were encoded in 32-bit float to nearest significant decimal digit that could be preserved
|
||||||
const multiplier = mult10[((src[position] & 0x7f) << 1) | (src[position + 1] >> 7)];
|
const multiplier =
|
||||||
|
mult10[((src[position] & 0x7f) << 1) | (src[position + 1] >> 7)];
|
||||||
position += 4;
|
position += 4;
|
||||||
return ((multiplier * value + (value > 0 ? 0.5 : -0.5)) >> 0) / multiplier;
|
return ((multiplier * value + (value > 0 ? 0.5 : -0.5)) >> 0) / multiplier;
|
||||||
}
|
}
|
||||||
@@ -391,7 +432,8 @@ export function read() {
|
|||||||
value = dataView.getBigInt64(position).toString();
|
value = dataView.getBigInt64(position).toString();
|
||||||
} else if (currentUnpackr.int64AsType === 'auto') {
|
} else if (currentUnpackr.int64AsType === 'auto') {
|
||||||
value = dataView.getBigInt64(position);
|
value = dataView.getBigInt64(position);
|
||||||
if (value >= BigInt(-2) << BigInt(52) && value <= BigInt(2) << BigInt(52)) value = Number(value);
|
if (value >= BigInt(-2) << BigInt(52) && value <= BigInt(2) << BigInt(52))
|
||||||
|
value = Number(value);
|
||||||
} else value = dataView.getBigInt64(position);
|
} else value = dataView.getBigInt64(position);
|
||||||
position += 8;
|
position += 8;
|
||||||
return value;
|
return value;
|
||||||
@@ -433,7 +475,10 @@ export function read() {
|
|||||||
// str 8
|
// str 8
|
||||||
value = src[position++];
|
value = src[position++];
|
||||||
if (srcStringEnd >= position) {
|
if (srcStringEnd >= position) {
|
||||||
return srcString.slice(position - srcStringStart, (position += value) - srcStringStart);
|
return srcString.slice(
|
||||||
|
position - srcStringStart,
|
||||||
|
(position += value) - srcStringStart
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return readString8(value);
|
return readString8(value);
|
||||||
case 0xda:
|
case 0xda:
|
||||||
@@ -441,7 +486,10 @@ export function read() {
|
|||||||
value = dataView.getUint16(position);
|
value = dataView.getUint16(position);
|
||||||
position += 2;
|
position += 2;
|
||||||
if (srcStringEnd >= position) {
|
if (srcStringEnd >= position) {
|
||||||
return srcString.slice(position - srcStringStart, (position += value) - srcStringStart);
|
return srcString.slice(
|
||||||
|
position - srcStringStart,
|
||||||
|
(position += value) - srcStringStart
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return readString16(value);
|
return readString16(value);
|
||||||
case 0xdb:
|
case 0xdb:
|
||||||
@@ -449,7 +497,10 @@ export function read() {
|
|||||||
value = dataView.getUint32(position);
|
value = dataView.getUint32(position);
|
||||||
position += 4;
|
position += 4;
|
||||||
if (srcStringEnd >= position) {
|
if (srcStringEnd >= position) {
|
||||||
return srcString.slice(position - srcStringStart, (position += value) - srcStringStart);
|
return srcString.slice(
|
||||||
|
position - srcStringStart,
|
||||||
|
(position += value) - srcStringStart
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return readString32(value);
|
return readString32(value);
|
||||||
case 0xdc:
|
case 0xdc:
|
||||||
@@ -498,13 +549,14 @@ function createStructureReader(structure, firstId) {
|
|||||||
key === '__proto__'
|
key === '__proto__'
|
||||||
? '__proto_:r()'
|
? '__proto_:r()'
|
||||||
: validName.test(key)
|
: validName.test(key)
|
||||||
? key + ':r()'
|
? key + ':r()'
|
||||||
: '[' + JSON.stringify(key) + ']:r()'
|
: '[' + JSON.stringify(key) + ']:r()'
|
||||||
)
|
)
|
||||||
.join(',') +
|
.join(',') +
|
||||||
'})}'
|
'})}'
|
||||||
)(read));
|
)(read));
|
||||||
if (structure.highByte === 0) structure.read = createSecondByteReader(firstId, structure.read);
|
if (structure.highByte === 0)
|
||||||
|
structure.read = createSecondByteReader(firstId, structure.read);
|
||||||
return readObject(); // second byte is already read, if there is one so immediately read object
|
return readObject(); // second byte is already read, if there is one so immediately read object
|
||||||
}
|
}
|
||||||
const object = {};
|
const object = {};
|
||||||
@@ -527,7 +579,8 @@ const createSecondByteReader = (firstId, read0) =>
|
|||||||
function () {
|
function () {
|
||||||
const highByte = src[position++];
|
const highByte = src[position++];
|
||||||
if (highByte === 0) return read0();
|
if (highByte === 0) return read0();
|
||||||
const id = firstId < 32 ? -(firstId + (highByte << 5)) : firstId + (highByte << 5);
|
const id =
|
||||||
|
firstId < 32 ? -(firstId + (highByte << 5)) : firstId + (highByte << 5);
|
||||||
const structure = currentStructures[id] || loadStructures()[id];
|
const structure = currentStructures[id] || loadStructures()[id];
|
||||||
if (!structure) {
|
if (!structure) {
|
||||||
throw new Error('Record id is not defined for ' + id);
|
throw new Error('Record id is not defined for ' + id);
|
||||||
@@ -542,7 +595,10 @@ export function loadStructures() {
|
|||||||
src = null;
|
src = null;
|
||||||
return currentUnpackr.getStructures();
|
return currentUnpackr.getStructures();
|
||||||
});
|
});
|
||||||
return (currentStructures = currentUnpackr._mergeStructures(loadedStructures, currentStructures));
|
return (currentStructures = currentUnpackr._mergeStructures(
|
||||||
|
loadedStructures,
|
||||||
|
currentStructures
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
var readFixedString = readStringJS;
|
var readFixedString = readStringJS;
|
||||||
@@ -563,7 +619,11 @@ export function setExtractor(extractStrings) {
|
|||||||
if (string == null) {
|
if (string == null) {
|
||||||
if (bundledStrings) return readStringJS(length);
|
if (bundledStrings) return readStringJS(length);
|
||||||
const byteOffset = src.byteOffset;
|
const byteOffset = src.byteOffset;
|
||||||
const extraction = extractStrings(position - headerLength + byteOffset, srcEnd + byteOffset, src.buffer);
|
const extraction = extractStrings(
|
||||||
|
position - headerLength + byteOffset,
|
||||||
|
srcEnd + byteOffset,
|
||||||
|
src.buffer
|
||||||
|
);
|
||||||
if (typeof extraction == 'string') {
|
if (typeof extraction == 'string') {
|
||||||
string = extraction;
|
string = extraction;
|
||||||
strings = EMPTY_ARRAY;
|
strings = EMPTY_ARRAY;
|
||||||
@@ -593,7 +653,8 @@ function readStringJS(length) {
|
|||||||
if (length < 16) {
|
if (length < 16) {
|
||||||
if ((result = shortStringInJS(length))) return result;
|
if ((result = shortStringInJS(length))) return result;
|
||||||
}
|
}
|
||||||
if (length > 64 && decoder) return decoder.decode(src.subarray(position, (position += length)));
|
if (length > 64 && decoder)
|
||||||
|
return decoder.decode(src.subarray(position, (position += length)));
|
||||||
const end = position + length;
|
const end = position + length;
|
||||||
const units = [];
|
const units = [];
|
||||||
result = '';
|
result = '';
|
||||||
@@ -616,7 +677,8 @@ function readStringJS(length) {
|
|||||||
const byte2 = src[position++] & 0x3f;
|
const byte2 = src[position++] & 0x3f;
|
||||||
const byte3 = src[position++] & 0x3f;
|
const byte3 = src[position++] & 0x3f;
|
||||||
const byte4 = src[position++] & 0x3f;
|
const byte4 = src[position++] & 0x3f;
|
||||||
let unit = ((byte1 & 0x07) << 0x12) | (byte2 << 0x0c) | (byte3 << 0x06) | byte4;
|
let unit =
|
||||||
|
((byte1 & 0x07) << 0x12) | (byte2 << 0x0c) | (byte3 << 0x06) | byte4;
|
||||||
if (unit > 0xffff) {
|
if (unit > 0xffff) {
|
||||||
unit -= 0x10000;
|
unit -= 0x10000;
|
||||||
units.push(((unit >>> 10) & 0x3ff) | 0xd800);
|
units.push(((unit >>> 10) & 0x3ff) | 0xd800);
|
||||||
@@ -810,7 +872,8 @@ function shortStringInJS(length) {
|
|||||||
position -= 14;
|
position -= 14;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (length < 15) return fromCharCode(a, b, c, d, e, f, g, h, i, j, k, l, m, n);
|
if (length < 15)
|
||||||
|
return fromCharCode(a, b, c, d, e, f, g, h, i, j, k, l, m, n);
|
||||||
const o = src[position++];
|
const o = src[position++];
|
||||||
if ((o & 0x80) > 0) {
|
if ((o & 0x80) > 0) {
|
||||||
position -= 15;
|
position -= 15;
|
||||||
@@ -862,14 +925,17 @@ function readExt(length) {
|
|||||||
const type = src[position++];
|
const type = src[position++];
|
||||||
if (currentExtensions[type]) {
|
if (currentExtensions[type]) {
|
||||||
let end;
|
let end;
|
||||||
return currentExtensions[type](src.subarray(position, (end = position += length)), (readPosition) => {
|
return currentExtensions[type](
|
||||||
position = readPosition;
|
src.subarray(position, (end = position += length)),
|
||||||
try {
|
(readPosition) => {
|
||||||
return read();
|
position = readPosition;
|
||||||
} finally {
|
try {
|
||||||
position = end;
|
return read();
|
||||||
|
} finally {
|
||||||
|
position = end;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
} else throw new Error('Unknown extension type ' + type);
|
} else throw new Error('Unknown extension type ' + type);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -881,14 +947,20 @@ function readKey() {
|
|||||||
length = length - 0xa0;
|
length = length - 0xa0;
|
||||||
if (srcStringEnd >= position)
|
if (srcStringEnd >= position)
|
||||||
// if it has been extracted, must use it (and faster anyway)
|
// if it has been extracted, must use it (and faster anyway)
|
||||||
return srcString.slice(position - srcStringStart, (position += length) - srcStringStart);
|
return srcString.slice(
|
||||||
|
position - srcStringStart,
|
||||||
|
(position += length) - srcStringStart
|
||||||
|
);
|
||||||
else if (!(srcStringEnd == 0 && srcEnd < 180)) return readFixedString(length);
|
else if (!(srcStringEnd == 0 && srcEnd < 180)) return readFixedString(length);
|
||||||
} else {
|
} else {
|
||||||
// not cacheable, go back and do a standard read
|
// not cacheable, go back and do a standard read
|
||||||
position--;
|
position--;
|
||||||
return read().toString();
|
return read().toString();
|
||||||
}
|
}
|
||||||
const key = ((length << 5) ^ (length > 1 ? dataView.getUint16(position) : length > 0 ? src[position] : 0)) & 0xfff;
|
const key =
|
||||||
|
((length << 5) ^
|
||||||
|
(length > 1 ? dataView.getUint16(position) : length > 0 ? src[position] : 0)) &
|
||||||
|
0xfff;
|
||||||
let entry = keyCache[key];
|
let entry = keyCache[key];
|
||||||
let checkPosition = position;
|
let checkPosition = position;
|
||||||
let end = position + length - 3;
|
let end = position + length - 3;
|
||||||
@@ -947,7 +1019,8 @@ const recordDefinition = (id, highByte) => {
|
|||||||
}
|
}
|
||||||
const existingStructure = currentStructures[id];
|
const existingStructure = currentStructures[id];
|
||||||
if (existingStructure && existingStructure.isShared) {
|
if (existingStructure && existingStructure.isShared) {
|
||||||
(currentStructures.restoreStructures || (currentStructures.restoreStructures = []))[id] = existingStructure;
|
(currentStructures.restoreStructures ||
|
||||||
|
(currentStructures.restoreStructures = []))[id] = existingStructure;
|
||||||
}
|
}
|
||||||
currentStructures[id] = structure;
|
currentStructures[id] = structure;
|
||||||
structure.read = createStructureReader(structure, firstByte);
|
structure.read = createStructureReader(structure, firstByte);
|
||||||
@@ -1009,7 +1082,8 @@ export const typedArrays = [
|
|||||||
currentExtensions[0x74] = (data) => {
|
currentExtensions[0x74] = (data) => {
|
||||||
const typeCode = data[0];
|
const typeCode = data[0];
|
||||||
const typedArrayName = typedArrays[typeCode];
|
const typedArrayName = typedArrays[typeCode];
|
||||||
if (!typedArrayName) throw new Error('Could not find typed array for code ' + typeCode);
|
if (!typedArrayName)
|
||||||
|
throw new Error('Could not find typed array for code ' + typeCode);
|
||||||
// we have to always slice/copy here to get a new ArrayBuffer that is word/byte aligned
|
// we have to always slice/copy here to get a new ArrayBuffer that is word/byte aligned
|
||||||
return new glbl[typedArrayName](Uint8Array.prototype.slice.call(data, 1).buffer);
|
return new glbl[typedArrayName](Uint8Array.prototype.slice.call(data, 1).buffer);
|
||||||
};
|
};
|
||||||
@@ -1033,11 +1107,20 @@ currentExtensions[0x62] = (data) => {
|
|||||||
|
|
||||||
currentExtensions[0xff] = (data) => {
|
currentExtensions[0xff] = (data) => {
|
||||||
// 32-bit date extension
|
// 32-bit date extension
|
||||||
if (data.length == 4) return new Date((data[0] * 0x1000000 + (data[1] << 16) + (data[2] << 8) + data[3]) * 1000);
|
if (data.length == 4)
|
||||||
|
return new Date(
|
||||||
|
(data[0] * 0x1000000 + (data[1] << 16) + (data[2] << 8) + data[3]) * 1000
|
||||||
|
);
|
||||||
else if (data.length == 8)
|
else if (data.length == 8)
|
||||||
return new Date(
|
return new Date(
|
||||||
((data[0] << 22) + (data[1] << 14) + (data[2] << 6) + (data[3] >> 2)) / 1000000 +
|
((data[0] << 22) + (data[1] << 14) + (data[2] << 6) + (data[3] >> 2)) /
|
||||||
((data[3] & 0x3) * 0x100000000 + data[4] * 0x1000000 + (data[5] << 16) + (data[6] << 8) + data[7]) * 1000
|
1000000 +
|
||||||
|
((data[3] & 0x3) * 0x100000000 +
|
||||||
|
data[4] * 0x1000000 +
|
||||||
|
(data[5] << 16) +
|
||||||
|
(data[6] << 8) +
|
||||||
|
data[7]) *
|
||||||
|
1000
|
||||||
);
|
);
|
||||||
else if (data.length == 12)
|
else if (data.length == 12)
|
||||||
return new Date(
|
return new Date(
|
||||||
@@ -1070,7 +1153,10 @@ function saveState(callback) {
|
|||||||
|
|
||||||
const savedSrc = new Uint8Array(src.slice(0, srcEnd)); // we copy the data in case it changes while external data is processed
|
const savedSrc = new Uint8Array(src.slice(0, srcEnd)); // we copy the data in case it changes while external data is processed
|
||||||
const savedStructures = currentStructures;
|
const savedStructures = currentStructures;
|
||||||
const savedStructuresContents = currentStructures.slice(0, currentStructures.length);
|
const savedStructuresContents = currentStructures.slice(
|
||||||
|
0,
|
||||||
|
currentStructures.length
|
||||||
|
);
|
||||||
const savedPackr = currentUnpackr;
|
const savedPackr = currentUnpackr;
|
||||||
const savedSequentialMode = sequentialMode;
|
const savedSequentialMode = sequentialMode;
|
||||||
const value = callback();
|
const value = callback();
|
||||||
@@ -1122,7 +1208,10 @@ const u8Array = new Uint8Array(f32Array.buffer, 0, 4);
|
|||||||
export function roundFloat32(float32Number) {
|
export function roundFloat32(float32Number) {
|
||||||
f32Array[0] = float32Number;
|
f32Array[0] = float32Number;
|
||||||
const multiplier = mult10[((u8Array[3] & 0x7f) << 1) | (u8Array[2] >> 7)];
|
const multiplier = mult10[((u8Array[3] & 0x7f) << 1) | (u8Array[2] >> 7)];
|
||||||
return ((multiplier * float32Number + (float32Number > 0 ? 0.5 : -0.5)) >> 0) / multiplier;
|
return (
|
||||||
|
((multiplier * float32Number + (float32Number > 0 ? 0.5 : -0.5)) >> 0) /
|
||||||
|
multiplier
|
||||||
|
);
|
||||||
}
|
}
|
||||||
export function setReadStruct(updatedReadStruct, loadedStructs, saveState) {
|
export function setReadStruct(updatedReadStruct, loadedStructs, saveState) {
|
||||||
readStruct = updatedReadStruct;
|
readStruct = updatedReadStruct;
|
||||||
|
|||||||
@@ -1,27 +1,41 @@
|
|||||||
import AddIcon from '@mui/icons-material/Add';
|
import { useCallback, useState } from 'react';
|
||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
import { useBlocker } from 'react-router';
|
||||||
import WarningIcon from '@mui/icons-material/Warning';
|
|
||||||
import { Button, Typography, Box } from '@mui/material';
|
|
||||||
import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table';
|
|
||||||
import { useTheme } from '@table-library/react-table-library/theme';
|
|
||||||
// eslint-disable-next-line import/named
|
|
||||||
import { updateState, useRequest } from 'alova';
|
|
||||||
import { useState, useCallback } from 'react';
|
|
||||||
import { unstable_useBlocker as useBlocker } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import SettingsEntitiesDialog from './SettingsEntitiesDialog';
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
import * as EMSESP from './api';
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
import { DeviceValueUOM_s } from './types';
|
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
|
||||||
import { entityItemValidation } from './validators';
|
import WarningIcon from '@mui/icons-material/Warning';
|
||||||
import type { EntityItem } from './types';
|
import { Box, Button, Typography } from '@mui/material';
|
||||||
import type { FC } from 'react';
|
|
||||||
import { ButtonRow, FormLoader, SectionContent, BlockNavigation } from 'components';
|
|
||||||
|
|
||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Cell,
|
||||||
|
Header,
|
||||||
|
HeaderCell,
|
||||||
|
HeaderRow,
|
||||||
|
Row,
|
||||||
|
Table
|
||||||
|
} from '@table-library/react-table-library/table';
|
||||||
|
import { useTheme } from '@table-library/react-table-library/theme';
|
||||||
|
import { updateState, useRequest } from 'alova/client';
|
||||||
|
import {
|
||||||
|
BlockNavigation,
|
||||||
|
ButtonRow,
|
||||||
|
FormLoader,
|
||||||
|
SectionContent,
|
||||||
|
useLayoutTitle
|
||||||
|
} from 'components';
|
||||||
import { useI18nContext } from 'i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
import { useInterval } from 'utils';
|
||||||
|
|
||||||
const SettingsEntities: FC = () => {
|
import { readCustomEntities, writeCustomEntities } from '../../api/app';
|
||||||
|
import SettingsCustomEntitiesDialog from './CustomEntitiesDialog';
|
||||||
|
import { DeviceValueTypeNames, DeviceValueUOM_s } from './types';
|
||||||
|
import type { Entities, EntityItem } from './types';
|
||||||
|
import { entityItemValidation } from './validators';
|
||||||
|
|
||||||
|
const CustomEntities = () => {
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
const [numChanges, setNumChanges] = useState<number>(0);
|
const [numChanges, setNumChanges] = useState<number>(0);
|
||||||
const blocker = useBlocker(numChanges !== 0);
|
const blocker = useBlocker(numChanges !== 0);
|
||||||
@@ -29,20 +43,31 @@ const SettingsEntities: FC = () => {
|
|||||||
const [creating, setCreating] = useState<boolean>(false);
|
const [creating, setCreating] = useState<boolean>(false);
|
||||||
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useLayoutTitle(LL.CUSTOM_ENTITIES(0));
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: entities,
|
data: entities,
|
||||||
send: fetchEntities,
|
send: fetchEntities,
|
||||||
error
|
error
|
||||||
} = useRequest(EMSESP.readEntities, {
|
} = useRequest(readCustomEntities, {
|
||||||
initialData: [],
|
initialData: []
|
||||||
force: true
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { send: writeEntities } = useRequest((data) => EMSESP.writeEntities(data), { immediate: false });
|
useInterval(() => {
|
||||||
|
if (!dialogOpen && !numChanges) {
|
||||||
|
void fetchEntities();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { send: writeEntities } = useRequest(
|
||||||
|
(data: Entities) => writeCustomEntities(data),
|
||||||
|
{ immediate: false }
|
||||||
|
);
|
||||||
|
|
||||||
function hasEntityChanged(ei: EntityItem) {
|
function hasEntityChanged(ei: EntityItem) {
|
||||||
return (
|
return (
|
||||||
ei.id !== ei.o_id ||
|
ei.id !== ei.o_id ||
|
||||||
|
ei.ram !== ei.o_ram ||
|
||||||
(ei?.name || '') !== (ei?.o_name || '') ||
|
(ei?.name || '') !== (ei?.o_name || '') ||
|
||||||
ei.device_id !== ei.o_device_id ||
|
ei.device_id !== ei.o_device_id ||
|
||||||
ei.type_id !== ei.o_type_id ||
|
ei.type_id !== ei.o_type_id ||
|
||||||
@@ -51,13 +76,15 @@ const SettingsEntities: FC = () => {
|
|||||||
ei.factor !== ei.o_factor ||
|
ei.factor !== ei.o_factor ||
|
||||||
ei.value_type !== ei.o_value_type ||
|
ei.value_type !== ei.o_value_type ||
|
||||||
ei.writeable !== ei.o_writeable ||
|
ei.writeable !== ei.o_writeable ||
|
||||||
ei.deleted !== ei.o_deleted
|
ei.hide !== ei.o_hide ||
|
||||||
|
ei.deleted !== ei.o_deleted ||
|
||||||
|
(ei.value || '') !== (ei.o_value || '')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const entity_theme = useTheme({
|
const entity_theme = useTheme({
|
||||||
Table: `
|
Table: `
|
||||||
--data-table-library_grid-template-columns: repeat(1, minmax(60px, 1fr)) minmax(80px, auto) 80px 80px 80px;
|
--data-table-library_grid-template-columns: repeat(1, minmax(60px, 1fr)) minmax(80px, auto) 80px 80px 80px 120px;
|
||||||
`,
|
`,
|
||||||
BaseRow: `
|
BaseRow: `
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -81,6 +108,9 @@ const SettingsEntities: FC = () => {
|
|||||||
&:nth-of-type(5) {
|
&:nth-of-type(5) {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
&:nth-of-type(6) {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
HeaderRow: `
|
HeaderRow: `
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@@ -96,15 +126,10 @@ const SettingsEntities: FC = () => {
|
|||||||
position: relative;
|
position: relative;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
.td {
|
.td {
|
||||||
border-top: 1px solid #565656;
|
|
||||||
border-bottom: 1px solid #565656;
|
border-bottom: 1px solid #565656;
|
||||||
}
|
}
|
||||||
&:hover .td {
|
&:hover .td {
|
||||||
border-top: 1px solid #177ac9;
|
background-color: #177ac9;
|
||||||
border-bottom: 1px solid #177ac9;
|
|
||||||
}
|
|
||||||
&:nth-of-type(odd) .td {
|
|
||||||
background-color: #303030;
|
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
});
|
});
|
||||||
@@ -115,6 +140,7 @@ const SettingsEntities: FC = () => {
|
|||||||
.filter((ei) => !ei.deleted)
|
.filter((ei) => !ei.deleted)
|
||||||
.map((condensed_ei) => ({
|
.map((condensed_ei) => ({
|
||||||
id: condensed_ei.id,
|
id: condensed_ei.id,
|
||||||
|
ram: condensed_ei.ram,
|
||||||
name: condensed_ei.name,
|
name: condensed_ei.name,
|
||||||
device_id: condensed_ei.device_id,
|
device_id: condensed_ei.device_id,
|
||||||
type_id: condensed_ei.type_id,
|
type_id: condensed_ei.type_id,
|
||||||
@@ -122,14 +148,16 @@ const SettingsEntities: FC = () => {
|
|||||||
factor: condensed_ei.factor,
|
factor: condensed_ei.factor,
|
||||||
uom: condensed_ei.uom,
|
uom: condensed_ei.uom,
|
||||||
writeable: condensed_ei.writeable,
|
writeable: condensed_ei.writeable,
|
||||||
value_type: condensed_ei.value_type
|
hide: condensed_ei.hide,
|
||||||
|
value_type: condensed_ei.value_type,
|
||||||
|
value: condensed_ei.value
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success(LL.ENTITIES_UPDATED());
|
toast.success(LL.ENTITIES_UPDATED());
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((error: Error) => {
|
||||||
toast.error(err.message);
|
toast.error(error.message);
|
||||||
})
|
})
|
||||||
.finally(async () => {
|
.finally(async () => {
|
||||||
await fetchEntities();
|
await fetchEntities();
|
||||||
@@ -155,39 +183,66 @@ const SettingsEntities: FC = () => {
|
|||||||
|
|
||||||
const onDialogSave = (updatedItem: EntityItem) => {
|
const onDialogSave = (updatedItem: EntityItem) => {
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
|
void updateState(readCustomEntities(), (data: EntityItem[]) => {
|
||||||
updateState('entities', (data) => {
|
|
||||||
const new_data = creating
|
const new_data = creating
|
||||||
? [...data.filter((ei) => creating || ei.o_id !== updatedItem.o_id), updatedItem]
|
? [
|
||||||
: data.map((ei) => (ei.id === updatedItem.id ? { ...ei, ...updatedItem } : ei));
|
...data.filter((ei) => creating || ei.o_id !== updatedItem.o_id),
|
||||||
|
updatedItem
|
||||||
|
]
|
||||||
|
: data.map((ei) =>
|
||||||
|
ei.id === updatedItem.id ? { ...ei, ...updatedItem } : ei
|
||||||
|
);
|
||||||
setNumChanges(new_data.filter((ei) => hasEntityChanged(ei)).length);
|
setNumChanges(new_data.filter((ei) => hasEntityChanged(ei)).length);
|
||||||
return new_data;
|
return new_data;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onDialogDup = (item: EntityItem) => {
|
||||||
|
setCreating(true);
|
||||||
|
setSelectedEntityItem({
|
||||||
|
id: Math.floor(Math.random() * (Math.floor(200) - 100) + 100),
|
||||||
|
name: item.name + '_',
|
||||||
|
ram: item.ram,
|
||||||
|
device_id: item.device_id,
|
||||||
|
type_id: item.type_id,
|
||||||
|
offset: item.offset,
|
||||||
|
factor: item.factor,
|
||||||
|
uom: item.uom,
|
||||||
|
value_type: item.value_type,
|
||||||
|
writeable: item.writeable,
|
||||||
|
deleted: false,
|
||||||
|
hide: item.hide,
|
||||||
|
value: item.value
|
||||||
|
});
|
||||||
|
setDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
const addEntityItem = () => {
|
const addEntityItem = () => {
|
||||||
setCreating(true);
|
setCreating(true);
|
||||||
setSelectedEntityItem({
|
setSelectedEntityItem({
|
||||||
id: Math.floor(Math.random() * (Math.floor(200) - 100) + 100),
|
id: Math.floor(Math.random() * (Math.floor(200) - 100) + 100),
|
||||||
name: '',
|
name: '',
|
||||||
device_id: '',
|
ram: 0,
|
||||||
type_id: '',
|
device_id: '0',
|
||||||
|
type_id: '0',
|
||||||
offset: 0,
|
offset: 0,
|
||||||
factor: 1,
|
factor: 1,
|
||||||
uom: 0,
|
uom: 0,
|
||||||
value_type: 0,
|
value_type: 0,
|
||||||
writeable: false,
|
writeable: false,
|
||||||
deleted: false
|
deleted: false,
|
||||||
|
value: ''
|
||||||
});
|
});
|
||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
function formatValue(value: any, uom: number) {
|
function formatValue(value: unknown, uom: number) {
|
||||||
return value === undefined || uom === undefined
|
return value === undefined
|
||||||
? ''
|
? ''
|
||||||
: typeof value === 'number'
|
: typeof value === 'number'
|
||||||
? new Intl.NumberFormat().format(value) + (uom === 0 ? '' : ' ' + DeviceValueUOM_s[uom])
|
? new Intl.NumberFormat().format(value) +
|
||||||
: value;
|
(uom === 0 ? '' : ' ' + DeviceValueUOM_s[uom])
|
||||||
|
: (value as string) + (uom === 0 ? '' : ' ' + DeviceValueUOM_s[uom]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showHex(value: number, digit: number) {
|
function showHex(value: number, digit: number) {
|
||||||
@@ -200,25 +255,44 @@ const SettingsEntities: FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table data={{ nodes: entities.filter((ei) => !ei.deleted) }} theme={entity_theme} layout={{ custom: true }}>
|
<Table
|
||||||
{(tableList: any) => (
|
data={{
|
||||||
|
nodes: entities
|
||||||
|
.filter((ei) => !ei.deleted)
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
}}
|
||||||
|
theme={entity_theme}
|
||||||
|
layout={{ custom: true }}
|
||||||
|
>
|
||||||
|
{(tableList: EntityItem[]) => (
|
||||||
<>
|
<>
|
||||||
<Header>
|
<Header>
|
||||||
<HeaderRow>
|
<HeaderRow>
|
||||||
<HeaderCell>{LL.NAME(0)}</HeaderCell>
|
<HeaderCell>{LL.NAME(0)}</HeaderCell>
|
||||||
<HeaderCell stiff>{LL.ID_OF(LL.DEVICE())}</HeaderCell>
|
<HeaderCell stiff>{LL.ID_OF(LL.DEVICE())}</HeaderCell>
|
||||||
<HeaderCell stiff>{LL.ID_OF(LL.TYPE(1))}</HeaderCell>
|
<HeaderCell stiff>{LL.ID_OF(LL.TYPE(1))}</HeaderCell>
|
||||||
<HeaderCell stiff>Offset</HeaderCell>
|
<HeaderCell stiff>{LL.OFFSET()}</HeaderCell>
|
||||||
|
<HeaderCell stiff>{LL.TYPE(0)}</HeaderCell>
|
||||||
<HeaderCell stiff>{LL.VALUE(0)}</HeaderCell>
|
<HeaderCell stiff>{LL.VALUE(0)}</HeaderCell>
|
||||||
</HeaderRow>
|
</HeaderRow>
|
||||||
</Header>
|
</Header>
|
||||||
<Body>
|
<Body>
|
||||||
{tableList.map((ei: EntityItem) => (
|
{tableList.map((ei: EntityItem) => (
|
||||||
<Row key={ei.name} item={ei} onClick={() => editEntityItem(ei)}>
|
<Row key={ei.name} item={ei} onClick={() => editEntityItem(ei)}>
|
||||||
<Cell>{ei.name}</Cell>
|
<Cell>
|
||||||
<Cell>{showHex(ei.device_id as number, 2)}</Cell>
|
{ei.name}
|
||||||
<Cell>{showHex(ei.type_id as number, 3)}</Cell>
|
{ei.writeable && (
|
||||||
<Cell>{ei.offset}</Cell>
|
<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>
|
<Cell>{formatValue(ei.value, ei.uom)}</Cell>
|
||||||
</Row>
|
</Row>
|
||||||
))}
|
))}
|
||||||
@@ -230,30 +304,36 @@ const SettingsEntities: FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContent title={LL.CUSTOM_ENTITIES(0)} titleGutter>
|
<SectionContent>
|
||||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||||
<Box mb={2} color="warning.main">
|
<Box mb={2} color="warning.main">
|
||||||
<Typography variant="body2">{LL.ENTITIES_HELP_1()}</Typography>
|
<Typography variant="body1">{LL.ENTITIES_HELP_1()}.</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{renderEntity()}
|
{renderEntity()}
|
||||||
|
|
||||||
{selectedEntityItem && (
|
{selectedEntityItem && (
|
||||||
<SettingsEntitiesDialog
|
<SettingsCustomEntitiesDialog
|
||||||
open={dialogOpen}
|
open={dialogOpen}
|
||||||
creating={creating}
|
creating={creating}
|
||||||
onClose={onDialogClose}
|
onClose={onDialogClose}
|
||||||
onSave={onDialogSave}
|
onSave={onDialogSave}
|
||||||
|
onDup={onDialogDup}
|
||||||
selectedItem={selectedEntityItem}
|
selectedItem={selectedEntityItem}
|
||||||
validator={entityItemValidation()}
|
validator={entityItemValidation(entities, selectedEntityItem)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Box display="flex" flexWrap="wrap">
|
<Box mt={1} display="flex" flexWrap="wrap">
|
||||||
<Box flexGrow={1}>
|
<Box flexGrow={1}>
|
||||||
{numChanges > 0 && (
|
{numChanges > 0 && (
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Button startIcon={<CancelIcon />} variant="outlined" onClick={onDialogCancel} color="secondary">
|
<Button
|
||||||
|
startIcon={<CancelIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={onDialogCancel}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
{LL.CANCEL()}
|
{LL.CANCEL()}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -268,15 +348,18 @@ const SettingsEntities: FC = () => {
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Box flexWrap="nowrap" whiteSpace="nowrap">
|
<Box flexWrap="nowrap" whiteSpace="nowrap">
|
||||||
<ButtonRow>
|
<Button
|
||||||
<Button startIcon={<AddIcon />} variant="outlined" color="secondary" onClick={addEntityItem}>
|
startIcon={<AddIcon />}
|
||||||
{LL.ADD(0)}
|
variant="outlined"
|
||||||
</Button>
|
color="primary"
|
||||||
</ButtonRow>
|
onClick={addEntityItem}
|
||||||
|
>
|
||||||
|
{LL.ADD(0)}
|
||||||
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SettingsEntities;
|
export default CustomEntities;
|
||||||
431
interface/src/app/main/CustomEntitiesDialog.tsx
Normal file
431
interface/src/app/main/CustomEntitiesDialog.tsx
Normal file
@@ -0,0 +1,431 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
|
import CommentsDisabledOutlinedIcon from '@mui/icons-material/CommentsDisabledOutlined';
|
||||||
|
import DoneIcon from '@mui/icons-material/Done';
|
||||||
|
import EditOffOutlinedIcon from '@mui/icons-material/EditOffOutlined';
|
||||||
|
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
|
||||||
|
import InsertCommentOutlinedIcon from '@mui/icons-material/InsertCommentOutlined';
|
||||||
|
import RemoveIcon from '@mui/icons-material/RemoveCircleOutline';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
Grid,
|
||||||
|
InputAdornment,
|
||||||
|
MenuItem,
|
||||||
|
TextField
|
||||||
|
} from '@mui/material';
|
||||||
|
|
||||||
|
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;
|
||||||
|
onDup: (ei: EntityItem) => void;
|
||||||
|
selectedItem: EntityItem;
|
||||||
|
validator: Schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CustomEntitiesDialog = ({
|
||||||
|
open,
|
||||||
|
creating,
|
||||||
|
onClose,
|
||||||
|
onSave,
|
||||||
|
onDup,
|
||||||
|
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(),
|
||||||
|
factor:
|
||||||
|
selectedItem.value_type === DeviceValueType.BOOL
|
||||||
|
? selectedItem.factor.toString(16).toUpperCase()
|
||||||
|
: selectedItem.factor
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [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);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
editItem.value_type === DeviceValueType.BOOL &&
|
||||||
|
typeof editItem.factor === 'string'
|
||||||
|
) {
|
||||||
|
editItem.factor = parseInt(editItem.factor, 16);
|
||||||
|
}
|
||||||
|
onSave(editItem);
|
||||||
|
} catch (error) {
|
||||||
|
setFieldErrors(error as ValidateFieldsError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const remove = () => {
|
||||||
|
editItem.deleted = true;
|
||||||
|
onSave(editItem);
|
||||||
|
};
|
||||||
|
|
||||||
|
const dup = () => {
|
||||||
|
onDup(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 mt={3}>
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
icon={<InsertCommentOutlinedIcon htmlColor="white" />}
|
||||||
|
checkedIcon={<CommentsDisabledOutlinedIcon color="primary" />}
|
||||||
|
checked={editItem.hide}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
name="hide"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="API/MQTT"
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
<Grid>
|
||||||
|
<TextField
|
||||||
|
name="uom"
|
||||||
|
label={LL.UNIT()}
|
||||||
|
value={editItem.uom}
|
||||||
|
margin="normal"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
select
|
||||||
|
>
|
||||||
|
{DeviceValueUOM_s.map((val, i) => (
|
||||||
|
<MenuItem key={val} value={i}>
|
||||||
|
{val}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{editItem.ram === 0 && (
|
||||||
|
<>
|
||||||
|
<Grid mt={3}>
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
icon={<EditOffOutlinedIcon color="primary" />}
|
||||||
|
checkedIcon={<EditOutlinedIcon htmlColor="white" />}
|
||||||
|
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 as number)}
|
||||||
|
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={LL.BYTES()}
|
||||||
|
value={numberValue(editItem.factor as number)}
|
||||||
|
sx={{ width: '11ch' }}
|
||||||
|
variant="outlined"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
type="number"
|
||||||
|
slotProps={{
|
||||||
|
htmlInput: { step: '1', min: '1', max: '255' }
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
{editItem.value_type === DeviceValueType.BOOL && (
|
||||||
|
<Grid>
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors}
|
||||||
|
name="factor"
|
||||||
|
label={LL.BITMASK()}
|
||||||
|
value={editItem.factor as string}
|
||||||
|
sx={{ width: '11ch' }}
|
||||||
|
variant="outlined"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
type="string"
|
||||||
|
slotProps={{
|
||||||
|
input: {
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">0x</InputAdornment>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
htmlInput: { style: { textTransform: 'uppercase' } }
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
{!creating && (
|
||||||
|
<Box flexGrow={1}>
|
||||||
|
<Button
|
||||||
|
startIcon={<RemoveIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
color="warning"
|
||||||
|
onClick={remove}
|
||||||
|
>
|
||||||
|
{LL.REMOVE()}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
sx={{ ml: 1 }}
|
||||||
|
startIcon={<AddIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
onClick={dup}
|
||||||
|
>
|
||||||
|
{LL.DUPLICATE()}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
startIcon={<CancelIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={onClose}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
|
{LL.CANCEL()}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
startIcon={creating ? <AddIcon /> : <DoneIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={save}
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
{creating ? LL.ADD(0) : LL.UPDATE()}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CustomEntitiesDialog;
|
||||||
@@ -1,50 +1,70 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useBlocker, useLocation } from 'react-router';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
|
import EditIcon from '@mui/icons-material/Edit';
|
||||||
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
|
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
|
||||||
|
import SaveIcon from '@mui/icons-material/Save';
|
||||||
import SearchIcon from '@mui/icons-material/Search';
|
import SearchIcon from '@mui/icons-material/Search';
|
||||||
import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore';
|
import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore';
|
||||||
import WarningIcon from '@mui/icons-material/Warning';
|
import WarningIcon from '@mui/icons-material/Warning';
|
||||||
import {
|
import {
|
||||||
Button,
|
|
||||||
Typography,
|
|
||||||
Box,
|
Box,
|
||||||
MenuItem,
|
Button,
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
|
Grid,
|
||||||
|
InputAdornment,
|
||||||
|
Link,
|
||||||
|
MenuItem,
|
||||||
|
TextField,
|
||||||
ToggleButton,
|
ToggleButton,
|
||||||
ToggleButtonGroup,
|
ToggleButtonGroup,
|
||||||
Grid,
|
Typography
|
||||||
TextField,
|
|
||||||
Link,
|
|
||||||
InputAdornment
|
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table';
|
|
||||||
import { useTheme } from '@table-library/react-table-library/theme';
|
|
||||||
import { useRequest } from 'alova';
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
|
||||||
import { unstable_useBlocker as useBlocker } from 'react-router-dom';
|
|
||||||
import { toast } from 'react-toastify';
|
|
||||||
|
|
||||||
|
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 SystemMonitor from 'app/status/SystemMonitor';
|
||||||
|
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 EntityMaskToggle from './EntityMaskToggle';
|
||||||
import OptionIcon from './OptionIcon';
|
import OptionIcon from './OptionIcon';
|
||||||
import SettingsCustomizationDialog from './SettingsCustomizationDialog';
|
|
||||||
|
|
||||||
import * as EMSESP from './api';
|
|
||||||
|
|
||||||
import { DeviceEntityMask } from './types';
|
import { DeviceEntityMask } from './types';
|
||||||
import type { DeviceShort, DeviceEntity } from './types';
|
import type { APIcall, Device, DeviceEntity } from './types';
|
||||||
import type { FC } from 'react';
|
|
||||||
import { dialogStyle } from 'CustomTheme';
|
|
||||||
import * as SystemApi from 'api/system';
|
|
||||||
import { ButtonRow, SectionContent, MessageBox, BlockNavigation } from 'components';
|
|
||||||
|
|
||||||
import RestartMonitor from 'framework/system/RestartMonitor';
|
|
||||||
import { useI18nContext } from 'i18n/i18n-react';
|
|
||||||
|
|
||||||
export const APIURL = window.location.origin + '/api/';
|
export const APIURL = window.location.origin + '/api/';
|
||||||
|
|
||||||
const SettingsCustomization: FC = () => {
|
const Customizations = () => {
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
const [numChanges, setNumChanges] = useState<number>(0);
|
const [numChanges, setNumChanges] = useState<number>(0);
|
||||||
const blocker = useBlocker(numChanges !== 0);
|
const blocker = useBlocker(numChanges !== 0);
|
||||||
@@ -52,41 +72,81 @@ const SettingsCustomization: FC = () => {
|
|||||||
const [restarting, setRestarting] = useState<boolean>(false);
|
const [restarting, setRestarting] = useState<boolean>(false);
|
||||||
const [restartNeeded, setRestartNeeded] = useState<boolean>(false);
|
const [restartNeeded, setRestartNeeded] = useState<boolean>(false);
|
||||||
const [deviceEntities, setDeviceEntities] = useState<DeviceEntity[]>([]);
|
const [deviceEntities, setDeviceEntities] = useState<DeviceEntity[]>([]);
|
||||||
const [selectedDevice, setSelectedDevice] = useState<number>(-1);
|
|
||||||
const [confirmReset, setConfirmReset] = useState<boolean>(false);
|
const [confirmReset, setConfirmReset] = useState<boolean>(false);
|
||||||
const [selectedFilters, setSelectedFilters] = useState<number>(0);
|
const [selectedFilters, setSelectedFilters] = useState<number>(0);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [selectedDeviceEntity, setSelectedDeviceEntity] = useState<DeviceEntity>();
|
const [selectedDeviceEntity, setSelectedDeviceEntity] = useState<DeviceEntity>();
|
||||||
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||||
|
const [rename, setRename] = useState<boolean>(false);
|
||||||
|
|
||||||
const { send: resetCustomizations } = useRequest(EMSESP.resetCustomizations(), {
|
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
|
immediate: false
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: devices } = useRequest(EMSESP.readDevices);
|
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: writeCustomEntities } = useRequest((data) => EMSESP.writeCustomEntities(data), { immediate: false });
|
const { send: sendResetCustomizations } = useRequest(resetCustomizations(), {
|
||||||
|
|
||||||
const { send: readDeviceEntities, onSuccess: onSuccess } = useRequest((data) => EMSESP.readDeviceEntities(data), {
|
|
||||||
initialData: [],
|
|
||||||
immediate: false
|
immediate: false
|
||||||
});
|
});
|
||||||
|
|
||||||
const setOriginalSettings = (data: DeviceEntity[]) => {
|
const { send: sendDeviceName } = useRequest(
|
||||||
setDeviceEntities(data.map((de) => ({ ...de, o_m: de.m, o_cn: de.cn, o_mi: de.mi, o_ma: de.ma })));
|
(data: { id: number; name: string }) => writeDeviceName(data),
|
||||||
};
|
{
|
||||||
|
immediate: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
onSuccess((event) => {
|
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);
|
setOriginalSettings(event.data);
|
||||||
});
|
});
|
||||||
|
|
||||||
const { send: restartCommand } = useRequest(SystemApi.restart(), {
|
const setOriginalSettings = (data: DeviceEntity[]) => {
|
||||||
immediate: false
|
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({
|
const entities_theme = useTheme({
|
||||||
Table: `
|
Table: `
|
||||||
--data-table-library_grid-template-columns: 150px repeat(1, minmax(80px, 1fr)) 45px minmax(45px, auto) minmax(120px, auto);
|
--data-table-library_grid-template-columns: 156px repeat(1, minmax(80px, 1fr)) 45px minmax(45px, auto) minmax(120px, auto);
|
||||||
`,
|
`,
|
||||||
BaseRow: `
|
BaseRow: `
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -130,10 +190,7 @@ const SettingsCustomization: FC = () => {
|
|||||||
}
|
}
|
||||||
&:hover .td {
|
&:hover .td {
|
||||||
border-top: 1px solid #177ac9;
|
border-top: 1px solid #177ac9;
|
||||||
border-bottom: 1px solid #177ac9;
|
background-color: #177ac9;
|
||||||
}
|
|
||||||
&:nth-of-type(odd) .td {
|
|
||||||
background-color: #303030;
|
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
Cell: `
|
Cell: `
|
||||||
@@ -153,7 +210,12 @@ const SettingsCustomization: FC = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function hasEntityChanged(de: DeviceEntity) {
|
function hasEntityChanged(de: DeviceEntity) {
|
||||||
return (de?.cn || '') !== (de?.o_cn || '') || de.m !== de.o_m || de.ma !== de.o_ma || de.mi !== de.o_mi;
|
return (
|
||||||
|
(de?.cn || '') !== (de?.o_cn || '') ||
|
||||||
|
de.m !== de.o_m ||
|
||||||
|
de.ma !== de.o_ma ||
|
||||||
|
de.mi !== de.o_mi
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -174,14 +236,23 @@ const SettingsCustomization: FC = () => {
|
|||||||
}
|
}
|
||||||
}, [deviceEntities]);
|
}, [deviceEntities]);
|
||||||
|
|
||||||
const restart = async () => {
|
useEffect(() => {
|
||||||
await restartCommand().catch((error) => {
|
if (devices && selectedDevice !== -1) {
|
||||||
toast.error(error.message);
|
void sendDeviceEntities(selectedDevice);
|
||||||
});
|
const index = devices.devices.findIndex((d) => d.id === selectedDevice);
|
||||||
setRestarting(true);
|
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: any) {
|
function formatValue(value: unknown) {
|
||||||
if (typeof value === 'number') {
|
if (typeof value === 'number') {
|
||||||
return new Intl.NumberFormat().format(value);
|
return new Intl.NumberFormat().format(value);
|
||||||
} else if (value === undefined) {
|
} else if (value === undefined) {
|
||||||
@@ -189,20 +260,21 @@ const SettingsCustomization: FC = () => {
|
|||||||
} else if (typeof value === 'boolean') {
|
} else if (typeof value === 'boolean') {
|
||||||
return value ? 'true' : 'false';
|
return value ? 'true' : 'false';
|
||||||
}
|
}
|
||||||
return value;
|
return value as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatName(de: DeviceEntity) {
|
const formatName = (de: DeviceEntity, withShortname: boolean) =>
|
||||||
return (
|
(de.n && de.n[0] === '!'
|
||||||
<>
|
? de.t
|
||||||
{de.n && (de.n[0] === '!' ? LL.COMMAND(1) + ': ' + de.n.slice(1) : de.cn && de.cn !== '' ? de.cn : de.n) + ' '}(
|
? LL.COMMAND(1) + ': ' + de.t + ' ' + de.n.slice(1)
|
||||||
<Link target="_blank" href={APIURL + devices?.devices[selectedDevice].tn + '/' + de.id}>
|
: LL.COMMAND(1) + ': ' + de.n.slice(1)
|
||||||
{de.id}
|
: de.cn && de.cn !== ''
|
||||||
</Link>
|
? de.t
|
||||||
)
|
? de.t + ' ' + de.cn
|
||||||
</>
|
: de.cn
|
||||||
);
|
: de.t
|
||||||
}
|
? de.t + ' ' + de.n
|
||||||
|
: de.n) + (withShortname ? ' ' + de.id : '');
|
||||||
|
|
||||||
const getMaskNumber = (newMask: string[]) => {
|
const getMaskNumber = (newMask: string[]) => {
|
||||||
let new_mask = 0;
|
let new_mask = 0;
|
||||||
@@ -232,15 +304,25 @@ const SettingsCustomization: FC = () => {
|
|||||||
return new_masks;
|
return new_masks;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const filter_entity = (de: DeviceEntity) =>
|
||||||
|
(de.m & selectedFilters || !selectedFilters) &&
|
||||||
|
formatName(de, true).toLowerCase().includes(search.toLowerCase());
|
||||||
|
|
||||||
const maskDisabled = (set: boolean) => {
|
const maskDisabled = (set: boolean) => {
|
||||||
setDeviceEntities(
|
setDeviceEntities(
|
||||||
deviceEntities.map(function (de) {
|
deviceEntities.map(function (de) {
|
||||||
if ((de.m & selectedFilters || !selectedFilters) && de.id.toLowerCase().includes(search.toLowerCase())) {
|
if (filter_entity(de)) {
|
||||||
return {
|
return {
|
||||||
...de,
|
...de,
|
||||||
m: set
|
m: set
|
||||||
? de.m | (DeviceEntityMask.DV_API_MQTT_EXCLUDE | DeviceEntityMask.DV_WEB_EXCLUDE)
|
? de.m |
|
||||||
: de.m & ~(DeviceEntityMask.DV_API_MQTT_EXCLUDE | DeviceEntityMask.DV_WEB_EXCLUDE)
|
(DeviceEntityMask.DV_API_MQTT_EXCLUDE |
|
||||||
|
DeviceEntityMask.DV_WEB_EXCLUDE)
|
||||||
|
: de.m &
|
||||||
|
~(
|
||||||
|
DeviceEntityMask.DV_API_MQTT_EXCLUDE |
|
||||||
|
DeviceEntityMask.DV_WEB_EXCLUDE
|
||||||
|
)
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return de;
|
return de;
|
||||||
@@ -249,22 +331,12 @@ const SettingsCustomization: FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const changeSelectedDevice = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
if (devices) {
|
|
||||||
const selected_device = parseInt(event.target.value, 10);
|
|
||||||
setSelectedDevice(selected_device);
|
|
||||||
setNumChanges(0);
|
|
||||||
void readDeviceEntities(devices?.devices[selected_device].i);
|
|
||||||
setRestartNeeded(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetCustomization = async () => {
|
const resetCustomization = async () => {
|
||||||
try {
|
try {
|
||||||
await resetCustomizations();
|
await sendResetCustomizations();
|
||||||
toast.info(LL.CUSTOMIZATIONS_RESTART());
|
toast.info(LL.CUSTOMIZATIONS_RESTART());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(error.message);
|
toast.error((error as Error).message);
|
||||||
} finally {
|
} finally {
|
||||||
setConfirmReset(false);
|
setConfirmReset(false);
|
||||||
}
|
}
|
||||||
@@ -275,7 +347,11 @@ const SettingsCustomization: FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateDeviceEntity = (updatedItem: DeviceEntity) => {
|
const updateDeviceEntity = (updatedItem: DeviceEntity) => {
|
||||||
setDeviceEntities(deviceEntities?.map((de) => (de.id === updatedItem.id ? { ...de, ...updatedItem } : de)));
|
setDeviceEntities(
|
||||||
|
deviceEntities?.map((de) =>
|
||||||
|
de.id === updatedItem.id ? { ...de, ...updatedItem } : de
|
||||||
|
)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDialogSave = (updatedItem: DeviceEntity) => {
|
const onDialogSave = (updatedItem: DeviceEntity) => {
|
||||||
@@ -317,67 +393,135 @@ const SettingsCustomization: FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await writeCustomEntities({ id: devices?.devices[selectedDevice].i, entity_ids: masked_entities }).catch(
|
await sendCustomizationEntities({
|
||||||
(error) => {
|
id: selectedDevice,
|
||||||
if (error.message === 'Reboot required') {
|
entity_ids: masked_entities
|
||||||
setRestartNeeded(true);
|
}).catch((error: Error) => {
|
||||||
} else {
|
if (error.message === 'Reboot required') {
|
||||||
toast.error(error.message);
|
setRestartNeeded(true);
|
||||||
}
|
} else {
|
||||||
|
toast.error(error.message);
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
setOriginalSettings(deviceEntities);
|
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 = () => (
|
const renderDeviceList = () => (
|
||||||
<>
|
<>
|
||||||
<Box mb={2} color="warning.main">
|
<Box mb={1} color="warning.main">
|
||||||
<Typography variant="body2">{LL.CUSTOMIZATIONS_HELP_1()}.</Typography>
|
<Typography variant="body1">{LL.CUSTOMIZATIONS_HELP_1()}.</Typography>
|
||||||
<Typography variant="body2" mt={1}>
|
</Box>
|
||||||
<OptionIcon type="favorite" isSet={true} />={LL.CUSTOMIZATIONS_HELP_2()}
|
<Box display="flex" flexWrap="wrap" alignItems="center" gap={2}>
|
||||||
<OptionIcon type="readonly" isSet={true} />={LL.CUSTOMIZATIONS_HELP_3()}
|
{rename ? (
|
||||||
<OptionIcon type="api_mqtt_exclude" isSet={true} />={LL.CUSTOMIZATIONS_HELP_4()}
|
<TextField
|
||||||
<OptionIcon type="web_exclude" isSet={true} />={LL.CUSTOMIZATIONS_HELP_5()}
|
name="device"
|
||||||
<OptionIcon type="deleted" isSet={true} />={LL.CUSTOMIZATIONS_HELP_6()}
|
label={LL.EMS_DEVICE()}
|
||||||
</Typography>
|
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>
|
</Box>
|
||||||
<TextField
|
|
||||||
name="device"
|
|
||||||
label={LL.EMS_DEVICE()}
|
|
||||||
variant="outlined"
|
|
||||||
fullWidth
|
|
||||||
value={selectedDevice}
|
|
||||||
disabled={numChanges !== 0}
|
|
||||||
onChange={changeSelectedDevice}
|
|
||||||
margin="normal"
|
|
||||||
select
|
|
||||||
>
|
|
||||||
<MenuItem disabled key={0} value={-1}>
|
|
||||||
{LL.SELECT_DEVICE()}...
|
|
||||||
</MenuItem>
|
|
||||||
{devices.devices.map((device: DeviceShort, index) => (
|
|
||||||
<MenuItem key={index} value={index}>
|
|
||||||
{device.s}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</TextField>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderDeviceData = () => {
|
const renderDeviceData = () => {
|
||||||
if (deviceEntities.length === 0) {
|
const shown_data = deviceEntities.filter((de) => filter_entity(de));
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const shown_data = deviceEntities.filter(
|
|
||||||
(de) => (de.m & selectedFilters || !selectedFilters) && de.id.toLowerCase().includes(search.toLowerCase())
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Grid container mb={1} mt={0} spacing={1} direction="row" justifyContent="flex-start" alignItems="center">
|
<Box color="warning.main">
|
||||||
<Grid item xs={2}>
|
<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
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@@ -385,21 +529,23 @@ const SettingsCustomization: FC = () => {
|
|||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setSearch(event.target.value);
|
setSearch(event.target.value);
|
||||||
}}
|
}}
|
||||||
InputProps={{
|
slotProps={{
|
||||||
startAdornment: (
|
input: {
|
||||||
<InputAdornment position="start">
|
startAdornment: (
|
||||||
<SearchIcon color="primary" sx={{ fontSize: 16 }} />
|
<InputAdornment position="start">
|
||||||
</InputAdornment>
|
<SearchIcon color="primary" sx={{ fontSize: 16 }} />
|
||||||
)
|
</InputAdornment>
|
||||||
|
)
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid>
|
||||||
<ToggleButtonGroup
|
<ToggleButtonGroup
|
||||||
size="small"
|
size="small"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
value={getMaskString(selectedFilters)}
|
value={getMaskString(selectedFilters)}
|
||||||
onChange={(event, mask) => {
|
onChange={(event, mask: string[]) => {
|
||||||
setSelectedFilters(getMaskNumber(mask));
|
setSelectedFilters(getMaskNumber(mask));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -420,7 +566,7 @@ const SettingsCustomization: FC = () => {
|
|||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
</ToggleButtonGroup>
|
</ToggleButtonGroup>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
sx={{ fontSize: 10 }}
|
sx={{ fontSize: 10 }}
|
||||||
@@ -433,7 +579,7 @@ const SettingsCustomization: FC = () => {
|
|||||||
<OptionIcon type="web_exclude" isSet={false} />
|
<OptionIcon type="web_exclude" isSet={false} />
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
sx={{ fontSize: 10 }}
|
sx={{ fontSize: 10 }}
|
||||||
@@ -446,14 +592,19 @@ const SettingsCustomization: FC = () => {
|
|||||||
<OptionIcon type="web_exclude" isSet={true} />
|
<OptionIcon type="web_exclude" isSet={true} />
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid>
|
||||||
<Typography variant="subtitle2" color="primary">
|
<Typography variant="subtitle2" color="grey">
|
||||||
{LL.SHOWING()} {shown_data.length}/{deviceEntities.length}
|
{LL.SHOWING()} {shown_data.length}/{deviceEntities.length}
|
||||||
|
{LL.ENTITIES(deviceEntities.length)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Table data={{ nodes: shown_data }} theme={entities_theme} layout={{ custom: true }}>
|
<Table
|
||||||
{(tableList: any) => (
|
data={{ nodes: shown_data }}
|
||||||
|
theme={entities_theme}
|
||||||
|
layout={{ custom: true }}
|
||||||
|
>
|
||||||
|
{(tableList: DeviceEntity[]) => (
|
||||||
<>
|
<>
|
||||||
<Header>
|
<Header>
|
||||||
<HeaderRow>
|
<HeaderRow>
|
||||||
@@ -470,9 +621,22 @@ const SettingsCustomization: FC = () => {
|
|||||||
<Cell stiff>
|
<Cell stiff>
|
||||||
<EntityMaskToggle onUpdate={updateDeviceEntity} de={de} />
|
<EntityMaskToggle onUpdate={updateDeviceEntity} de={de} />
|
||||||
</Cell>
|
</Cell>
|
||||||
<Cell>{formatName(de)}</Cell>
|
<Cell>
|
||||||
<Cell>{!(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.mi)}</Cell>
|
{formatName(de, false)} (
|
||||||
<Cell>{!(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.ma)}</Cell>
|
<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>
|
<Cell>{formatValue(de.v)}</Cell>
|
||||||
</Row>
|
</Row>
|
||||||
))}
|
))}
|
||||||
@@ -485,14 +649,28 @@ const SettingsCustomization: FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderResetDialog = () => (
|
const renderResetDialog = () => (
|
||||||
<Dialog sx={dialogStyle} open={confirmReset} onClose={() => setConfirmReset(false)}>
|
<Dialog
|
||||||
|
sx={dialogStyle}
|
||||||
|
open={confirmReset}
|
||||||
|
onClose={() => setConfirmReset(false)}
|
||||||
|
>
|
||||||
<DialogTitle>{LL.RESET(1)}</DialogTitle>
|
<DialogTitle>{LL.RESET(1)}</DialogTitle>
|
||||||
<DialogContent dividers>{LL.CUSTOMIZATIONS_RESET()}</DialogContent>
|
<DialogContent dividers>{LL.CUSTOMIZATIONS_RESET()}</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button startIcon={<CancelIcon />} variant="outlined" onClick={() => setConfirmReset(false)} color="secondary">
|
<Button
|
||||||
|
startIcon={<CancelIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => setConfirmReset(false)}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
{LL.CANCEL()}
|
{LL.CANCEL()}
|
||||||
</Button>
|
</Button>
|
||||||
<Button startIcon={<SettingsBackupRestoreIcon />} variant="outlined" onClick={resetCustomization} color="error">
|
<Button
|
||||||
|
startIcon={<SettingsBackupRestoreIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={resetCustomization}
|
||||||
|
color="error"
|
||||||
|
>
|
||||||
{LL.RESET(0)}
|
{LL.RESET(0)}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
@@ -501,19 +679,20 @@ const SettingsCustomization: FC = () => {
|
|||||||
|
|
||||||
const renderContent = () => (
|
const renderContent = () => (
|
||||||
<>
|
<>
|
||||||
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
|
|
||||||
{LL.DEVICE_ENTITIES()}
|
|
||||||
</Typography>
|
|
||||||
{devices && renderDeviceList()}
|
{devices && renderDeviceList()}
|
||||||
{renderDeviceData()}
|
{selectedDevice !== -1 && !rename && renderDeviceData()}
|
||||||
{restartNeeded && (
|
{restartNeeded ? (
|
||||||
<MessageBox my={2} level="warning" message={LL.RESTART_TEXT()}>
|
<MessageBox my={2} level="warning" message={LL.RESTART_TEXT(0)}>
|
||||||
<Button startIcon={<PowerSettingsNewIcon />} variant="contained" color="error" onClick={restart}>
|
<Button
|
||||||
|
startIcon={<PowerSettingsNewIcon />}
|
||||||
|
variant="contained"
|
||||||
|
color="error"
|
||||||
|
onClick={doRestart}
|
||||||
|
>
|
||||||
{LL.RESTART()}
|
{LL.RESTART()}
|
||||||
</Button>
|
</Button>
|
||||||
</MessageBox>
|
</MessageBox>
|
||||||
)}
|
) : (
|
||||||
{!restartNeeded && (
|
|
||||||
<Box display="flex" flexWrap="wrap">
|
<Box display="flex" flexWrap="wrap">
|
||||||
<Box flexGrow={1}>
|
<Box flexGrow={1}>
|
||||||
{numChanges !== 0 && (
|
{numChanges !== 0 && (
|
||||||
@@ -522,7 +701,7 @@ const SettingsCustomization: FC = () => {
|
|||||||
startIcon={<CancelIcon />}
|
startIcon={<CancelIcon />}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
onClick={() => devices && readDeviceEntities(devices.devices[selectedDevice].i)}
|
onClick={() => devices && sendDeviceEntities(selectedDevice)}
|
||||||
>
|
>
|
||||||
{LL.CANCEL()}
|
{LL.CANCEL()}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -537,16 +716,18 @@ const SettingsCustomization: FC = () => {
|
|||||||
</ButtonRow>
|
</ButtonRow>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<ButtonRow>
|
{!rename && (
|
||||||
<Button
|
<ButtonRow mt={1}>
|
||||||
startIcon={<SettingsBackupRestoreIcon />}
|
<Button
|
||||||
variant="outlined"
|
startIcon={<SettingsBackupRestoreIcon />}
|
||||||
color="error"
|
variant="outlined"
|
||||||
onClick={() => setConfirmReset(true)}
|
color="error"
|
||||||
>
|
onClick={() => setConfirmReset(true)}
|
||||||
{LL.RESET(0)}
|
>
|
||||||
</Button>
|
{LL.RESET(0)}
|
||||||
</ButtonRow>
|
</Button>
|
||||||
|
</ButtonRow>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
{renderResetDialog()}
|
{renderResetDialog()}
|
||||||
@@ -554,11 +735,11 @@ const SettingsCustomization: FC = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContent title={LL.CUSTOMIZATIONS()} titleGutter>
|
<SectionContent>
|
||||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||||
{restarting ? <RestartMonitor /> : renderContent()}
|
{restarting ? <SystemMonitor /> : renderContent()}
|
||||||
{selectedDeviceEntity && (
|
{selectedDeviceEntity && (
|
||||||
<SettingsCustomizationDialog
|
<SettingsCustomizationsDialog
|
||||||
open={dialogOpen}
|
open={dialogOpen}
|
||||||
onClose={onDialogClose}
|
onClose={onDialogClose}
|
||||||
onSave={onDialogSave}
|
onSave={onDialogSave}
|
||||||
@@ -569,4 +750,4 @@ const SettingsCustomization: FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SettingsCustomization;
|
export default Customizations;
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
import { useEffect, useState } from 'react';
|
||||||
import DoneIcon from '@mui/icons-material/Done';
|
|
||||||
|
|
||||||
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
|
import DoneIcon from '@mui/icons-material/Done';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@@ -12,25 +14,28 @@ import {
|
|||||||
TextField,
|
TextField,
|
||||||
Typography
|
Typography
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
|
import { dialogStyle } from 'CustomTheme';
|
||||||
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
import { numberValue, updateValue } from 'utils';
|
||||||
|
|
||||||
import EntityMaskToggle from './EntityMaskToggle';
|
import EntityMaskToggle from './EntityMaskToggle';
|
||||||
import { DeviceEntityMask } from './types';
|
import { DeviceEntityMask } from './types';
|
||||||
import type { DeviceEntity } from './types';
|
import type { DeviceEntity } from './types';
|
||||||
|
|
||||||
import { dialogStyle } from 'CustomTheme';
|
interface SettingsCustomizationsDialogProps {
|
||||||
import { useI18nContext } from 'i18n/i18n-react';
|
|
||||||
|
|
||||||
import { updateValue } from 'utils';
|
|
||||||
|
|
||||||
type SettingsCustomizationDialogProps = {
|
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSave: (di: DeviceEntity) => void;
|
onSave: (di: DeviceEntity) => void;
|
||||||
selectedItem: DeviceEntity;
|
selectedItem: DeviceEntity;
|
||||||
};
|
}
|
||||||
|
|
||||||
const SettingsCustomizationDialog = ({ open, onClose, onSave, selectedItem }: SettingsCustomizationDialogProps) => {
|
const CustomizationsDialog = ({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
onSave,
|
||||||
|
selectedItem
|
||||||
|
}: SettingsCustomizationsDialogProps) => {
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
const [editItem, setEditItem] = useState<DeviceEntity>(selectedItem);
|
const [editItem, setEditItem] = useState<DeviceEntity>(selectedItem);
|
||||||
const [error, setError] = useState<boolean>(false);
|
const [error, setError] = useState<boolean>(false);
|
||||||
@@ -38,7 +43,9 @@ const SettingsCustomizationDialog = ({ open, onClose, onSave, selectedItem }: Se
|
|||||||
const updateFormValue = updateValue(setEditItem);
|
const updateFormValue = updateValue(setEditItem);
|
||||||
|
|
||||||
const isWriteableNumber =
|
const isWriteableNumber =
|
||||||
typeof editItem.v === 'number' && editItem.w && !(editItem.m & DeviceEntityMask.DV_READONLY);
|
typeof editItem.v === 'number' &&
|
||||||
|
editItem.w &&
|
||||||
|
!(editItem.m & DeviceEntityMask.DV_READONLY);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
@@ -47,12 +54,19 @@ const SettingsCustomizationDialog = ({ open, onClose, onSave, selectedItem }: Se
|
|||||||
}
|
}
|
||||||
}, [open, selectedItem]);
|
}, [open, selectedItem]);
|
||||||
|
|
||||||
const close = () => {
|
const handleClose = (_event, reason: 'backdropClick' | 'escapeKeyDown') => {
|
||||||
onClose();
|
if (reason !== 'backdropClick') {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const save = () => {
|
const save = () => {
|
||||||
if (isWriteableNumber && editItem.mi && editItem.ma && editItem.mi > editItem?.ma) {
|
if (
|
||||||
|
isWriteableNumber &&
|
||||||
|
editItem.mi &&
|
||||||
|
editItem.ma &&
|
||||||
|
editItem.mi > editItem?.ma
|
||||||
|
) {
|
||||||
setError(true);
|
setError(true);
|
||||||
} else {
|
} else {
|
||||||
onSave(editItem);
|
onSave(editItem);
|
||||||
@@ -64,49 +78,67 @@ const SettingsCustomizationDialog = ({ open, onClose, onSave, selectedItem }: Se
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog sx={dialogStyle} open={open} onClose={close}>
|
<Dialog sx={dialogStyle} open={open} onClose={handleClose}>
|
||||||
<DialogTitle>{LL.EDIT() + ' ' + LL.ENTITY()}</DialogTitle>
|
<DialogTitle>{LL.EDIT() + ' ' + LL.ENTITY()}</DialogTitle>
|
||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
<Box color="warning.main">
|
<Grid container>
|
||||||
<Typography variant="body2">{editItem.id}</Typography>
|
<Typography variant="body2" color="warning.main">
|
||||||
</Box>
|
{LL.ID_OF(LL.ENTITY())}:
|
||||||
<Box color="warning.main" mt={1} mb={2}>
|
|
||||||
<Typography variant="body2">
|
|
||||||
{LL.DEFAULT(1) + ' ' + LL.ENTITY_NAME(1)}: {editItem.n}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
<Typography variant="body2">{editItem.id}</Typography>
|
||||||
<Box mb={3}>
|
</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} />
|
<EntityMaskToggle onUpdate={updateDeviceEntity} de={editItem} />
|
||||||
</Box>
|
</Box>
|
||||||
<Grid container spacing={1}>
|
<Grid container spacing={2}>
|
||||||
<Grid item>
|
<Grid>
|
||||||
<TextField
|
<TextField
|
||||||
name="cn"
|
name="cn"
|
||||||
label={LL.NEW_NAME_OF(LL.ENTITY())}
|
label={LL.NEW_NAME_OF(LL.ENTITY())}
|
||||||
value={editItem.cn}
|
value={editItem.cn}
|
||||||
autoFocus
|
|
||||||
sx={{ width: '30ch' }}
|
sx={{ width: '30ch' }}
|
||||||
onChange={updateFormValue}
|
onChange={updateFormValue}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
{isWriteableNumber && (
|
{isWriteableNumber && (
|
||||||
<>
|
<>
|
||||||
<Grid item>
|
<Grid>
|
||||||
<TextField
|
<TextField
|
||||||
name="mi"
|
name="mi"
|
||||||
label={LL.MIN()}
|
label={LL.MIN()}
|
||||||
value={editItem.mi}
|
value={numberValue(editItem.mi)}
|
||||||
sx={{ width: '8ch' }}
|
sx={{ width: '11ch' }}
|
||||||
type="number"
|
type="number"
|
||||||
onChange={updateFormValue}
|
onChange={updateFormValue}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid>
|
||||||
<TextField
|
<TextField
|
||||||
name="ma"
|
name="ma"
|
||||||
label={LL.MAX()}
|
label={LL.MAX()}
|
||||||
value={editItem.ma}
|
value={numberValue(editItem.ma)}
|
||||||
sx={{ width: '8ch' }}
|
sx={{ width: '11ch' }}
|
||||||
type="number"
|
type="number"
|
||||||
onChange={updateFormValue}
|
onChange={updateFormValue}
|
||||||
/>
|
/>
|
||||||
@@ -121,10 +153,20 @@ const SettingsCustomizationDialog = ({ open, onClose, onSave, selectedItem }: Se
|
|||||||
)}
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button startIcon={<CancelIcon />} variant="outlined" onClick={close} color="secondary">
|
<Button
|
||||||
|
startIcon={<CancelIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={onClose}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
{LL.CANCEL()}
|
{LL.CANCEL()}
|
||||||
</Button>
|
</Button>
|
||||||
<Button startIcon={<DoneIcon />} variant="outlined" onClick={save} color="primary">
|
<Button
|
||||||
|
startIcon={<DoneIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={save}
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
{LL.UPDATE()}
|
{LL.UPDATE()}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
@@ -132,4 +174,4 @@ const SettingsCustomizationDialog = ({ open, onClose, onSave, selectedItem }: Se
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SettingsCustomizationDialog;
|
export default CustomizationsDialog;
|
||||||
380
interface/src/app/main/Dashboard.tsx
Normal file
380
interface/src/app/main/Dashboard.tsx
Normal file
@@ -0,0 +1,380 @@
|
|||||||
|
import { useContext, useEffect, useState } from 'react';
|
||||||
|
import { IconContext } from 'react-icons/lib';
|
||||||
|
import { Link } from 'react-router';
|
||||||
|
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 HelpOutlineIcon from '@mui/icons-material/HelpOutline';
|
||||||
|
import UnfoldLessIcon from '@mui/icons-material/UnfoldLess';
|
||||||
|
import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
IconButton,
|
||||||
|
ToggleButton,
|
||||||
|
ToggleButtonGroup,
|
||||||
|
Typography
|
||||||
|
} from '@mui/material';
|
||||||
|
|
||||||
|
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 {
|
||||||
|
ButtonTooltip,
|
||||||
|
FormLoader,
|
||||||
|
MessageBox,
|
||||||
|
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
|
||||||
|
} = useRequest(readDashboard, {
|
||||||
|
initialData: { connected: true, nodes: [] }
|
||||||
|
}).onSuccess((event) => {
|
||||||
|
if (event.data.nodes.length !== parentNodes) {
|
||||||
|
setParentNodes(event.data.nodes.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.nodes },
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
showAll
|
||||||
|
? tree.fns.onAddAll(data.nodes.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={{ fontWeight: 'bold', fontSize: '14px' }}>
|
||||||
|
<DeviceIcon type_id={di.t ?? 0} />
|
||||||
|
{showType(di.n, di.t)}
|
||||||
|
<span style={{ color: 'lightblue' }}> ({di.nodes?.length})</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (di.dv) {
|
||||||
|
return <span>{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} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasFavEntities = data.nodes.filter(
|
||||||
|
(item: DashboardItem) => item.id <= 90
|
||||||
|
).length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!data.connected && (
|
||||||
|
<MessageBox mb={2} level="error" message={LL.EMS_BUS_WARNING()} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{data.connected && data.nodes.length > 0 && !hasFavEntities && (
|
||||||
|
<MessageBox mb={2} level="warning">
|
||||||
|
<Typography>
|
||||||
|
{LL.NO_DATA_1()}
|
||||||
|
<Link to="/customizations" style={{ color: 'white' }}>
|
||||||
|
{LL.CUSTOMIZATIONS()}
|
||||||
|
</Link>
|
||||||
|
{LL.NO_DATA_2()}
|
||||||
|
{LL.NO_DATA_3()}
|
||||||
|
<Link to="/devices" style={{ color: 'white' }}>
|
||||||
|
{LL.DEVICES()}
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</Typography>
|
||||||
|
</MessageBox>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{data.nodes.length > 0 && (
|
||||||
|
<>
|
||||||
|
<ToggleButtonGroup
|
||||||
|
color="primary"
|
||||||
|
size="small"
|
||||||
|
value={showAll}
|
||||||
|
exclusive
|
||||||
|
onChange={handleShowAll}
|
||||||
|
>
|
||||||
|
<ButtonTooltip title={LL.ALLVALUES()} arrow>
|
||||||
|
<ToggleButton value={true}>
|
||||||
|
<UnfoldMoreIcon sx={{ fontSize: 18 }} />
|
||||||
|
</ToggleButton>
|
||||||
|
</ButtonTooltip>
|
||||||
|
<ButtonTooltip title={LL.COMPACT()} arrow>
|
||||||
|
<ToggleButton value={false}>
|
||||||
|
<UnfoldLessIcon sx={{ fontSize: 18 }} />
|
||||||
|
</ToggleButton>
|
||||||
|
</ButtonTooltip>
|
||||||
|
</ToggleButtonGroup>
|
||||||
|
<ButtonTooltip title={LL.DASHBOARD_1()} arrow>
|
||||||
|
<HelpOutlineIcon color="primary" sx={{ ml: 1, fontSize: 20 }} />
|
||||||
|
</ButtonTooltip>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
padding={1}
|
||||||
|
justifyContent="center"
|
||||||
|
flexDirection="column"
|
||||||
|
sx={{
|
||||||
|
borderRadius: 1,
|
||||||
|
border: '1px solid grey'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconContext.Provider
|
||||||
|
value={{
|
||||||
|
color: 'lightblue',
|
||||||
|
size: '18',
|
||||||
|
style: { verticalAlign: 'middle' }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Table
|
||||||
|
data={{ nodes: data.nodes }}
|
||||||
|
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>
|
||||||
|
<ButtonTooltip
|
||||||
|
title={formatValue(LL, di.dv?.v, di.dv?.u)}
|
||||||
|
>
|
||||||
|
<span>{formatValue(LL, di.dv?.v, di.dv?.u)}</span>
|
||||||
|
</ButtonTooltip>
|
||||||
|
</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;
|
||||||
797
interface/src/app/main/Devices.tsx
Normal file
797
interface/src/app/main/Devices.tsx
Normal file
@@ -0,0 +1,797 @@
|
|||||||
|
import {
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useLayoutEffect,
|
||||||
|
useState
|
||||||
|
} from 'react';
|
||||||
|
import { IconContext } from 'react-icons';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
import CommentsDisabledOutlinedIcon from '@mui/icons-material/CommentsDisabledOutlined';
|
||||||
|
import ConstructionIcon from '@mui/icons-material/Construction';
|
||||||
|
import EditIcon from '@mui/icons-material/Edit';
|
||||||
|
import EditOffOutlinedIcon from '@mui/icons-material/EditOffOutlined';
|
||||||
|
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 SearchIcon from '@mui/icons-material/Search';
|
||||||
|
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,
|
||||||
|
Grid,
|
||||||
|
IconButton,
|
||||||
|
InputAdornment,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemText,
|
||||||
|
TextField,
|
||||||
|
ToggleButton,
|
||||||
|
Typography
|
||||||
|
} from '@mui/material';
|
||||||
|
|
||||||
|
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 {
|
||||||
|
ButtonTooltip,
|
||||||
|
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 [search, setSearch] = useState('');
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
});
|
||||||
|
|
||||||
|
const device_theme = useTheme([
|
||||||
|
common_theme,
|
||||||
|
{
|
||||||
|
Table: `
|
||||||
|
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) 130px;
|
||||||
|
`,
|
||||||
|
HeaderRow: `
|
||||||
|
.th {
|
||||||
|
padding: 8px;
|
||||||
|
`,
|
||||||
|
Row: `
|
||||||
|
font-weight: bold;
|
||||||
|
&: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 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();
|
||||||
|
setSearch('');
|
||||||
|
};
|
||||||
|
|
||||||
|
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) {
|
||||||
|
void navigate('/customentities');
|
||||||
|
} else {
|
||||||
|
void 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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
if (selectedDevice !== undefined) {
|
||||||
|
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) &&
|
||||||
|
dv.id.slice(2).toLowerCase().includes(search.toLowerCase())
|
||||||
|
)
|
||||||
|
: deviceData.nodes.filter((dv) =>
|
||||||
|
dv.id.slice(2).toLowerCase().includes(search.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
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={{ p: 1 }}>
|
||||||
|
<Grid container justifyContent="space-between">
|
||||||
|
<Typography noWrap variant="subtitle1" color="warning.main">
|
||||||
|
{coreData.devices[deviceIndex].n} (
|
||||||
|
{coreData.devices[deviceIndex].tn})
|
||||||
|
</Typography>
|
||||||
|
<Grid justifyContent="flex-end">
|
||||||
|
<ButtonTooltip title={LL.CLOSE()}>
|
||||||
|
<IconButton onClick={resetDeviceSelect}>
|
||||||
|
<HighlightOffIcon color="primary" sx={{ fontSize: 18 }} />
|
||||||
|
</IconButton>
|
||||||
|
</ButtonTooltip>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
sx={{ width: '22ch' }}
|
||||||
|
placeholder={LL.SEARCH()}
|
||||||
|
onChange={(event) => {
|
||||||
|
setSearch(event.target.value);
|
||||||
|
}}
|
||||||
|
slotProps={{
|
||||||
|
input: {
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">
|
||||||
|
<SearchIcon color="primary" sx={{ fontSize: 16 }} />
|
||||||
|
</InputAdornment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ButtonTooltip title={LL.DEVICE_DETAILS()}>
|
||||||
|
<IconButton onClick={() => setShowDeviceInfo(true)}>
|
||||||
|
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18 }} />
|
||||||
|
</IconButton>
|
||||||
|
</ButtonTooltip>
|
||||||
|
{me.admin && (
|
||||||
|
<ButtonTooltip title={LL.CUSTOMIZATIONS()}>
|
||||||
|
<IconButton onClick={customize}>
|
||||||
|
<ConstructionIcon 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()}>
|
||||||
|
<ToggleButton
|
||||||
|
value="1"
|
||||||
|
size="small"
|
||||||
|
selected={onlyFav}
|
||||||
|
onChange={() => {
|
||||||
|
setOnlyFav(!onlyFav);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{onlyFav ? (
|
||||||
|
<StarIcon color="primary" sx={{ fontSize: 18 }} />
|
||||||
|
) : (
|
||||||
|
<StarBorderOutlinedIcon color="primary" sx={{ fontSize: 18 }} />
|
||||||
|
)}{' '}
|
||||||
|
</ToggleButton>
|
||||||
|
</ButtonTooltip>
|
||||||
|
|
||||||
|
<span style={{ color: 'grey', fontSize: '12px' }}>
|
||||||
|
|
||||||
|
{LL.SHOWING() +
|
||||||
|
' ' +
|
||||||
|
shown_data.length +
|
||||||
|
'/' +
|
||||||
|
coreData.devices[deviceIndex].e +
|
||||||
|
' ' +
|
||||||
|
LL.ENTITIES(shown_data.length)}
|
||||||
|
</span>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Table
|
||||||
|
data={{ nodes: shown_data }}
|
||||||
|
theme={data_theme}
|
||||||
|
sort={dv_sort}
|
||||||
|
layout={{ custom: true, fixedHeader: true }}
|
||||||
|
>
|
||||||
|
{(tableList: DeviceValue[]) => (
|
||||||
|
<>
|
||||||
|
<Header>
|
||||||
|
<HeaderRow>
|
||||||
|
<HeaderCell resize>
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
style={{ fontSize: '14px', justifyContent: 'flex-start' }}
|
||||||
|
endIcon={getSortIcon(dv_sort.state, 'NAME')}
|
||||||
|
onClick={() => dv_sort.fns.onToggleSort({ sortKey: 'NAME' })}
|
||||||
|
>
|
||||||
|
{LL.ENTITY_NAME(0)}
|
||||||
|
</Button>
|
||||||
|
</HeaderCell>
|
||||||
|
<HeaderCell resize>
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
style={{ fontSize: '14px', justifyContent: 'flex-end' }}
|
||||||
|
endIcon={getSortIcon(dv_sort.state, 'VALUE')}
|
||||||
|
onClick={() => dv_sort.fns.onToggleSort({ sortKey: 'VALUE' })}
|
||||||
|
>
|
||||||
|
{LL.VALUE(0)}
|
||||||
|
</Button>
|
||||||
|
</HeaderCell>
|
||||||
|
<HeaderCell stiff />
|
||||||
|
</HeaderRow>
|
||||||
|
</Header>
|
||||||
|
<Body>
|
||||||
|
{tableList.map((dv: DeviceValue) => (
|
||||||
|
<Row key={dv.id} item={dv} onClick={() => showDeviceValue(dv)}>
|
||||||
|
<Cell>{renderNameCell(dv)}</Cell>
|
||||||
|
<Cell>{formatValue(LL, dv.v, dv.u)}</Cell>
|
||||||
|
<Cell stiff>
|
||||||
|
{me.admin &&
|
||||||
|
dv.c &&
|
||||||
|
!hasMask(dv.id, DeviceEntityMask.DV_READONLY) && (
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => showDeviceValue(dv)}
|
||||||
|
>
|
||||||
|
{dv.v === '' ? (
|
||||||
|
<PlayArrowIcon color="primary" sx={{ fontSize: 16 }} />
|
||||||
|
) : (
|
||||||
|
<EditIcon color="primary" sx={{ fontSize: 16 }} />
|
||||||
|
)}
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</Cell>
|
||||||
|
</Row>
|
||||||
|
))}
|
||||||
|
</Body>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionContent id="devices-window">
|
||||||
|
{renderCoreData()}
|
||||||
|
{renderDeviceData()}
|
||||||
|
{renderDeviceDetails()}
|
||||||
|
{selectedDeviceValue && (
|
||||||
|
<DevicesDialog
|
||||||
|
open={deviceValueDialogOpen}
|
||||||
|
onClose={deviceValueDialogClose}
|
||||||
|
onSave={deviceValueDialogSave}
|
||||||
|
selectedItem={selectedDeviceValue}
|
||||||
|
writeable={
|
||||||
|
selectedDeviceValue.c !== undefined &&
|
||||||
|
!hasMask(selectedDeviceValue.id, DeviceEntityMask.DV_READONLY)
|
||||||
|
}
|
||||||
|
validator={deviceValueItemValidation(selectedDeviceValue)}
|
||||||
|
progress={submitting}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</SectionContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Devices;
|
||||||
@@ -1,46 +1,35 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
import WarningIcon from '@mui/icons-material/Warning';
|
import WarningIcon from '@mui/icons-material/Warning';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
Box,
|
||||||
Button,
|
Button,
|
||||||
|
CircularProgress,
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogTitle,
|
|
||||||
DialogContent,
|
|
||||||
DialogActions,
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
FormHelperText,
|
||||||
|
Grid,
|
||||||
InputAdornment,
|
InputAdornment,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
TextField,
|
TextField,
|
||||||
FormHelperText,
|
Typography
|
||||||
Grid,
|
|
||||||
Box,
|
|
||||||
Typography,
|
|
||||||
CircularProgress
|
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
|
import { dialogStyle } from 'CustomTheme';
|
||||||
|
import type Schema from 'async-validator';
|
||||||
|
import type { ValidateFieldsError } from 'async-validator';
|
||||||
|
import { 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 { DeviceValueUOM, DeviceValueUOM_s } from './types';
|
||||||
import type { DeviceValue } from './types';
|
import type { DeviceValue } from './types';
|
||||||
import type Schema from 'async-validator';
|
|
||||||
|
|
||||||
import type { ValidateFieldsError } from 'async-validator';
|
interface DevicesDialogProps {
|
||||||
import { dialogStyle } from 'CustomTheme';
|
|
||||||
import { ValidatedTextField } from 'components';
|
|
||||||
import { useI18nContext } from 'i18n/i18n-react';
|
|
||||||
import { updateValue } from 'utils';
|
|
||||||
|
|
||||||
import { validate } from 'validators';
|
|
||||||
|
|
||||||
// const dialogStyle = {
|
|
||||||
// '& .MuiDialog-paper': {
|
|
||||||
// borderRadius: '8px',
|
|
||||||
// borderColor: '#565656',
|
|
||||||
// borderStyle: 'solid',
|
|
||||||
// borderWidth: '1px'
|
|
||||||
// },
|
|
||||||
// backdropFilter: 'blur(1px)'
|
|
||||||
// };
|
|
||||||
|
|
||||||
type DashboardDevicesDialogProps = {
|
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSave: (as: DeviceValue) => void;
|
onSave: (as: DeviceValue) => void;
|
||||||
@@ -48,9 +37,9 @@ type DashboardDevicesDialogProps = {
|
|||||||
writeable: boolean;
|
writeable: boolean;
|
||||||
validator: Schema;
|
validator: Schema;
|
||||||
progress: boolean;
|
progress: boolean;
|
||||||
};
|
}
|
||||||
|
|
||||||
const DashboardDevicesDialog = ({
|
const DevicesDialog = ({
|
||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
onSave,
|
onSave,
|
||||||
@@ -58,7 +47,7 @@ const DashboardDevicesDialog = ({
|
|||||||
writeable,
|
writeable,
|
||||||
validator,
|
validator,
|
||||||
progress
|
progress
|
||||||
}: DashboardDevicesDialogProps) => {
|
}: DevicesDialogProps) => {
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
const [editItem, setEditItem] = useState<DeviceValue>(selectedItem);
|
const [editItem, setEditItem] = useState<DeviceValue>(selectedItem);
|
||||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
@@ -81,12 +70,15 @@ const DashboardDevicesDialog = ({
|
|||||||
setFieldErrors(undefined);
|
setFieldErrors(undefined);
|
||||||
await validate(validator, editItem);
|
await validate(validator, editItem);
|
||||||
onSave(editItem);
|
onSave(editItem);
|
||||||
} catch (errors: any) {
|
} catch (error) {
|
||||||
setFieldErrors(errors);
|
setFieldErrors(error as ValidateFieldsError);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const setUom = (uom: number) => {
|
const setUom = (uom?: DeviceValueUOM) => {
|
||||||
|
if (uom === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
switch (uom) {
|
switch (uom) {
|
||||||
case DeviceValueUOM.HOURS:
|
case DeviceValueUOM.HOURS:
|
||||||
return LL.HOURS();
|
return LL.HOURS();
|
||||||
@@ -99,46 +91,38 @@ const DashboardDevicesDialog = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const showHelperText = (dv: DeviceValue) => {
|
const showHelperText = (dv: DeviceValue) =>
|
||||||
if (dv.h) {
|
dv.h ? (
|
||||||
return dv.h;
|
dv.h
|
||||||
}
|
) : dv.l ? (
|
||||||
if (dv.l) {
|
dv.l.join(' | ')
|
||||||
return '[ ' + dv.l.join(' | ') + ' ]';
|
) : dv.m !== undefined && dv.x !== undefined ? (
|
||||||
}
|
<>
|
||||||
|
{dv.m} → {dv.x}
|
||||||
let helperText = '<';
|
</>
|
||||||
if (dv.s) {
|
) : undefined;
|
||||||
helperText += 'n';
|
|
||||||
if (dv.m !== undefined && dv.x !== undefined) {
|
|
||||||
helperText += ' between ' + dv.m + ' and ' + dv.x;
|
|
||||||
} else {
|
|
||||||
helperText += ' , step ' + dv.s;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
helperText += 'text';
|
|
||||||
}
|
|
||||||
return helperText + '>';
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog sx={dialogStyle} open={open} onClose={close}>
|
<Dialog sx={dialogStyle} open={open} onClose={close}>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{selectedItem.v === '' && selectedItem.c ? LL.RUN_COMMAND() : writeable ? LL.CHANGE_VALUE() : LL.VALUE(0)}
|
{selectedItem.v === '' && selectedItem.c
|
||||||
|
? LL.RUN_COMMAND()
|
||||||
|
: writeable
|
||||||
|
? LL.CHANGE_VALUE()
|
||||||
|
: LL.VALUE(0)}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
<Box color="warning.main" p={0} pl={0} pr={0} mt={0} mb={2}>
|
<Box color="warning.main" p={0} pl={0} pr={0} mt={0} mb={2}>
|
||||||
<Typography variant="body2">{editItem.id.slice(2)}</Typography>
|
<Typography variant="body2">{editItem.id.slice(2)}</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Grid>
|
<Grid container>
|
||||||
<Grid item>
|
<Grid size={12}>
|
||||||
{editItem.l ? (
|
{editItem.l ? (
|
||||||
<TextField
|
<TextField
|
||||||
name="v"
|
name="v"
|
||||||
label={LL.VALUE(0)}
|
label={LL.VALUE(0)}
|
||||||
value={editItem.v}
|
value={editItem.v}
|
||||||
disabled={!writeable}
|
disabled={!writeable}
|
||||||
autoFocus
|
|
||||||
sx={{ width: '30ch' }}
|
sx={{ width: '30ch' }}
|
||||||
select
|
select
|
||||||
onChange={updateFormValue}
|
onChange={updateFormValue}
|
||||||
@@ -154,15 +138,23 @@ const DashboardDevicesDialog = ({
|
|||||||
fieldErrors={fieldErrors}
|
fieldErrors={fieldErrors}
|
||||||
name="v"
|
name="v"
|
||||||
label={LL.VALUE(0)}
|
label={LL.VALUE(0)}
|
||||||
value={Math.round(editItem.v * 10) / 10}
|
value={numberValue(Math.round((editItem.v as number) * 10) / 10)}
|
||||||
autoFocus
|
autoFocus
|
||||||
disabled={!writeable}
|
disabled={!writeable}
|
||||||
type="number"
|
type="number"
|
||||||
sx={{ width: '30ch' }}
|
sx={{ width: '30ch' }}
|
||||||
onChange={updateFormValue}
|
onChange={updateFormValue}
|
||||||
inputProps={editItem.s ? { min: editItem.m, max: editItem.x, step: editItem.s } : {}}
|
slotProps={{
|
||||||
InputProps={{
|
htmlInput: editItem.s
|
||||||
startAdornment: <InputAdornment position="start">{setUom(editItem.u)}</InputAdornment>
|
? { min: editItem.m, max: editItem.x, step: editItem.s }
|
||||||
|
: {},
|
||||||
|
input: {
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">
|
||||||
|
{setUom(editItem.u)}
|
||||||
|
</InputAdornment>
|
||||||
|
)
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -172,16 +164,15 @@ const DashboardDevicesDialog = ({
|
|||||||
label={LL.VALUE(0)}
|
label={LL.VALUE(0)}
|
||||||
value={editItem.v}
|
value={editItem.v}
|
||||||
disabled={!writeable}
|
disabled={!writeable}
|
||||||
autoFocus
|
|
||||||
sx={{ width: '30ch' }}
|
sx={{ width: '30ch' }}
|
||||||
multiline={editItem.u ? false : true}
|
multiline={!editItem.u}
|
||||||
onChange={updateFormValue}
|
onChange={updateFormValue}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
{writeable && (
|
{writeable && (
|
||||||
<Grid item>
|
<Grid>
|
||||||
<FormHelperText>format: {showHelperText(editItem)}</FormHelperText>
|
<FormHelperText>{showHelperText(editItem)}</FormHelperText>
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -197,10 +188,20 @@ const DashboardDevicesDialog = ({
|
|||||||
position: 'relative'
|
position: 'relative'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button startIcon={<CancelIcon />} variant="outlined" onClick={close} color="secondary">
|
<Button
|
||||||
|
startIcon={<CancelIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={close}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
{LL.CANCEL()}
|
{LL.CANCEL()}
|
||||||
</Button>
|
</Button>
|
||||||
<Button startIcon={<WarningIcon color="warning" />} variant="contained" onClick={save} color="info">
|
<Button
|
||||||
|
startIcon={<WarningIcon color="warning" />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={save}
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
{selectedItem.v === '' && selectedItem.c ? LL.EXECUTE() : LL.UPDATE()}
|
{selectedItem.v === '' && selectedItem.c ? LL.EXECUTE() : LL.UPDATE()}
|
||||||
</Button>
|
</Button>
|
||||||
{progress && (
|
{progress && (
|
||||||
@@ -225,4 +226,4 @@ const DashboardDevicesDialog = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DashboardDevicesDialog;
|
export default DevicesDialog;
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
import { ToggleButton, ToggleButtonGroup } from '@mui/material';
|
import { ToggleButton, ToggleButtonGroup } from '@mui/material';
|
||||||
|
|
||||||
import OptionIcon from './OptionIcon';
|
import OptionIcon from './OptionIcon';
|
||||||
import { DeviceEntityMask } from './types';
|
import { DeviceEntityMask } from './types';
|
||||||
import type { DeviceEntity } from './types';
|
import type { DeviceEntity } from './types';
|
||||||
|
|
||||||
type EntityMaskToggleProps = {
|
interface EntityMaskToggleProps {
|
||||||
onUpdate: (de: DeviceEntity) => void;
|
onUpdate: (de: DeviceEntity) => void;
|
||||||
de: DeviceEntity;
|
de: DeviceEntity;
|
||||||
};
|
}
|
||||||
|
|
||||||
const EntityMaskToggle = ({ onUpdate, de }: EntityMaskToggleProps) => {
|
const EntityMaskToggle = ({ onUpdate, de }: EntityMaskToggleProps) => {
|
||||||
const getMaskNumber = (newMask: string[]) => {
|
const getMaskNumber = (newMask: string[]) => {
|
||||||
@@ -42,7 +43,7 @@ const EntityMaskToggle = ({ onUpdate, de }: EntityMaskToggleProps) => {
|
|||||||
size="small"
|
size="small"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
value={getMaskString(de.m)}
|
value={getMaskString(de.m)}
|
||||||
onChange={(event, mask) => {
|
onChange={(event, mask: string[]) => {
|
||||||
de.m = getMaskNumber(mask);
|
de.m = getMaskNumber(mask);
|
||||||
if (de.n === '' && de.m & DeviceEntityMask.DV_READONLY) {
|
if (de.n === '' && de.m & DeviceEntityMask.DV_READONLY) {
|
||||||
de.m = de.m | DeviceEntityMask.DV_WEB_EXCLUDE;
|
de.m = de.m | DeviceEntityMask.DV_WEB_EXCLUDE;
|
||||||
@@ -54,25 +55,46 @@ const EntityMaskToggle = ({ onUpdate, de }: EntityMaskToggleProps) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ToggleButton value="8" disabled={(de.m & 0x81) !== 0 || de.n === undefined}>
|
<ToggleButton value="8" disabled={(de.m & 0x81) !== 0 || de.n === undefined}>
|
||||||
<OptionIcon type="favorite" isSet={(de.m & DeviceEntityMask.DV_FAVORITE) === DeviceEntityMask.DV_FAVORITE} />
|
<OptionIcon
|
||||||
|
type="favorite"
|
||||||
|
isSet={
|
||||||
|
(de.m & DeviceEntityMask.DV_FAVORITE) === DeviceEntityMask.DV_FAVORITE
|
||||||
|
}
|
||||||
|
/>
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
<ToggleButton value="4" disabled={!de.w || (de.m & 0x83) >= 3}>
|
<ToggleButton value="4" disabled={!de.w || (de.m & 0x83) >= 3}>
|
||||||
<OptionIcon type="readonly" isSet={(de.m & DeviceEntityMask.DV_READONLY) === DeviceEntityMask.DV_READONLY} />
|
<OptionIcon
|
||||||
|
type="readonly"
|
||||||
|
isSet={
|
||||||
|
(de.m & DeviceEntityMask.DV_READONLY) === DeviceEntityMask.DV_READONLY
|
||||||
|
}
|
||||||
|
/>
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
<ToggleButton value="2" disabled={de.n === '' || (de.m & 0x80) !== 0}>
|
<ToggleButton value="2" disabled={de.n === '' || (de.m & 0x80) !== 0}>
|
||||||
<OptionIcon
|
<OptionIcon
|
||||||
type="api_mqtt_exclude"
|
type="api_mqtt_exclude"
|
||||||
isSet={(de.m & DeviceEntityMask.DV_API_MQTT_EXCLUDE) === DeviceEntityMask.DV_API_MQTT_EXCLUDE}
|
isSet={
|
||||||
|
(de.m & DeviceEntityMask.DV_API_MQTT_EXCLUDE) ===
|
||||||
|
DeviceEntityMask.DV_API_MQTT_EXCLUDE
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
<ToggleButton value="1" disabled={de.n === undefined || (de.m & 0x80) !== 0}>
|
<ToggleButton value="1" disabled={de.n === undefined || (de.m & 0x80) !== 0}>
|
||||||
<OptionIcon
|
<OptionIcon
|
||||||
type="web_exclude"
|
type="web_exclude"
|
||||||
isSet={(de.m & DeviceEntityMask.DV_WEB_EXCLUDE) === DeviceEntityMask.DV_WEB_EXCLUDE}
|
isSet={
|
||||||
|
(de.m & DeviceEntityMask.DV_WEB_EXCLUDE) ===
|
||||||
|
DeviceEntityMask.DV_WEB_EXCLUDE
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
<ToggleButton value="128">
|
<ToggleButton value="128">
|
||||||
<OptionIcon type="deleted" isSet={(de.m & DeviceEntityMask.DV_DELETED) === DeviceEntityMask.DV_DELETED} />
|
<OptionIcon
|
||||||
|
type="deleted"
|
||||||
|
isSet={
|
||||||
|
(de.m & DeviceEntityMask.DV_DELETED) === DeviceEntityMask.DV_DELETED
|
||||||
|
}
|
||||||
|
/>
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
</ToggleButtonGroup>
|
</ToggleButtonGroup>
|
||||||
);
|
);
|
||||||
184
interface/src/app/main/Help.tsx
Normal file
184
interface/src/app/main/Help.tsx
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
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: 'getCustomSupport' })).onSuccess((event) => {
|
||||||
|
if (event && event.data && Object.keys(event.data).length !== 0) {
|
||||||
|
const data = (event.data as { Support: { img_url?: string; html?: string[] } })
|
||||||
|
.Support;
|
||||||
|
if (data.img_url) {
|
||||||
|
setCustomSupportIMG(data.img_url);
|
||||||
|
}
|
||||||
|
if (data.html) {
|
||||||
|
setCustomSupportHTML(data.html.join('<br/>'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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(String(error.error?.message || 'An error occurred'));
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionContent>
|
||||||
|
{customSupportHTML && (
|
||||||
|
<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">
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: customSupportHTML }} />
|
||||||
|
</Typography>
|
||||||
|
<Box
|
||||||
|
component="img"
|
||||||
|
referrerPolicy="no-referrer"
|
||||||
|
sx={{
|
||||||
|
maxHeight: { xs: 100, md: 250 }
|
||||||
|
}}
|
||||||
|
onError={() => setNotFound(true)}
|
||||||
|
src={
|
||||||
|
notFound
|
||||||
|
? ''
|
||||||
|
: customSupportIMG ||
|
||||||
|
'https://docs.emsesp.org/_media/images/installer.jpeg'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{me.admin && (
|
||||||
|
<List sx={{ borderRadius: 3, border: '2px solid grey' }}>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemButton
|
||||||
|
component="a"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
href="https://docs.emsesp.org"
|
||||||
|
>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<Avatar sx={{ bgcolor: '#72caf9' }}>
|
||||||
|
<MenuBookIcon />
|
||||||
|
</Avatar>
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText primary={LL.HELP_INFORMATION_1()} />
|
||||||
|
</ListItemButton>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
|
<ListItem>
|
||||||
|
<ListItemButton
|
||||||
|
component="a"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
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"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
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>
|
||||||
|
|
||||||
|
<Divider sx={{ mt: 4 }} />
|
||||||
|
|
||||||
|
<Typography color="white" variant="subtitle1" align="center" mt={1}>
|
||||||
|
©
|
||||||
|
<Link
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
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';
|
||||||
|
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,
|
||||||
|
Grid,
|
||||||
|
TextField
|
||||||
|
} from '@mui/material';
|
||||||
|
|
||||||
|
import { dialogStyle } from 'CustomTheme';
|
||||||
|
import { BlockFormControlLabel } from 'components';
|
||||||
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
import { updateValue } from 'utils';
|
||||||
|
|
||||||
|
import type { ModuleItem } from './types';
|
||||||
|
|
||||||
|
interface ModulesDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSave: (mi: ModuleItem) => void;
|
||||||
|
selectedItem: ModuleItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModulesDialog = ({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
onSave,
|
||||||
|
selectedItem
|
||||||
|
}: ModulesDialogProps) => {
|
||||||
|
const { LL } = useI18nContext();
|
||||||
|
const [editItem, setEditItem] = useState<ModuleItem>(selectedItem);
|
||||||
|
|
||||||
|
const updateFormValue = updateValue(setEditItem);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
setEditItem(selectedItem);
|
||||||
|
}
|
||||||
|
}, [open, selectedItem]);
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const save = () => {
|
||||||
|
onSave(editItem);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog sx={dialogStyle} fullWidth maxWidth="xs" open={open} onClose={onClose}>
|
||||||
|
<DialogTitle>{LL.EDIT() + ' ' + editItem.key}</DialogTitle>
|
||||||
|
<DialogContent dividers>
|
||||||
|
<Grid container>
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={editItem.enabled}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
name="enabled"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Enabled"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Box mt={2} mb={1}>
|
||||||
|
<TextField
|
||||||
|
name="license"
|
||||||
|
label="License Key"
|
||||||
|
multiline
|
||||||
|
rows={6}
|
||||||
|
fullWidth
|
||||||
|
value={editItem.license}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button
|
||||||
|
startIcon={<CancelIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={close}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
|
{LL.CANCEL()}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
startIcon={<DoneIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={save}
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
{LL.UPDATE()}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModulesDialog;
|
||||||
@@ -3,20 +3,26 @@ import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
|
|||||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||||
import EditOffOutlinedIcon from '@mui/icons-material/EditOffOutlined';
|
import EditOffOutlinedIcon from '@mui/icons-material/EditOffOutlined';
|
||||||
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
|
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
|
||||||
|
|
||||||
import InsertCommentOutlinedIcon from '@mui/icons-material/InsertCommentOutlined';
|
import InsertCommentOutlinedIcon from '@mui/icons-material/InsertCommentOutlined';
|
||||||
import StarIcon from '@mui/icons-material/Star';
|
import StarIcon from '@mui/icons-material/Star';
|
||||||
import StarOutlineIcon from '@mui/icons-material/StarOutline';
|
import StarOutlineIcon from '@mui/icons-material/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 type { SvgIconProps } from '@mui/material';
|
||||||
import type { FC } from 'react';
|
|
||||||
|
|
||||||
type OptionType = 'deleted' | 'readonly' | 'web_exclude' | 'api_mqtt_exclude' | 'favorite';
|
type OptionType =
|
||||||
|
| 'deleted'
|
||||||
|
| 'readonly'
|
||||||
|
| 'web_exclude'
|
||||||
|
| 'api_mqtt_exclude'
|
||||||
|
| 'favorite';
|
||||||
|
|
||||||
const OPTION_ICONS: { [type in OptionType]: [React.ComponentType<SvgIconProps>, React.ComponentType<SvgIconProps>] } = {
|
const OPTION_ICONS: {
|
||||||
|
[type in OptionType]: [
|
||||||
|
React.ComponentType<SvgIconProps>,
|
||||||
|
React.ComponentType<SvgIconProps>
|
||||||
|
];
|
||||||
|
} = {
|
||||||
deleted: [DeleteForeverIcon, DeleteOutlineIcon],
|
deleted: [DeleteForeverIcon, DeleteOutlineIcon],
|
||||||
readonly: [EditOffOutlinedIcon, EditOutlinedIcon],
|
readonly: [EditOffOutlinedIcon, EditOutlinedIcon],
|
||||||
web_exclude: [VisibilityOffOutlinedIcon, VisibilityOutlinedIcon],
|
web_exclude: [VisibilityOffOutlinedIcon, VisibilityOutlinedIcon],
|
||||||
@@ -24,12 +30,7 @@ const OPTION_ICONS: { [type in OptionType]: [React.ComponentType<SvgIconProps>,
|
|||||||
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: 16, verticalAlign: 'middle' }} />
|
<Icon color="primary" sx={{ fontSize: 16, verticalAlign: 'middle' }} />
|
||||||
@@ -1,28 +1,41 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useBlocker } from 'react-router';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
import CircleIcon from '@mui/icons-material/Circle';
|
||||||
import WarningIcon from '@mui/icons-material/Warning';
|
import WarningIcon from '@mui/icons-material/Warning';
|
||||||
|
import { Box, Button, Divider, Stack, Typography } from '@mui/material';
|
||||||
|
|
||||||
import { Box, Typography, Divider, Stack, Button } from '@mui/material';
|
import {
|
||||||
import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table';
|
Body,
|
||||||
|
Cell,
|
||||||
|
Header,
|
||||||
|
HeaderCell,
|
||||||
|
HeaderRow,
|
||||||
|
Row,
|
||||||
|
Table
|
||||||
|
} from '@table-library/react-table-library/table';
|
||||||
import { useTheme } from '@table-library/react-table-library/theme';
|
import { useTheme } from '@table-library/react-table-library/theme';
|
||||||
// eslint-disable-next-line import/named
|
import { updateState, useRequest } from 'alova/client';
|
||||||
import { updateState, useRequest } from 'alova';
|
import {
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
BlockNavigation,
|
||||||
import { unstable_useBlocker as useBlocker } from 'react-router-dom';
|
ButtonRow,
|
||||||
import { toast } from 'react-toastify';
|
FormLoader,
|
||||||
import SettingsSchedulerDialog from './SettingsSchedulerDialog';
|
SectionContent,
|
||||||
import * as EMSESP from './api';
|
useLayoutTitle
|
||||||
import { ScheduleFlag } from './types';
|
} from 'components';
|
||||||
import { schedulerItemValidation } from './validators';
|
|
||||||
import type { ScheduleItem } from './types';
|
|
||||||
import type { FC } from 'react';
|
|
||||||
|
|
||||||
import { ButtonRow, FormLoader, SectionContent, BlockNavigation } from 'components';
|
|
||||||
|
|
||||||
import { useI18nContext } from 'i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
import { useInterval } from 'utils';
|
||||||
|
|
||||||
const SettingsScheduler: FC = () => {
|
import { readSchedule, writeSchedule } from '../../api/app';
|
||||||
|
import SettingsSchedulerDialog from './SchedulerDialog';
|
||||||
|
import { ScheduleFlag } from './types';
|
||||||
|
import type { Schedule, ScheduleItem } from './types';
|
||||||
|
import { schedulerItemValidation } from './validators';
|
||||||
|
|
||||||
|
const Scheduler = () => {
|
||||||
const { LL, locale } = useI18nContext();
|
const { LL, locale } = useI18nContext();
|
||||||
const [numChanges, setNumChanges] = useState<number>(0);
|
const [numChanges, setNumChanges] = useState<number>(0);
|
||||||
const blocker = useBlocker(numChanges !== 0);
|
const blocker = useBlocker(numChanges !== 0);
|
||||||
@@ -31,16 +44,22 @@ const SettingsScheduler: FC = () => {
|
|||||||
const [creating, setCreating] = useState<boolean>(false);
|
const [creating, setCreating] = useState<boolean>(false);
|
||||||
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useLayoutTitle(LL.SCHEDULER());
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: schedule,
|
data: schedule,
|
||||||
send: fetchSchedule,
|
send: fetchSchedule,
|
||||||
error
|
error
|
||||||
} = useRequest(EMSESP.readSchedule, {
|
} = useRequest(readSchedule, {
|
||||||
initialData: [],
|
initialData: []
|
||||||
force: true
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { send: writeSchedule } = useRequest((data) => EMSESP.writeSchedule(data), { immediate: false });
|
const { send: updateSchedule } = useRequest(
|
||||||
|
(data: Schedule) => writeSchedule(data),
|
||||||
|
{
|
||||||
|
immediate: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
function hasScheduleChanged(si: ScheduleItem) {
|
function hasScheduleChanged(si: ScheduleItem) {
|
||||||
return (
|
return (
|
||||||
@@ -55,8 +74,17 @@ const SettingsScheduler: FC = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useInterval(() => {
|
||||||
|
if (numChanges === 0) {
|
||||||
|
void fetchSchedule();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const formatter = new Intl.DateTimeFormat(locale, { weekday: 'short', timeZone: 'UTC' });
|
const formatter = new Intl.DateTimeFormat(locale, {
|
||||||
|
weekday: 'short',
|
||||||
|
timeZone: 'UTC'
|
||||||
|
});
|
||||||
const days = [1, 2, 3, 4, 5, 6, 7].map((day) => {
|
const days = [1, 2, 3, 4, 5, 6, 7].map((day) => {
|
||||||
const dd = day < 10 ? `0${day}` : day;
|
const dd = day < 10 ? `0${day}` : day;
|
||||||
return new Date(`2017-01-${dd}T00:00:00+00:00`);
|
return new Date(`2017-01-${dd}T00:00:00+00:00`);
|
||||||
@@ -66,7 +94,7 @@ const SettingsScheduler: FC = () => {
|
|||||||
|
|
||||||
const schedule_theme = useTheme({
|
const schedule_theme = useTheme({
|
||||||
Table: `
|
Table: `
|
||||||
--data-table-library_grid-template-columns: 36px 324px 50px 192px repeat(1, minmax(100px, 1fr)) 160px;
|
--data-table-library_grid-template-columns: 36px 210px 100px 192px repeat(1, minmax(100px, 1fr)) 160px;
|
||||||
`,
|
`,
|
||||||
BaseRow: `
|
BaseRow: `
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -96,21 +124,16 @@ const SettingsScheduler: FC = () => {
|
|||||||
position: relative;
|
position: relative;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
.td {
|
.td {
|
||||||
border-top: 1px solid #565656;
|
|
||||||
border-bottom: 1px solid #565656;
|
border-bottom: 1px solid #565656;
|
||||||
}
|
}
|
||||||
&:hover .td {
|
&:hover .td {
|
||||||
border-top: 1px solid #177ac9;
|
background-color: #177ac9;
|
||||||
border-bottom: 1px solid #177ac9;
|
|
||||||
}
|
|
||||||
&:nth-of-type(odd) .td {
|
|
||||||
background-color: #303030;
|
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
});
|
});
|
||||||
|
|
||||||
const saveSchedule = async () => {
|
const saveSchedule = async () => {
|
||||||
await writeSchedule({
|
await updateSchedule({
|
||||||
schedule: schedule
|
schedule: schedule
|
||||||
.filter((si) => !si.deleted)
|
.filter((si) => !si.deleted)
|
||||||
.map((condensed_si) => ({
|
.map((condensed_si) => ({
|
||||||
@@ -126,8 +149,8 @@ const SettingsScheduler: FC = () => {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success(LL.SCHEDULE_UPDATED());
|
toast.success(LL.SCHEDULE_UPDATED());
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((error: Error) => {
|
||||||
toast.error(err.message);
|
toast.error(error.message);
|
||||||
})
|
})
|
||||||
.finally(async () => {
|
.finally(async () => {
|
||||||
await fetchSchedule();
|
await fetchSchedule();
|
||||||
@@ -139,6 +162,9 @@ const SettingsScheduler: FC = () => {
|
|||||||
setCreating(false);
|
setCreating(false);
|
||||||
setSelectedScheduleItem(si);
|
setSelectedScheduleItem(si);
|
||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
|
if (si.o_name === undefined) {
|
||||||
|
si.o_name = si.name;
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onDialogClose = () => {
|
const onDialogClose = () => {
|
||||||
@@ -153,12 +179,18 @@ const SettingsScheduler: FC = () => {
|
|||||||
|
|
||||||
const onDialogSave = (updatedItem: ScheduleItem) => {
|
const onDialogSave = (updatedItem: ScheduleItem) => {
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
|
void updateState(readSchedule(), (data: ScheduleItem[]) => {
|
||||||
updateState('schedule', (data) => {
|
|
||||||
const new_data = creating
|
const new_data = creating
|
||||||
? [...data.filter((si) => creating || si.o_id !== updatedItem.o_id), updatedItem]
|
? [
|
||||||
: data.map((si) => (si.id === updatedItem.id ? { ...si, ...updatedItem } : si));
|
...data.filter((si) => creating || si.o_id !== updatedItem.o_id),
|
||||||
|
updatedItem
|
||||||
|
]
|
||||||
|
: data.map((si) =>
|
||||||
|
si.id === updatedItem.id ? { ...si, ...updatedItem } : si
|
||||||
|
);
|
||||||
|
|
||||||
setNumChanges(new_data.filter((si) => hasScheduleChanged(si)).length);
|
setNumChanges(new_data.filter((si) => hasScheduleChanged(si)).length);
|
||||||
|
|
||||||
return new_data;
|
return new_data;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -169,8 +201,8 @@ const SettingsScheduler: FC = () => {
|
|||||||
id: Math.floor(Math.random() * (Math.floor(200) - 100) + 100),
|
id: Math.floor(Math.random() * (Math.floor(200) - 100) + 100),
|
||||||
active: false,
|
active: false,
|
||||||
deleted: false,
|
deleted: false,
|
||||||
flags: 0,
|
flags: ScheduleFlag.SCHEDULE_DAY,
|
||||||
time: '12:00',
|
time: '',
|
||||||
cmd: '',
|
cmd: '',
|
||||||
value: '',
|
value: '',
|
||||||
name: ''
|
name: ''
|
||||||
@@ -186,27 +218,52 @@ const SettingsScheduler: FC = () => {
|
|||||||
const dayBox = (si: ScheduleItem, flag: number) => (
|
const dayBox = (si: ScheduleItem, flag: number) => (
|
||||||
<>
|
<>
|
||||||
<Box>
|
<Box>
|
||||||
<Typography sx={{ fontSize: 11 }} color={(si.flags & flag) === flag ? 'primary' : 'grey'}>
|
<Typography
|
||||||
{flag === ScheduleFlag.SCHEDULE_TIMER ? LL.TIMER(0) : dow[Math.log(flag) / Math.log(2)]}
|
sx={{ fontSize: 11 }}
|
||||||
|
color={(si.flags & flag) === flag ? 'primary' : 'grey'}
|
||||||
|
>
|
||||||
|
{dow[Math.log(flag) / Math.log(2)]}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Divider orientation="vertical" flexItem />
|
<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 (
|
return (
|
||||||
<Table
|
<Table
|
||||||
data={{ nodes: schedule.filter((si) => !si.deleted).sort((a, b) => a.time.localeCompare(b.time)) }}
|
data={{
|
||||||
|
nodes: schedule
|
||||||
|
.filter((si) => !si.deleted)
|
||||||
|
.sort((a, b) => a.flags - b.flags)
|
||||||
|
}}
|
||||||
theme={schedule_theme}
|
theme={schedule_theme}
|
||||||
layout={{ custom: true }}
|
layout={{ custom: true }}
|
||||||
>
|
>
|
||||||
{(tableList: any) => (
|
{(tableList: ScheduleItem[]) => (
|
||||||
<>
|
<>
|
||||||
<Header>
|
<Header>
|
||||||
<HeaderRow>
|
<HeaderRow>
|
||||||
<HeaderCell />
|
<HeaderCell />
|
||||||
<HeaderCell stiff>{LL.SCHEDULE(0)}</HeaderCell>
|
<HeaderCell stiff>{LL.SCHEDULE(0)}</HeaderCell>
|
||||||
<HeaderCell stiff>{LL.TIME(0)}</HeaderCell>
|
<HeaderCell stiff>{LL.TIME(0)}/Cond.</HeaderCell>
|
||||||
<HeaderCell stiff>{LL.COMMAND(0)}</HeaderCell>
|
<HeaderCell stiff>{LL.COMMAND(0)}</HeaderCell>
|
||||||
<HeaderCell stiff>{LL.VALUE(0)}</HeaderCell>
|
<HeaderCell stiff>{LL.VALUE(0)}</HeaderCell>
|
||||||
<HeaderCell stiff>{LL.NAME(0)}</HeaderCell>
|
<HeaderCell stiff>{LL.NAME(0)}</HeaderCell>
|
||||||
@@ -216,19 +273,34 @@ const SettingsScheduler: FC = () => {
|
|||||||
{tableList.map((si: ScheduleItem) => (
|
{tableList.map((si: ScheduleItem) => (
|
||||||
<Row key={si.id} item={si} onClick={() => editScheduleItem(si)}>
|
<Row key={si.id} item={si} onClick={() => editScheduleItem(si)}>
|
||||||
<Cell stiff>
|
<Cell stiff>
|
||||||
{si.active && <CheckCircleIcon sx={{ color: '#79D200', fontSize: 16, verticalAlign: 'middle' }} />}
|
{si.active ? (
|
||||||
|
<CircleIcon
|
||||||
|
color="success"
|
||||||
|
sx={{ fontSize: 16, verticalAlign: 'middle' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<CircleIcon
|
||||||
|
color="error"
|
||||||
|
sx={{ fontSize: 16, verticalAlign: 'middle' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Cell>
|
</Cell>
|
||||||
<Cell stiff>
|
<Cell stiff>
|
||||||
<Stack spacing={1} direction="row">
|
<Stack spacing={0.5} direction="row">
|
||||||
<Divider orientation="vertical" flexItem />
|
<Divider orientation="vertical" flexItem />
|
||||||
{dayBox(si, ScheduleFlag.SCHEDULE_MON)}
|
{si.flags > 127 ? (
|
||||||
{dayBox(si, ScheduleFlag.SCHEDULE_TUE)}
|
scheduleType(si)
|
||||||
{dayBox(si, ScheduleFlag.SCHEDULE_WED)}
|
) : (
|
||||||
{dayBox(si, ScheduleFlag.SCHEDULE_THU)}
|
<>
|
||||||
{dayBox(si, ScheduleFlag.SCHEDULE_FRI)}
|
{dayBox(si, ScheduleFlag.SCHEDULE_MON)}
|
||||||
{dayBox(si, ScheduleFlag.SCHEDULE_SAT)}
|
{dayBox(si, ScheduleFlag.SCHEDULE_TUE)}
|
||||||
{dayBox(si, ScheduleFlag.SCHEDULE_SUN)}
|
{dayBox(si, ScheduleFlag.SCHEDULE_WED)}
|
||||||
{dayBox(si, ScheduleFlag.SCHEDULE_TIMER)}
|
{dayBox(si, ScheduleFlag.SCHEDULE_THU)}
|
||||||
|
{dayBox(si, ScheduleFlag.SCHEDULE_FRI)}
|
||||||
|
{dayBox(si, ScheduleFlag.SCHEDULE_SAT)}
|
||||||
|
{dayBox(si, ScheduleFlag.SCHEDULE_SUN)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Cell>
|
</Cell>
|
||||||
<Cell>{si.time}</Cell>
|
<Cell>{si.time}</Cell>
|
||||||
@@ -245,10 +317,10 @@ const SettingsScheduler: FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContent title={LL.SCHEDULER()} titleGutter>
|
<SectionContent>
|
||||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||||
<Box mb={2} color="warning.main">
|
<Box mb={2} color="warning.main">
|
||||||
<Typography variant="body2">{LL.SCHEDULER_HELP_1()}</Typography>
|
<Typography variant="body1">{LL.SCHEDULER_HELP_1()}.</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
{renderSchedule()}
|
{renderSchedule()}
|
||||||
|
|
||||||
@@ -264,11 +336,16 @@ const SettingsScheduler: FC = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Box display="flex" flexWrap="wrap">
|
<Box mt={1} display="flex" flexWrap="wrap">
|
||||||
<Box flexGrow={1}>
|
<Box flexGrow={1}>
|
||||||
{numChanges !== 0 && (
|
{numChanges !== 0 && (
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Button startIcon={<CancelIcon />} variant="outlined" onClick={onDialogCancel} color="secondary">
|
<Button
|
||||||
|
startIcon={<CancelIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={onDialogCancel}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
{LL.CANCEL()}
|
{LL.CANCEL()}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -284,7 +361,12 @@ const SettingsScheduler: FC = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
<Box flexWrap="nowrap" whiteSpace="nowrap">
|
<Box flexWrap="nowrap" whiteSpace="nowrap">
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Button startIcon={<AddIcon />} variant="outlined" color="secondary" onClick={addScheduleItem}>
|
<Button
|
||||||
|
startIcon={<AddIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
onClick={addScheduleItem}
|
||||||
|
>
|
||||||
{LL.ADD(0)}
|
{LL.ADD(0)}
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonRow>
|
</ButtonRow>
|
||||||
@@ -294,4 +376,4 @@ const SettingsScheduler: FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SettingsScheduler;
|
export default Scheduler;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user