mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-06 15:59:52 +03:00
Compare commits
680 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29110e96e5 | ||
|
|
02e2b51814 | ||
|
|
e9588cc7a1 | ||
|
|
3d8e1b8f86 | ||
|
|
312f969364 | ||
|
|
0f41079803 | ||
|
|
7f30e8dadc | ||
|
|
9cd20cfc05 | ||
|
|
1343bbf6ea | ||
|
|
90c39d1f85 | ||
|
|
92da61376b | ||
|
|
69976c2caf | ||
|
|
3c6fd0c83a | ||
|
|
b0a09747d4 | ||
|
|
b65866217a | ||
|
|
611e3b1243 | ||
|
|
84d3d42306 | ||
|
|
61e2739ef7 | ||
|
|
a3391afd27 | ||
|
|
ce9b7d1468 | ||
|
|
1b86314a14 | ||
|
|
b2a0519f83 | ||
|
|
3dec4bda8c | ||
|
|
5de529cbb2 | ||
|
|
4fd1d8e08a | ||
|
|
0847ccc602 | ||
|
|
969803569e | ||
|
|
6532abd870 | ||
|
|
07dabb4ceb | ||
|
|
1c6a683015 | ||
|
|
bb38458ac4 | ||
|
|
53de2ca25b | ||
|
|
5a9122f8be | ||
|
|
dc84f91044 | ||
|
|
4be6626470 | ||
|
|
7e4494aae1 | ||
|
|
50e54e6a1c | ||
|
|
30b111b986 | ||
|
|
a63f2e6131 | ||
|
|
51d487b938 | ||
|
|
96180c837d | ||
|
|
da20cf1ed2 | ||
|
|
bffad5e3c8 | ||
|
|
74287ebb99 | ||
|
|
8b40c92f7e | ||
|
|
5f6033cac1 | ||
|
|
24582407d4 | ||
|
|
22c9e3ee1f | ||
|
|
cb2c898b7e | ||
|
|
c0bf623266 | ||
|
|
18f22d3951 | ||
|
|
e2dad610b0 | ||
|
|
5ed3cbee2e | ||
|
|
27712badb6 | ||
|
|
72c032adff | ||
|
|
7197df9812 | ||
|
|
898e2e5f21 | ||
|
|
cde3b7541f | ||
|
|
822f55497e | ||
|
|
7c16870294 | ||
|
|
e368f422f4 | ||
|
|
9d9ac4ed9e | ||
|
|
d7576ebda1 | ||
|
|
fd810ff01c | ||
|
|
999a05f7ff | ||
|
|
a19f2f19c5 | ||
|
|
f54ccd40e6 | ||
|
|
5893487d4a | ||
|
|
3c8e20d4e4 | ||
|
|
bf68a5523b | ||
|
|
9c5c27152c | ||
|
|
c229371c68 | ||
|
|
805cef68a2 | ||
|
|
09addcb975 | ||
|
|
409d382ff9 | ||
|
|
27bfc14438 | ||
|
|
6c20a5f4f9 | ||
|
|
ffd61a9f67 | ||
|
|
4f2da0347c | ||
|
|
3c13c144d5 | ||
|
|
d4eaedef3d | ||
|
|
a8f1892d48 | ||
|
|
e347ac5742 | ||
|
|
5f2a9b093d | ||
|
|
e821e8d082 | ||
|
|
05f56be2d8 | ||
|
|
88f78f6541 | ||
|
|
234533f241 | ||
|
|
b1f72b0e3e | ||
|
|
a3022f6f20 | ||
|
|
95f7583511 | ||
|
|
578ba386e6 | ||
|
|
df7be9d11e | ||
|
|
72a59917fb | ||
|
|
9406c76e55 | ||
|
|
ea550b1656 | ||
|
|
d20741c0f0 | ||
|
|
91b7fd59d1 | ||
|
|
95e81ad824 | ||
|
|
316832fc4f | ||
|
|
23b6d81c47 | ||
|
|
8284520733 | ||
|
|
57f53818e1 | ||
|
|
7bca3fb2ed | ||
|
|
ae4a1358af | ||
|
|
95e3a11a11 | ||
|
|
f1a859c650 | ||
|
|
8d9fd95e85 | ||
|
|
452921d198 | ||
|
|
3e0f6f55fb | ||
|
|
0e480bbd94 | ||
|
|
7a079d866f | ||
|
|
af2710125e | ||
|
|
fb3de2e36d | ||
|
|
bf40222105 | ||
|
|
e1419edb15 | ||
|
|
131b936a69 | ||
|
|
5850a82d80 | ||
|
|
b76b6be3d1 | ||
|
|
b8f69eeaa8 | ||
|
|
d78fb53845 | ||
|
|
54889fec41 | ||
|
|
2c5c4d6e04 | ||
|
|
e6a44c9c82 | ||
|
|
7cba52d77e | ||
|
|
a79a67e4b2 | ||
|
|
01bace4048 | ||
|
|
d5f8419157 | ||
|
|
40a7026d4c | ||
|
|
47cb296cc4 | ||
|
|
be27033d41 | ||
|
|
3001a2d66f | ||
|
|
becaff0711 | ||
|
|
aaab9f409f | ||
|
|
068bb5cbeb | ||
|
|
337c07d7bc | ||
|
|
c387f65b4a | ||
|
|
df13081f97 | ||
|
|
50f6d0ab26 | ||
|
|
fb7bafdb87 | ||
|
|
b3472c3919 | ||
|
|
4f47712d52 | ||
|
|
547ccb96c9 | ||
|
|
7f3ff434ea | ||
|
|
aad4b0ade3 | ||
|
|
d587f44ec9 | ||
|
|
f30d3cf637 | ||
|
|
8fed47f39b | ||
|
|
48cedbd0fb | ||
|
|
752530a381 | ||
|
|
45fc6daa4a | ||
|
|
e17ce9c3b5 | ||
|
|
2657b9d1a5 | ||
|
|
b77a56ade2 | ||
|
|
4d5f588748 | ||
|
|
6582ba6317 | ||
|
|
b3453d9d02 | ||
|
|
e4b73140c8 | ||
|
|
235f789228 | ||
|
|
c029cf79f7 | ||
|
|
0b796a85a8 | ||
|
|
d8191f79a4 | ||
|
|
6a259f7cca | ||
|
|
22a2b92022 | ||
|
|
25616ae3b4 | ||
|
|
372aee30cd | ||
|
|
0c5023323a | ||
|
|
2d1126b9e4 | ||
|
|
3ecf92fa41 | ||
|
|
50befd8991 | ||
|
|
676268d7af | ||
|
|
f2457a7050 | ||
|
|
ded90dc4ce | ||
|
|
cddadcfae2 | ||
|
|
3113d392ac | ||
|
|
6433d5f744 | ||
|
|
f42c265714 | ||
|
|
d6711ac850 | ||
|
|
9d7c2de1d5 | ||
|
|
b1e1c44e77 | ||
|
|
85f54dd210 | ||
|
|
eea32ad134 | ||
|
|
4f24035082 | ||
|
|
0b0b1d9ca4 | ||
|
|
98828ed848 | ||
|
|
7b0f7cd32c | ||
|
|
67cb778039 | ||
|
|
9f49afae0a | ||
|
|
49c7c7aa2d | ||
|
|
42d89d1d10 | ||
|
|
24fae0d03e | ||
|
|
2c337f1d03 | ||
|
|
fcc521d5ed | ||
|
|
91005876eb | ||
|
|
da5b4aa79d | ||
|
|
ced440392b | ||
|
|
33d7ba1fda | ||
|
|
a6095fc305 | ||
|
|
84cc964a7a | ||
|
|
9e856b28a9 | ||
|
|
9378fdf2b6 | ||
|
|
6a134dda1f | ||
|
|
4f927ee571 | ||
|
|
b111869422 | ||
|
|
89a249eae4 | ||
|
|
7942d52843 | ||
|
|
b6310302d2 | ||
|
|
87774e73e1 | ||
|
|
548b5ff4a1 | ||
|
|
80fedf3fa3 | ||
|
|
8523cdffa3 | ||
|
|
29f2335935 | ||
|
|
eba6324d18 | ||
|
|
c3874d7c95 | ||
|
|
3086342d2b | ||
|
|
f836209249 | ||
|
|
d4b06cf0c0 | ||
|
|
54199affc1 | ||
|
|
4d88c6a90b | ||
|
|
906813b8f5 | ||
|
|
30d1e7ecb4 | ||
|
|
8fab4e29bf | ||
|
|
252554ea87 | ||
|
|
ba3d49172c | ||
|
|
6ef8ff757a | ||
|
|
fcc2c0b3de | ||
|
|
16e390f849 | ||
|
|
a31cf53863 | ||
|
|
09d8a6360b | ||
|
|
b1f59d4727 | ||
|
|
d8add7edcb | ||
|
|
1a71921fd6 | ||
|
|
6d12fff4fe | ||
|
|
769301c804 | ||
|
|
6a828e9ca5 | ||
|
|
2516d2d6de | ||
|
|
e92a3ad025 | ||
|
|
5686094151 | ||
|
|
915749dd69 | ||
|
|
258bc2b544 | ||
|
|
a9748f5b46 | ||
|
|
56f1c7946e | ||
|
|
88a427578f | ||
|
|
0ebd9e1fe1 | ||
|
|
bea0b4ba01 | ||
|
|
f8bee9b5c5 | ||
|
|
500e089b97 | ||
|
|
2adef52abf | ||
|
|
e01bfe1bdf | ||
|
|
418b1b3d68 | ||
|
|
401929d992 | ||
|
|
ee34dd72f7 | ||
|
|
b214d7a662 | ||
|
|
079a40cd32 | ||
|
|
adebea1561 | ||
|
|
28bf7c3225 | ||
|
|
78b2efd148 | ||
|
|
11590061a3 | ||
|
|
09847ee33c | ||
|
|
db34785be0 | ||
|
|
538a9cf642 | ||
|
|
d0346e436a | ||
|
|
442202d978 | ||
|
|
3b2a89d9f4 | ||
|
|
0bf366ac75 | ||
|
|
c62e73d21e | ||
|
|
2889899bfd | ||
|
|
f18ac2e48b | ||
|
|
1d259d14c8 | ||
|
|
b8c07e31cf | ||
|
|
3f27ed7b18 | ||
|
|
d7678b340f | ||
|
|
b697356465 | ||
|
|
1b9a2f21d2 | ||
|
|
e26451fcc0 | ||
|
|
17db542775 | ||
|
|
dd865a2db5 | ||
|
|
834c7cb87f | ||
|
|
a9a015cb5b | ||
|
|
94a71998a7 | ||
|
|
aa212924bc | ||
|
|
edef2f1bf6 | ||
|
|
912f53762e | ||
|
|
02c04c8f41 | ||
|
|
3ad90a6e34 | ||
|
|
0f83870db0 | ||
|
|
7529f3a73e | ||
|
|
77d559ac8c | ||
|
|
1311cad913 | ||
|
|
4d1ba9bede | ||
|
|
6b07651ed3 | ||
|
|
50ddfc0437 | ||
|
|
c0ac485772 | ||
|
|
8d4ae6971d | ||
|
|
88d0dfee2d | ||
|
|
551a479c6b | ||
|
|
3f85541c9a | ||
|
|
74cdb610d8 | ||
|
|
165dd3b418 | ||
|
|
7421f3e345 | ||
|
|
d00559bcd5 | ||
|
|
9774051fab | ||
|
|
c50692bae0 | ||
|
|
6348283001 | ||
|
|
1cbd34d94e | ||
|
|
f9d768a7a7 | ||
|
|
6c1bfccfaf | ||
|
|
2ca0a0c634 | ||
|
|
30ce3f1dc3 | ||
|
|
15541c52ce | ||
|
|
20c3e8c7bd | ||
|
|
59797fb89c | ||
|
|
849cc85398 | ||
|
|
dd318a1c8e | ||
|
|
2edf2a4231 | ||
|
|
d58ee1e693 | ||
|
|
088cb7fe40 | ||
|
|
38498a5587 | ||
|
|
6e3b30b03c | ||
|
|
4e9cf72816 | ||
|
|
7eb1f061b7 | ||
|
|
5e4f5916f2 | ||
|
|
1450737d94 | ||
|
|
bfd20e559e | ||
|
|
98a7932dee | ||
|
|
19e26d0d64 | ||
|
|
1715218864 | ||
|
|
e503c6cd79 | ||
|
|
9515e3d00b | ||
|
|
53e25ae213 | ||
|
|
4863ecc329 | ||
|
|
b5892f5b5e | ||
|
|
d16502c872 | ||
|
|
e29fb9ba8a | ||
|
|
2ee0411582 | ||
|
|
f210466cb1 | ||
|
|
6af28b1c29 | ||
|
|
049be2484e | ||
|
|
8f438e8045 | ||
|
|
a8382dd6ce | ||
|
|
c55385d6d8 | ||
|
|
87e6691433 | ||
|
|
aa9ba65f70 | ||
|
|
39fef48915 | ||
|
|
362fead7e8 | ||
|
|
e809ed3743 | ||
|
|
dc8c322b42 | ||
|
|
c5688ab632 | ||
|
|
d5d75eee63 | ||
|
|
6ec16733c3 | ||
|
|
ce2b2658ad | ||
|
|
3a866b1aea | ||
|
|
1cf938e16a | ||
|
|
1df427366f | ||
|
|
f97cdcb4d6 | ||
|
|
15c682cd1e | ||
|
|
008983be26 | ||
|
|
2f01000665 | ||
|
|
616955daef | ||
|
|
e22b191a48 | ||
|
|
8930c52ada | ||
|
|
6d94335079 | ||
|
|
f2e0b193af | ||
|
|
5b55902cd9 | ||
|
|
694f647a2c | ||
|
|
fc1cb00523 | ||
|
|
2fda59d7db | ||
|
|
a95837404a | ||
|
|
c6db2a1adf | ||
|
|
f6d22732a0 | ||
|
|
220a69938f | ||
|
|
d438866864 | ||
|
|
2e0ed9ce9f | ||
|
|
5de3b69e2c | ||
|
|
dfd6798377 | ||
|
|
074ae2a5a1 | ||
|
|
77f6a18075 | ||
|
|
0762d9e124 | ||
|
|
37dae04715 | ||
|
|
f299a7ad14 | ||
|
|
ba295385ab | ||
|
|
33adf518ae | ||
|
|
93885d0dd5 | ||
|
|
747cda79db | ||
|
|
f3cfc38adc | ||
|
|
dd3a0a706d | ||
|
|
37d001e7b5 | ||
|
|
add09e5a1c | ||
|
|
561d1c0e55 | ||
|
|
239ba335b1 | ||
|
|
ec83123090 | ||
|
|
4f6d5164a4 | ||
|
|
ae1e2eccd2 | ||
|
|
d7bc821bbe | ||
|
|
f8579f7c96 | ||
|
|
046a9ef6f2 | ||
|
|
64e15542a2 | ||
|
|
7dec674452 | ||
|
|
0f48d3e72c | ||
|
|
1f793c49ae | ||
|
|
e581539cf9 | ||
|
|
736eee79df | ||
|
|
7a0fe3819b | ||
|
|
65c9bf7e52 | ||
|
|
1e61b5670e | ||
|
|
6c2bae6296 | ||
|
|
358d6010b0 | ||
|
|
82978a25c5 | ||
|
|
3519696bae | ||
|
|
bd33df2cc7 | ||
|
|
5f44eb14ad | ||
|
|
fb94cf953a | ||
|
|
d7486218bc | ||
|
|
59913cdc4b | ||
|
|
2d7449aeba | ||
|
|
3ea53a8012 | ||
|
|
48cd12ec3d | ||
|
|
7c71ed2dc6 | ||
|
|
24d8ccc52f | ||
|
|
05cd96f2be | ||
|
|
75795ab1e9 | ||
|
|
bb1602f179 | ||
|
|
ac268f0f73 | ||
|
|
d924567e5f | ||
|
|
109d8df782 | ||
|
|
0aaa35098d | ||
|
|
6f57beab28 | ||
|
|
5b26e27834 | ||
|
|
8429f650aa | ||
|
|
3eb2202117 | ||
|
|
c217a40710 | ||
|
|
4a7308c5bb | ||
|
|
5fba51103e | ||
|
|
95a9808f35 | ||
|
|
c09e180c48 | ||
|
|
b09b650c1d | ||
|
|
44a41b963d | ||
|
|
0510189f54 | ||
|
|
16b3cf764d | ||
|
|
c634c39874 | ||
|
|
ae0846e877 | ||
|
|
40e7e1b418 | ||
|
|
e2a5853dde | ||
|
|
e419e67cb0 | ||
|
|
3356a4ce14 | ||
|
|
50459a23fe | ||
|
|
26a4347155 | ||
|
|
5bf53c3389 | ||
|
|
4b7aa95be3 | ||
|
|
b5921d15ac | ||
|
|
82a4f1499a | ||
|
|
e99c9208ad | ||
|
|
65dae7af42 | ||
|
|
050c75944a | ||
|
|
2d96aa1736 | ||
|
|
85161ec09a | ||
|
|
967eee67c4 | ||
|
|
469f78a329 | ||
|
|
1e4eb52c90 | ||
|
|
fdbbfe8ddb | ||
|
|
e79d4603fc | ||
|
|
9ef2e62955 | ||
|
|
c234503a9c | ||
|
|
2056d3ff19 | ||
|
|
270298eb8a | ||
|
|
7e7bd29c9a | ||
|
|
19b37d9e0e | ||
|
|
fc2bcd50ca | ||
|
|
37da9d3755 | ||
|
|
fffed3b411 | ||
|
|
9738c0848d | ||
|
|
fc11db03f0 | ||
|
|
17a28d246d | ||
|
|
3143ed1060 | ||
|
|
50540f1f82 | ||
|
|
fad1b09e19 | ||
|
|
a1912405c7 | ||
|
|
cc30e09e4b | ||
|
|
07a943eedf | ||
|
|
adf4584717 | ||
|
|
b3d647850d | ||
|
|
ee6a09c9df | ||
|
|
a84ae9e7cc | ||
|
|
40206a27ac | ||
|
|
5c282b7a7e | ||
|
|
8dd18aa24d | ||
|
|
db43f2d711 | ||
|
|
e9741ea4f8 | ||
|
|
cf416ee080 | ||
|
|
af41f352ba | ||
|
|
feed65bea6 | ||
|
|
66f14fff82 | ||
|
|
70943f5758 | ||
|
|
3bc280b817 | ||
|
|
9e432efcd1 | ||
|
|
8417c715c1 | ||
|
|
ddb3633fdb | ||
|
|
62b15a5319 | ||
|
|
8dd18802d6 | ||
|
|
8530520a62 | ||
|
|
b077d867ba | ||
|
|
8c48639572 | ||
|
|
4a6ca636e5 | ||
|
|
4bce819bd3 | ||
|
|
de63d10e3d | ||
|
|
c62183f886 | ||
|
|
6a73ee4a0b | ||
|
|
964db8e7d7 | ||
|
|
5b66528c0b | ||
|
|
01fd90f3ed | ||
|
|
5e7bed1063 | ||
|
|
4d69846932 | ||
|
|
fec5ff3132 | ||
|
|
15df0c0552 | ||
|
|
42a362196e | ||
|
|
ab28013ec6 | ||
|
|
75f3a6f82a | ||
|
|
e467e73755 | ||
|
|
505e846dd8 | ||
|
|
7808959d67 | ||
|
|
1ecee740d3 | ||
|
|
47eaeba373 | ||
|
|
e7dbccabec | ||
|
|
6ff3d243bd | ||
|
|
4027003729 | ||
|
|
70fd0ad658 | ||
|
|
94127ad3eb | ||
|
|
44734713f1 | ||
|
|
2f0f45f3ec | ||
|
|
8641e9d9cb | ||
|
|
bc78dd3f50 | ||
|
|
a9fca73f2d | ||
|
|
5cccfacbc4 | ||
|
|
7425d0e095 | ||
|
|
28068bdb98 | ||
|
|
bc69ca0a9b | ||
|
|
fd9ac28254 | ||
|
|
312fd85469 | ||
|
|
57a516a83a | ||
|
|
37bee39cea | ||
|
|
ed843ba58d | ||
|
|
f8c7da6e0c | ||
|
|
e0b1ff1353 | ||
|
|
0f78df517f | ||
|
|
e7dae28922 | ||
|
|
dbd3c04d1b | ||
|
|
e19566ecb8 | ||
|
|
370af11200 | ||
|
|
0e67e8311e | ||
|
|
81f4724d71 | ||
|
|
4a06d328d6 | ||
|
|
4dab735dad | ||
|
|
ce2fa15554 | ||
|
|
c0cb121660 | ||
|
|
efac66835a | ||
|
|
4a269fd508 | ||
|
|
bcef360252 | ||
|
|
bb262ed0df | ||
|
|
039d60abfb | ||
|
|
c6a40d2125 | ||
|
|
d15aa79d18 | ||
|
|
c0d5bd1f05 | ||
|
|
54c2a73d68 | ||
|
|
461aa1fd58 | ||
|
|
9211d29e17 | ||
|
|
3c1b30a5e4 | ||
|
|
7f52ef8bd8 | ||
|
|
32f477726b | ||
|
|
e9068e702e | ||
|
|
e97f6c09e5 | ||
|
|
a57fdaa4b3 | ||
|
|
1ae738016e | ||
|
|
f0e7ede499 | ||
|
|
a595bde1b8 | ||
|
|
e113ebd298 | ||
|
|
5339e0876e | ||
|
|
101978f713 | ||
|
|
23218bca7d | ||
|
|
7d0ed2246a | ||
|
|
c43fe4f9ae | ||
|
|
ee5b1b8c34 | ||
|
|
4f98b4bb21 | ||
|
|
2b95a0d125 | ||
|
|
87b2a05d39 | ||
|
|
44d0b52424 | ||
|
|
de9ff6a3a1 | ||
|
|
4a4e5f1890 | ||
|
|
fcc4831c9f | ||
|
|
6f435cbcfd | ||
|
|
b01264f701 | ||
|
|
e6e507a470 | ||
|
|
2b60eaf462 | ||
|
|
bf892aa5dc | ||
|
|
1bd834924a | ||
|
|
e854161da9 | ||
|
|
018b4af8d3 | ||
|
|
903696726c | ||
|
|
0a82c28fbf | ||
|
|
70d8b6824c | ||
|
|
c4e7747fd1 | ||
|
|
661b8791b3 | ||
|
|
c9a30a23ec | ||
|
|
28fde37f93 | ||
|
|
3797342a93 | ||
|
|
7faa0d6e65 | ||
|
|
23455750fa | ||
|
|
7cabae7ef5 | ||
|
|
7baf5c1d9a | ||
|
|
36780509a9 | ||
|
|
48c3aa7656 | ||
|
|
a951ebc3ed | ||
|
|
8ea48f7c81 | ||
|
|
a633225ad2 | ||
|
|
6b327e3ab3 | ||
|
|
cd43a9feb8 | ||
|
|
cf641476bf | ||
|
|
462a91b122 | ||
|
|
67a8b4eb80 | ||
|
|
e59f349a66 | ||
|
|
031f1abd5d | ||
|
|
73e478c50c | ||
|
|
14199ee4ea | ||
|
|
a9ec926ffb | ||
|
|
9f089bad75 | ||
|
|
8071fe04bc | ||
|
|
47a401b66e | ||
|
|
ddd2684d60 | ||
|
|
784ba7fc23 | ||
|
|
4bcc23641a | ||
|
|
dabb48fb61 | ||
|
|
9aea9aab50 | ||
|
|
b4aed240a7 | ||
|
|
015ab649af | ||
|
|
4cac16093f | ||
|
|
b77d9d4125 | ||
|
|
ac26d58b97 | ||
|
|
ed7b2ef4ef | ||
|
|
5fe5750130 | ||
|
|
314fff587c | ||
|
|
8318981f4e | ||
|
|
365e2fdb6b | ||
|
|
7e196785d8 | ||
|
|
5ef1c7e3bd | ||
|
|
11bdff9132 | ||
|
|
060802c8f1 | ||
|
|
312aeea39d | ||
|
|
6c41c49866 | ||
|
|
9dbc6d4d8f | ||
|
|
33c3ef64e9 | ||
|
|
8c1a138621 | ||
|
|
4f239d035e | ||
|
|
7fa93a8de0 | ||
|
|
84e76e2bd7 | ||
|
|
2021a2e52b | ||
|
|
e1f777e33a | ||
|
|
166f8f6c3a | ||
|
|
3ace3e2b63 | ||
|
|
8c52145c7b | ||
|
|
6e3b496f86 | ||
|
|
88c8cb424b | ||
|
|
74179ab6e9 | ||
|
|
f6fefc9a69 | ||
|
|
601f91e5a7 | ||
|
|
d553542206 | ||
|
|
3bacfc3361 | ||
|
|
45a6cd3606 | ||
|
|
577017bd0c | ||
|
|
9787d1686f | ||
|
|
108f236874 | ||
|
|
d47fcda0fe | ||
|
|
5d21ba2648 | ||
|
|
1b730062b7 | ||
|
|
6fb8a4bbe9 | ||
|
|
5c605e15dd | ||
|
|
9983269662 | ||
|
|
d891c7a325 | ||
|
|
9771ea8f2d | ||
|
|
5cf41bdce0 | ||
|
|
f28fafed8d | ||
|
|
81e2c31dd3 |
@@ -2,7 +2,7 @@ Language: Cpp
|
|||||||
BasedOnStyle: LLVM
|
BasedOnStyle: LLVM
|
||||||
UseTab: Never
|
UseTab: Never
|
||||||
IndentWidth: 4
|
IndentWidth: 4
|
||||||
ColumnLimit: 220
|
ColumnLimit: 160
|
||||||
TabWidth: 4
|
TabWidth: 4
|
||||||
#BreakBeforeBraces: Custom
|
#BreakBeforeBraces: Custom
|
||||||
BraceWrapping:
|
BraceWrapping:
|
||||||
|
|||||||
152
.clang-tidy
Normal file
152
.clang-tidy
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
---
|
||||||
|
Checks: >-
|
||||||
|
*,
|
||||||
|
-abseil-*,
|
||||||
|
-android-*,
|
||||||
|
-boost-*,
|
||||||
|
-bugprone-branch-clone,
|
||||||
|
-bugprone-narrowing-conversions,
|
||||||
|
-bugprone-signed-char-misuse,
|
||||||
|
-bugprone-too-small-loop-variable,
|
||||||
|
-cert-dcl50-cpp,
|
||||||
|
-cert-err58-cpp,
|
||||||
|
-cert-oop57-cpp,
|
||||||
|
-cert-str34-c,
|
||||||
|
-clang-analyzer-optin.cplusplus.UninitializedObject,
|
||||||
|
-clang-analyzer-osx.*,
|
||||||
|
-clang-diagnostic-delete-abstract-non-virtual-dtor,
|
||||||
|
-clang-diagnostic-delete-non-abstract-non-virtual-dtor,
|
||||||
|
-clang-diagnostic-shadow-field,
|
||||||
|
-clang-diagnostic-sign-compare,
|
||||||
|
-clang-diagnostic-unused-variable,
|
||||||
|
-clang-diagnostic-unused-const-variable,
|
||||||
|
-cppcoreguidelines-avoid-c-arrays,
|
||||||
|
-cppcoreguidelines-avoid-goto,
|
||||||
|
-cppcoreguidelines-avoid-magic-numbers,
|
||||||
|
-cppcoreguidelines-init-variables,
|
||||||
|
-cppcoreguidelines-macro-usage,
|
||||||
|
-cppcoreguidelines-narrowing-conversions,
|
||||||
|
-cppcoreguidelines-non-private-member-variables-in-classes,
|
||||||
|
-cppcoreguidelines-owning-memory,
|
||||||
|
-cppcoreguidelines-pro-bounds-array-to-pointer-decay,
|
||||||
|
-cppcoreguidelines-pro-bounds-constant-array-index,
|
||||||
|
-cppcoreguidelines-pro-bounds-pointer-arithmetic,
|
||||||
|
-cppcoreguidelines-pro-type-const-cast,
|
||||||
|
-cppcoreguidelines-pro-type-cstyle-cast,
|
||||||
|
-cppcoreguidelines-pro-type-member-init,
|
||||||
|
-cppcoreguidelines-pro-type-reinterpret-cast,
|
||||||
|
-cppcoreguidelines-pro-type-static-cast-downcast,
|
||||||
|
-cppcoreguidelines-pro-type-union-access,
|
||||||
|
-cppcoreguidelines-pro-type-vararg,
|
||||||
|
-cppcoreguidelines-special-member-functions,
|
||||||
|
-fuchsia-default-arguments,
|
||||||
|
-fuchsia-multiple-inheritance,
|
||||||
|
-fuchsia-overloaded-operator,
|
||||||
|
-fuchsia-statically-constructed-objects,
|
||||||
|
-fuchsia-default-arguments-declarations,
|
||||||
|
-fuchsia-default-arguments-calls,
|
||||||
|
-google-build-using-namespace,
|
||||||
|
-google-explicit-constructor,
|
||||||
|
-google-readability-braces-around-statements,
|
||||||
|
-google-readability-casting,
|
||||||
|
-google-readability-todo,
|
||||||
|
-google-runtime-references,
|
||||||
|
-hicpp-*,
|
||||||
|
-llvm-else-after-return,
|
||||||
|
-llvm-header-guard,
|
||||||
|
-llvm-include-order,
|
||||||
|
-llvm-qualified-auto,
|
||||||
|
-llvmlibc-*,
|
||||||
|
-misc-non-private-member-variables-in-classes,
|
||||||
|
-misc-no-recursion,
|
||||||
|
-misc-unused-parameters,
|
||||||
|
-modernize-avoid-c-arrays,
|
||||||
|
-modernize-return-braced-init-list,
|
||||||
|
-modernize-use-auto,
|
||||||
|
-modernize-use-default-member-init,
|
||||||
|
-modernize-use-equals-default,
|
||||||
|
-modernize-use-trailing-return-type,
|
||||||
|
-mpi-*,
|
||||||
|
-objc-*,
|
||||||
|
-readability-braces-around-statements,
|
||||||
|
-readability-const-return-type,
|
||||||
|
-readability-convert-member-functions-to-static,
|
||||||
|
-readability-else-after-return,
|
||||||
|
-readability-implicit-bool-conversion,
|
||||||
|
-readability-isolate-declaration,
|
||||||
|
-readability-magic-numbers,
|
||||||
|
-readability-make-member-function-const,
|
||||||
|
-readability-named-parameter,
|
||||||
|
-readability-qualified-auto,
|
||||||
|
-readability-redundant-access-specifiers,
|
||||||
|
-readability-redundant-member-init,
|
||||||
|
-readability-redundant-string-init,
|
||||||
|
-readability-uppercase-literal-suffix,
|
||||||
|
-readability-use-anyofallof,
|
||||||
|
-warnings-as-errors
|
||||||
|
WarningsAsErrors: '*'
|
||||||
|
AnalyzeTemporaryDtors: false
|
||||||
|
FormatStyle: google
|
||||||
|
CheckOptions:
|
||||||
|
- key: google-readability-braces-around-statements.ShortStatementLines
|
||||||
|
value: '1'
|
||||||
|
- key: google-readability-function-size.StatementThreshold
|
||||||
|
value: '800'
|
||||||
|
- key: google-readability-namespace-comments.ShortNamespaceLines
|
||||||
|
value: '10'
|
||||||
|
- key: google-readability-namespace-comments.SpacesBeforeComments
|
||||||
|
value: '2'
|
||||||
|
- key: modernize-loop-convert.MaxCopySize
|
||||||
|
value: '16'
|
||||||
|
- key: modernize-loop-convert.MinConfidence
|
||||||
|
value: reasonable
|
||||||
|
- key: modernize-loop-convert.NamingStyle
|
||||||
|
value: CamelCase
|
||||||
|
- key: modernize-pass-by-value.IncludeStyle
|
||||||
|
value: llvm
|
||||||
|
- key: modernize-replace-auto-ptr.IncludeStyle
|
||||||
|
value: llvm
|
||||||
|
- key: modernize-use-nullptr.NullMacros
|
||||||
|
value: 'NULL'
|
||||||
|
- key: readability-identifier-naming.LocalVariableCase
|
||||||
|
value: 'lower_case'
|
||||||
|
- key: readability-identifier-naming.ClassCase
|
||||||
|
value: 'CamelCase'
|
||||||
|
- key: readability-identifier-naming.StructCase
|
||||||
|
value: 'CamelCase'
|
||||||
|
- key: readability-identifier-naming.EnumCase
|
||||||
|
value: 'CamelCase'
|
||||||
|
- key: readability-identifier-naming.EnumConstantCase
|
||||||
|
value: 'UPPER_CASE'
|
||||||
|
- key: readability-identifier-naming.StaticConstantCase
|
||||||
|
value: 'UPPER_CASE'
|
||||||
|
- key: readability-identifier-naming.StaticVariableCase
|
||||||
|
value: 'UPPER_CASE'
|
||||||
|
- key: readability-identifier-naming.GlobalConstantCase
|
||||||
|
value: 'UPPER_CASE'
|
||||||
|
- key: readability-identifier-naming.ParameterCase
|
||||||
|
value: 'lower_case'
|
||||||
|
- key: readability-identifier-naming.PrivateMemberPrefix
|
||||||
|
value: 'NO_PRIVATE_MEMBERS_ALWAYS_USE_PROTECTED'
|
||||||
|
- key: readability-identifier-naming.PrivateMethodPrefix
|
||||||
|
value: 'NO_PRIVATE_METHODS_ALWAYS_USE_PROTECTED'
|
||||||
|
- key: readability-identifier-naming.ClassMemberCase
|
||||||
|
value: 'lower_case'
|
||||||
|
- key: readability-identifier-naming.ClassMemberCase
|
||||||
|
value: 'lower_case'
|
||||||
|
- key: readability-identifier-naming.ProtectedMemberCase
|
||||||
|
value: 'lower_case'
|
||||||
|
- key: readability-identifier-naming.ProtectedMemberSuffix
|
||||||
|
value: '_'
|
||||||
|
- key: readability-identifier-naming.FunctionCase
|
||||||
|
value: 'lower_case'
|
||||||
|
- key: readability-identifier-naming.ClassMethodCase
|
||||||
|
value: 'lower_case'
|
||||||
|
- key: readability-identifier-naming.ProtectedMethodCase
|
||||||
|
value: 'lower_case'
|
||||||
|
- key: readability-identifier-naming.ProtectedMethodSuffix
|
||||||
|
value: '_'
|
||||||
|
- key: readability-identifier-naming.VirtualMethodCase
|
||||||
|
value: 'lower_case'
|
||||||
|
- key: readability-identifier-naming.VirtualMethodSuffix
|
||||||
|
value: ''
|
||||||
37
.github/workflows/check_code.yml
vendored
37
.github/workflows/check_code.yml
vendored
@@ -1,37 +0,0 @@
|
|||||||
name: Code Check
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ dev ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ dev ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
analyze:
|
|
||||||
name: Analyze
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
# Override automatic language detection by changing the below list
|
|
||||||
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
|
|
||||||
language: ['cpp']
|
|
||||||
# Learn more...
|
|
||||||
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
|
||||||
- name: Initialize CodeQL
|
|
||||||
uses: github/codeql-action/init@v1
|
|
||||||
with:
|
|
||||||
languages: ${{ matrix.language }}
|
|
||||||
|
|
||||||
- run: |
|
|
||||||
make
|
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
|
||||||
uses: github/codeql-action/analyze@v1
|
|
||||||
28
.github/workflows/pre_release.yml
vendored
28
.github/workflows/pre_release.yml
vendored
@@ -1,6 +1,7 @@
|
|||||||
name: "pre-release"
|
name: "pre-release"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- "dev"
|
- "dev"
|
||||||
@@ -13,46 +14,41 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- name: Checkout source code
|
- uses: actions/checkout@v2
|
||||||
uses: actions/checkout@v2
|
- uses: actions/setup-python@v2
|
||||||
|
- uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: '16'
|
||||||
|
|
||||||
- name: Get build variables
|
- name: Get EMS-ESP source code and version
|
||||||
id: build_info
|
id: build_info
|
||||||
run: |
|
run: |
|
||||||
version=`grep -E '^#define EMSESP_APP_VERSION' ./src/version.h | awk -F'"' '{print $2}'`
|
version=`grep -E '^#define EMSESP_APP_VERSION' ./src/version.h | awk -F'"' '{print $2}'`
|
||||||
echo "::set-output name=version::$version"
|
echo "::set-output name=version::$version"
|
||||||
platform=`grep -E '^#define EMSESP_PLATFORM' ./src/version.h | awk -F'"' '{print $2}'`
|
|
||||||
echo "::set-output name=platform::$platform"
|
|
||||||
|
|
||||||
- name: Compile locally
|
- name: Install PlatformIO
|
||||||
run: make
|
|
||||||
|
|
||||||
- name: Setup Python
|
|
||||||
uses: actions/setup-python@v2
|
|
||||||
|
|
||||||
- name: Install pio
|
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
pip install -U platformio
|
pip install -U platformio
|
||||||
platformio upgrade
|
platformio upgrade
|
||||||
platformio update
|
platformio update
|
||||||
|
|
||||||
- name: Build web
|
- name: Build WebUI
|
||||||
run: |
|
run: |
|
||||||
cd interface
|
cd interface
|
||||||
npm install
|
npm ci
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
- name: Build firmware
|
- name: Build firmware
|
||||||
run: |
|
run: |
|
||||||
platformio run -e ci
|
platformio run -e ci
|
||||||
|
|
||||||
- name: Release
|
- name: Create a GH Release
|
||||||
id: "automatic_releases"
|
id: "automatic_releases"
|
||||||
uses: "marvinpinto/action-automatic-releases@latest"
|
uses: "marvinpinto/action-automatic-releases@latest"
|
||||||
with:
|
with:
|
||||||
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
title: ${{steps.build_info.outputs.platform}} Development Build v${{steps.build_info.outputs.version}}
|
title: Development Build v${{steps.build_info.outputs.version}}
|
||||||
automatic_release_tag: "latest"
|
automatic_release_tag: "latest"
|
||||||
prerelease: true
|
prerelease: true
|
||||||
files: |
|
files: |
|
||||||
|
|||||||
26
.github/workflows/tagged_release.yml
vendored
26
.github/workflows/tagged_release.yml
vendored
@@ -12,34 +12,24 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Get build variables
|
- uses: actions/checkout@v2
|
||||||
id: build_info
|
- uses: actions/setup-python@v2
|
||||||
run: |
|
- uses: actions/setup-node@v2
|
||||||
version=`grep -E '^#define EMSESP_APP_VERSION' ./src/version.h | awk -F'"' '{print $2}'`
|
with:
|
||||||
echo "::set-output name=version::$version"
|
node-version: '16'
|
||||||
platform=`grep -E '^#define EMSESP_PLATFORM' ./src/version.h | awk -F'"' '{print $2}'`
|
|
||||||
echo "::set-output name=platform::$platform"
|
|
||||||
|
|
||||||
- name: Compile locally
|
- name: Install PlatformIO
|
||||||
run: make
|
|
||||||
|
|
||||||
- name: Setup Python
|
|
||||||
uses: actions/setup-python@v2
|
|
||||||
|
|
||||||
- name: Install pio
|
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
pip install -U platformio
|
pip install -U platformio
|
||||||
platformio upgrade
|
platformio upgrade
|
||||||
platformio update
|
platformio update
|
||||||
|
|
||||||
- name: Build web
|
- name: Build WebUI
|
||||||
run: |
|
run: |
|
||||||
cd interface
|
cd interface
|
||||||
npm install
|
npm ci
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
- name: Build firmware
|
- name: Build firmware
|
||||||
|
|||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -25,6 +25,8 @@ emsesp
|
|||||||
/data/www
|
/data/www
|
||||||
/lib/framework/WWWData.h
|
/lib/framework/WWWData.h
|
||||||
/interface/build
|
/interface/build
|
||||||
/interface/node_modules
|
node_modules
|
||||||
/interface/.eslintcache
|
/interface/.eslintcache
|
||||||
|
test.sh
|
||||||
|
scripts/__pycache__
|
||||||
|
.temp
|
||||||
|
|||||||
181
CHANGELOG.md
181
CHANGELOG.md
@@ -5,7 +5,178 @@ 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.0.1] March 30 2021
|
# [3.3.1] January 20 2022
|
||||||
|
|
||||||
|
- lastcode broke MQTT JSON structure [#228](https://github.com/emsesp/EMS-ESP32/issues/228)
|
||||||
|
- overlapping while reading sequence of EMS1.0 telegrams
|
||||||
|
- redundant telegram readings (because of offset overflow)
|
||||||
|
- added missing RC30/Moduline400 [#243](https://github.com/emsesp/EMS-ESP32/issues/243)
|
||||||
|
- check received status before toggling fetch on empty telegram [#268][#282]
|
||||||
|
|
||||||
|
# [3.3.0] November 28 2021
|
||||||
|
|
||||||
|
## Added
|
||||||
|
|
||||||
|
- Add system commands for syslog level and watch [#98](https://github.com/emsesp/EMS-ESP32/issues/98)
|
||||||
|
- Added pool data to telegrams 0x494 & 0x495 [#102](https://github.com/emsesp/EMS-ESP32/issues/102)
|
||||||
|
- Add RC300 second summermode telegram [#108](https://github.com/emsesp/EMS-ESP32/issues/108)
|
||||||
|
- Add support for the RC25 thermostat [#106](https://github.com/emsesp/EMS-ESP32/issues/106)
|
||||||
|
- Add new command 'entities' for a device, e.g. http://ems-esp/api/boiler/entities to show the shortname, description and HA Entity name (if HA enabled) [#116](https://github.com/emsesp/EMS-ESP32/issues/116)
|
||||||
|
- Support for Junkers program and remote (fb10/fb110) temperature
|
||||||
|
- Home Assistant `state_class` attribute for Wh, kWh, W and KW [#129](https://github.com/emsesp/EMS-ESP32/issues/129)
|
||||||
|
- Add current room influence for RC300 [#136](https://github.com/emsesp/EMS-ESP32/issues/136)
|
||||||
|
- Added Home Assistant device_class to sensor entities
|
||||||
|
- Added another Buderus RC10 thermostat with Product ID 65 [#160](https://github.com/emsesp/EMS-ESP32/issues/160)
|
||||||
|
- Added support for mDNS [#161](https://github.com/emsesp/EMS-ESP32/issues/161)
|
||||||
|
- Added last system ESP32 reset code to log (and `system info` output)
|
||||||
|
- Firmware Checker in WebUI [#168](https://github.com/emsesp/EMS-ESP32/issues/168)
|
||||||
|
- Added new MQTT setting for enabling 'response' topic
|
||||||
|
- Support for non-standard Thermostats like Tado [#174](https://github.com/emsesp/EMS-ESP32/issues/174)
|
||||||
|
- Include MQTT connection status in 'api/system/info'
|
||||||
|
- Include Network status in 'api/system/info' and also the MQTT topic `info` [#202](https://github.com/emsesp/EMS-ESP32/issues/202)
|
||||||
|
- Added Ethernet PHY module as an option in the Board Profile [#210](https://github.com/emsesp/EMS-ESP32/issues/210)
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
|
||||||
|
- MQTT reconnecting after WiFi reconnect [#99](https://github.com/emsesp/EMS-ESP32/issues/99)
|
||||||
|
- Manually Controlling Solar Circuit [#107](https://github.com/emsesp/EMS-ESP32/issues/107)
|
||||||
|
- Fix thermostat commands not defaulting to the master thermostat [#110](https://github.com/emsesp/EMS-ESP32/issues/110)
|
||||||
|
- Enlarge parse-buffer for long names like `cylinderpumpmodulation`
|
||||||
|
- MQTT not subscribing to all device entities [#166](https://github.com/emsesp/EMS-ESP32/issues/166)
|
||||||
|
- Help fix issues with WebUI unable to fully load UI over Ethernet [#177](https://github.com/emsesp/EMS-ESP32/issues/177)
|
||||||
|
- Shower alert never reset after limit reached when enabled [(PR #185)]
|
||||||
|
- Remove HA entity entries when a device value goes dormant [#196](https://github.com/emsesp/EMS-ESP32/issues/196)
|
||||||
|
- deciphering last error code dates on 0xC2 telegram [#204](https://github.com/emsesp/EMS-ESP32/issues/204)
|
||||||
|
|
||||||
|
## Changed
|
||||||
|
|
||||||
|
- Syslog BOM only for utf-8 messages [#91](https://github.com/emsesp/EMS-ESP32/issues/91)
|
||||||
|
- Check for KM200 by device-id 0x48, remove tx-delay [#90](https://github.com/emsesp/EMS-ESP32/issues/90)
|
||||||
|
- rename `fastheatupfactor` to `fastheatup` and add percent [#122](https://github.com/emsesp/EMS-ESP32/issues/122)
|
||||||
|
- "unit" renamed to "uom" in API call to recall a Device Value
|
||||||
|
- initial backend React changes to replace the class components (HOCs) with React Hooks
|
||||||
|
- Use program-names instead of numbers
|
||||||
|
- Boiler's maintenancemessage always published in MQTT (to prevent HA missing entity)
|
||||||
|
- Unit of Measure 'times' added to MQTT Fails, Rx fails, Rx received, Tx fails, Tx reads & Tx writes
|
||||||
|
- Improved API. Restful HTTP API works in the same way as MQTT calls
|
||||||
|
- Removed settings for MQTT subscribe format [#173](https://github.com/emsesp/EMS-ESP32/issues/173)
|
||||||
|
- Improve Nefit Moduline 200 functionality [#183](https://github.com/emsesp/EMS-ESP32/issues/183)
|
||||||
|
- `status` in the MQTT heartbeat renamed to `bus_status`
|
||||||
|
- Layout changes in the WebUI, showing stripped table rows in Dashboard
|
||||||
|
- Alternative font for log window [#219](https://github.com/emsesp/EMS-ESP32/issues/219)
|
||||||
|
|
||||||
|
## **BREAKING CHANGES**
|
||||||
|
|
||||||
|
- API: "unit" renamed to "uom" in API call to recall a Device Value
|
||||||
|
- HA: `sensor.boiler_boiler_temperature` renamed to `sensor.actual_boiler_temperature`
|
||||||
|
- HA: `binary_sensor.boiler_ww_disinfecting` renamed to `binary_sensor.boiler_ww_disinfection`
|
||||||
|
- HA: # removed from counts in MQTT Fails, Rx fails, Rx received, Tx fails, Tx reads & Tx writes
|
||||||
|
- `txread` renamed to `txreads` and `txwrite` renamed to `txwrites` in MQTT heartbeat payload
|
||||||
|
- 'dallas sensors' in api/system/info moved to the "System" section. Renamed "uptime (seconds)" and "reset reason"
|
||||||
|
- `status` in the MQTT heartbeat renamed to `bus_status`
|
||||||
|
|
||||||
|
# [3.2.1] August 8 2021
|
||||||
|
|
||||||
|
## Added
|
||||||
|
|
||||||
|
- json body in API can now take device, name, cmd, hc and id
|
||||||
|
- added example of how to use API directly to control values from Home Assistant
|
||||||
|
- API calls are shown in debug log (For troubleshooting)
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
|
||||||
|
- fixed issue with Home Assistant entity naming where boiler's ww was duplicated in entity name
|
||||||
|
- fixed issue where wwSetTemp was written too instead of wwSelTemp
|
||||||
|
|
||||||
|
## Changed
|
||||||
|
|
||||||
|
- fixed case on mqtt names, like 'wwtankmiddletemp'
|
||||||
|
- renamed Product ID to 'EMS Product ID' in Home Assistant
|
||||||
|
- removed brackets around tags, e.g. (hc1) selected room temperature" is now just "hc1 selected room temperature"
|
||||||
|
|
||||||
|
# [3.2.0] August 6 2021
|
||||||
|
|
||||||
|
## Added
|
||||||
|
|
||||||
|
- support for IPv6 (web/api/mqtt, not syslog yet) [#83](https://github.com/emsesp/EMS-ESP32/issues/83)
|
||||||
|
- System Log in Web UI will show current time if the NTP Service is enabled [#82](https://github.com/emsesp/EMS-ESP32/issues/82)
|
||||||
|
- Network settings for Tx-power, WiFi-bandwidth, WiFi-sleepmode [#83](https://github.com/emsesp/EMS-ESP32/issues/83)
|
||||||
|
- optional low CPU clockrate (160 MHz) [#83](https://github.com/emsesp/EMS-ESP32/issues/83)
|
||||||
|
- select format for enumerated values in web
|
||||||
|
- settings for water hysteresis on/off
|
||||||
|
- dallas sensor name editable. `sensorname` console-command, replace sensorid with a unique name [#84](https://github.com/emsesp/EMS-ESP32/issues/84)
|
||||||
|
- 'restart' system command. Can be invoked via API with authentication. [#87](https://github.com/emsesp/EMS-ESP32/issues/87)
|
||||||
|
- add Download button in Web UI for log
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
|
||||||
|
- set mode allow numbers
|
||||||
|
- Junkers thermostat shows mode as selected by set_mode
|
||||||
|
- HA thermostat mode if bool-format: numbers is selected
|
||||||
|
- Web UI System Log sometimes skipped a few log messages when watching real-time
|
||||||
|
- fix wwactivated [#89](https://github.com/emsesp/EMS-ESP32/issues/89)
|
||||||
|
- don't show commands (like reset) as Device values in the Web or Console
|
||||||
|
|
||||||
|
## Changed
|
||||||
|
|
||||||
|
- removed Rx echo failures counting as incomplete telegrams. Bad telegrams show as Warning and not Errors. [#80](https://github.com/emsesp/EMS-ESP32/issues/80)
|
||||||
|
- add upload_sec to `api/system/info` and removed # from some names to keep consistent with MQTT heartbeat
|
||||||
|
- added debug target to PlatformIO build to help hunt down system crashes
|
||||||
|
- enumerated values always start at zero
|
||||||
|
- maintenance settings for time/date as extra setting
|
||||||
|
- move api/mqtt formats to `settings`, add `enum format`
|
||||||
|
- UI improvements for editing Dallas Sensor details
|
||||||
|
- RESTful GET commands can also require authentication (via bearer access token) for better security
|
||||||
|
- Updated AsyncMqttClient to 0.9.0 and ArduinoJson to 6.18.3
|
||||||
|
- Download buttons for settings and info under the Help tab
|
||||||
|
|
||||||
|
# [3.1.1] June 26 2021
|
||||||
|
|
||||||
|
## Added
|
||||||
|
|
||||||
|
- new command called `commands` which lists all available commands. `ems-esp/api/{device}/commands`
|
||||||
|
- More Home Assistant icons to match the UOMs
|
||||||
|
- new API. Using secure access tokens and OpenAPI standard. See `doc/EMS-ESP32 API.md` and [#50](https://github.com/emsesp/EMS-ESP32/issues/50)
|
||||||
|
- show log messages in Web UI [#71](https://github.com/emsesp/EMS-ESP32/issues/71)
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
|
||||||
|
- HA thermostat mode was not in sync with actual mode [#66](https://github.com/emsesp/EMS-ESP32/issues/66)
|
||||||
|
- Don't publish rssi if Wifi is disabled and ethernet is being used
|
||||||
|
- Booleans are shown as true/false in API GETs
|
||||||
|
|
||||||
|
## Changed
|
||||||
|
|
||||||
|
- `info` command always shows full names in API. For short names query the device or name directly, e.g. `http://ems-esp/api/boiler`
|
||||||
|
- free memory is shown in kilobytes
|
||||||
|
- boiler's warm water entities have ww added to the Home Assistant entity name [#67](https://github.com/emsesp/EMS-ESP32/issues/67)
|
||||||
|
- improved layout and rendering of device values in the WebUI, also the edit value screen
|
||||||
|
|
||||||
|
# [3.1.0] May 4 2021
|
||||||
|
|
||||||
|
## Added
|
||||||
|
|
||||||
|
- Mock API to simulate an ESP, for testing web
|
||||||
|
- Able to write values from the Web UI
|
||||||
|
- check values with `"cmd":<valuename>` and data empty or `?`
|
||||||
|
- set hc for values and commands by id or prefix `hc<x>`+separator, separator can be any char
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
|
||||||
|
- Don't create Home Assistant MQTT discovery entries for device values that don't exists (#756 on EMS-ESP repo)
|
||||||
|
- Update shower MQTT when a shower start is detected
|
||||||
|
- S32 board profile
|
||||||
|
|
||||||
|
## Changed
|
||||||
|
|
||||||
|
- Icon for Network
|
||||||
|
- MQTT Formatting payload (nested vs single) is a pull-down option
|
||||||
|
- moved mqtt-topics and texts to local_EN, all topics lower case
|
||||||
|
- Re-enabled Shower Alert (still experimental)
|
||||||
|
- lowercased Flow temp in commands
|
||||||
|
- system console commands to main
|
||||||
|
|
||||||
|
# [3.0.1] March 30 2021
|
||||||
|
|
||||||
## Added
|
## Added
|
||||||
|
|
||||||
@@ -63,9 +234,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
- Shower Alert (disabled for now)
|
- Shower Alert (disabled for now)
|
||||||
|
|
||||||
## [3.0.0] March 18 2021
|
# [3.0.0] March 18 2021
|
||||||
|
|
||||||
### Added
|
## Added
|
||||||
|
|
||||||
- Power settings, disabling BLE and turning off Wifi sleep
|
- Power settings, disabling BLE and turning off Wifi sleep
|
||||||
- Rx and Tx counts to Heartbeat MQTT payload
|
- Rx and Tx counts to Heartbeat MQTT payload
|
||||||
@@ -82,7 +253,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- RC300 `thermostat temp -1` to clear temporary setpoint in auto mode
|
- RC300 `thermostat temp -1` to clear temporary setpoint in auto mode
|
||||||
- Syslog port selectable (#744)
|
- Syslog port selectable (#744)
|
||||||
|
|
||||||
### Fixed
|
## Fixed
|
||||||
|
|
||||||
- telegrams matched to masterthermostat 0x18
|
- telegrams matched to masterthermostat 0x18
|
||||||
- multiple roomcontrollers
|
- multiple roomcontrollers
|
||||||
@@ -93,7 +264,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- wrong position of values #723, #732
|
- wrong position of values #723, #732
|
||||||
- OTA Upload via Web on OSX
|
- OTA Upload via Web on OSX
|
||||||
|
|
||||||
### Changed
|
## Changed
|
||||||
|
|
||||||
- changed how telegram parameters are rendered for mqtt, console and web (#632)
|
- changed how telegram parameters are rendered for mqtt, console and web (#632)
|
||||||
- split `show values` in smaller packages (edited)
|
- split `show values` in smaller packages (edited)
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
# Changelog
|
|
||||||
|
|
||||||
## Added
|
|
||||||
|
|
||||||
## Fixed
|
|
||||||
|
|
||||||
## Changed
|
|
||||||
|
|
||||||
## Removed
|
|
||||||
|
|||||||
@@ -24,17 +24,17 @@ This document describes rules that are in effect for this repository, meant for
|
|||||||
|
|
||||||
## Triaging of Issues/PR's
|
## Triaging of Issues/PR's
|
||||||
|
|
||||||
1. Any contributor to the project can participate in the triaging process, if he/she chooses to do so.
|
1. Any contributor to the project can participate in the triaging process, if he/she chooses to do so.
|
||||||
2. An issue that needs to be closed, either due to not complying with this policy, or for other reasons, should be closed by a contributor.
|
2. An issue that needs to be closed, either due to not complying with this policy, or for other reasons, should be closed by a contributor.
|
||||||
3. Issues that are accepted should be marked with appropriate labels.
|
3. Issues that are accepted should be marked with appropriate labels.
|
||||||
4. Issues that could impact functionality for many users should be considered severe.
|
4. Issues that could impact functionality for many users should be considered severe.
|
||||||
5. Issues caused by the SDK or chip should not be marked severe, as there usually isn’t much to be done. Common sense should be applied when deciding. Such issues should be documented in the documentation, for reference by users.
|
5. Issues caused by the SDK or chip should not be marked severe, as there usually isn’t much to be done. Common sense should be applied when deciding. Such issues should be documented in the documentation, for reference by users.
|
||||||
6. Issues with feature requests should be discussed for viability/desirability.
|
6. Issues with feature requests should be discussed for viability/desirability.
|
||||||
7. Feature requests or changes that are meant to address a very specific/limited use case, especially if at the expense of increased code complexity, may be denied, or may be required to be redesigned, generalized, or simplified.
|
7. Feature requests or changes that are meant to address a very specific/limited use case, especially if at the expense of increased code complexity, may be denied, or may be required to be redesigned, generalized, or simplified.
|
||||||
8. Feature requests that are not accompanied by a PR:
|
8. Feature requests that are not accompanied by a PR:
|
||||||
* could be closed immediately (denied).
|
- could be closed immediately (denied).
|
||||||
* could be closed after some predetermined period of time (left as candidate for somebody to pick up).
|
- could be closed after some predetermined period of time (left as candidate for somebody to pick up).
|
||||||
9. In some cases, feedback may be requested from the issue reporter, either as additional info for clarification, additional testing, or other. If no feedback is provided, the issue may be closed by a contributor or after 40 days by the STALE bot.
|
9. In some cases, feedback may be requested from the issue reporter, either as additional info for clarification, additional testing, or other. If no feedback is provided, the issue may be closed by a contributor or after 40 days by the STALE bot.
|
||||||
|
|
||||||
## Pull requests
|
## Pull requests
|
||||||
|
|
||||||
@@ -42,24 +42,24 @@ A Pull Request (PR) is the process where code modifications are managed in GitHu
|
|||||||
|
|
||||||
The process is straight-forward.
|
The process is straight-forward.
|
||||||
|
|
||||||
- Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#best-practices-for-faster-reviews) by Kubernetes (but skip step 0)
|
- Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#best-practices-for-faster-reviews) by Kubernetes (but skip step 0)
|
||||||
- Fork the EMS-ESP Repository [git repository](https://github.com/emsesp/EMS-ESP32).
|
- Fork the EMS-ESP Repository [git repository](https://github.com/emsesp/EMS-ESP32).
|
||||||
- Write/Change the code in your Fork for a new feature, bug fix, new sensor, optimization, etc.
|
- Write/Change the code in your Fork for a new feature, bug fix, new sensor, optimization, etc.
|
||||||
- Ensure tests work.
|
- Ensure tests work.
|
||||||
- Create a Pull Request against the [**dev**](https://github.com/emsesp/EMS-ESP32/tree/dev) branch of EMS-ESP.
|
- Create a Pull Request against the [**dev**](https://github.com/emsesp/EMS-ESP32/tree/dev) branch of EMS-ESP.
|
||||||
|
|
||||||
1. All pull requests must be done against the dev branch.
|
1. All pull requests must be done against the dev branch.
|
||||||
2. Make sure code is formatting per the `.clang-format`
|
2. Make sure code is formatting per the `.clang-format`.
|
||||||
3. Only relevant files should be touched (Also beware if your editor has auto-formatting feature enabled).
|
3. Make sure any new code is clearly commented explaining what the function/logic does.
|
||||||
4. Only one feature/fix should be added per PR.
|
4. Only relevant files should be touched (Also beware if your editor has auto-formatting feature enabled).
|
||||||
5. PRs that don't compile (fail in CI Tests) or cause coding errors will not be merged. Please fix the issue. Same goes for PRs that are raised against older commit in dev - you might need to rebase and resolve conflicts.
|
5. Only one feature/fix should be added per PR.
|
||||||
6. All pull requests should undergo peer review by at least one contributor other than the creator, excepts for the owner.
|
6. PRs that don't compile (fail in CI Tests) or cause coding errors will not be merged. Please fix the issue. Same goes for PRs that are raised against older commit in dev - you might need to rebase and resolve conflicts.
|
||||||
7. All pull requests should consider updates to the documentation.
|
7. All pull requests should undergo peer review by at least one contributor other than the creator, excepts for the owner.
|
||||||
8. Pull requests that address an outstanding issue, particularly an issue deemed to be severe, should be given priority.
|
8. All pull requests should consider updates to the documentation.
|
||||||
9. If a PR is accepted, then it should undergo review and updated based on the feedback provided, then merged.
|
9. Pull requests that address an outstanding issue, particularly an issue deemed to be severe, should be given priority.
|
||||||
10. By submitting a PR, it is needed to use the provided PR template and check all boxes, performing the required tasks and accepting the CLA.
|
10. If a PR is accepted, then it should undergo review and updated based on the feedback provided, then merged.
|
||||||
11. Pull requests that don't meet the above will be denied and closed.
|
11. By submitting a PR, it is needed to use the provided PR template and check all boxes, performing the required tasks and accepting the CLA.
|
||||||
|
12. Pull requests that don't meet the above will be denied and closed.
|
||||||
|
|
||||||
## Semantic Commit Messages
|
## Semantic Commit Messages
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ More Examples:
|
|||||||
|
|
||||||
References:
|
References:
|
||||||
|
|
||||||
- https://www.conventionalcommits.org/
|
- <https://www.conventionalcommits.org/>
|
||||||
|
|
||||||
--------------------------------------
|
--------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ MAKEFLAGS+="j "
|
|||||||
#TARGET := $(notdir $(CURDIR))
|
#TARGET := $(notdir $(CURDIR))
|
||||||
TARGET := emsesp
|
TARGET := emsesp
|
||||||
BUILD := build
|
BUILD := build
|
||||||
SOURCES := src lib_standalone lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src src/devices lib/ArduinoJson/src lib/PButton src/test
|
SOURCES := src src/* lib_standalone lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src src/devices lib/ArduinoJson/src lib/PButton
|
||||||
INCLUDES := lib/ArduinoJson/src lib_standalone lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src lib/uuid-telnet/src lib/uuid-syslog/src lib/PButton src/devices lib src
|
INCLUDES := src lib_standalone lib/ArduinoJson/src lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src lib/uuid-telnet/src lib/uuid-syslog/src lib/* src/devices
|
||||||
LIBRARIES :=
|
LIBRARIES :=
|
||||||
|
|
||||||
CPPCHECK = cppcheck
|
CPPCHECK = cppcheck
|
||||||
@@ -33,7 +33,7 @@ CXX_STANDARD := -std=c++11
|
|||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
# Defined Symbols
|
# Defined Symbols
|
||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
DEFINES += -DARDUINOJSON_ENABLE_STD_STRING=1 -DARDUINOJSON_ENABLE_ARDUINO_STRING -DEMSESP_DEBUG -DEMSESP_STANDALONE -DEMSESP_TEST -DEMSESP_DEFAULT_BOARD_PROFILE=\"LOLIN\"
|
DEFINES += -DARDUINOJSON_ENABLE_STD_STRING=1 -DARDUINOJSON_ENABLE_ARDUINO_STRING -DARDUINOJSON_USE_DOUBLE -DEMSESP_DEBUG -DEMSESP_STANDALONE -DEMSESP_DEFAULT_BOARD_PROFILE=\"LOLIN\"
|
||||||
|
|
||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
# Sources & Files
|
# Sources & Files
|
||||||
@@ -72,9 +72,9 @@ CPPFLAGS += -g3
|
|||||||
CPPFLAGS += -Os
|
CPPFLAGS += -Os
|
||||||
|
|
||||||
CFLAGS += $(CPPFLAGS)
|
CFLAGS += $(CPPFLAGS)
|
||||||
# CFLAGS += -Wall
|
CFLAGS += -Wall
|
||||||
# CFLAGS += -Wno-unused -Wno-restrict
|
CFLAGS += -Wno-unused -Wno-restrict
|
||||||
# CFLAGS += -Wextra
|
CFLAGS += -Wextra
|
||||||
|
|
||||||
CXXFLAGS += $(CFLAGS) -MMD
|
CXXFLAGS += $(CFLAGS) -MMD
|
||||||
|
|
||||||
113
README.md
113
README.md
@@ -2,16 +2,21 @@
|
|||||||
|
|
||||||
**EMS-ESP** is an open-source firmware for the Espressif ESP8266 and ESP32 microcontroller that communicates with **EMS** (Energy Management System) based equipment from manufacturers like Bosch, Buderus, Nefit, Junkers, Worcester and Sieger.
|
**EMS-ESP** is an open-source firmware for the Espressif ESP8266 and ESP32 microcontroller that communicates with **EMS** (Energy Management System) based equipment from manufacturers like Bosch, Buderus, Nefit, Junkers, Worcester and Sieger.
|
||||||
|
|
||||||
This is the firmware for the ESP32.
|
This project is the specifically for the ESP32. Compared with the previous ESP8266 (version 2) release it has the following enhancements:
|
||||||
|
|
||||||
|
- Ethernet Support
|
||||||
|
- Pre-configured circuit board layouts
|
||||||
|
- Supports writing EMS values directly from within Web UI
|
||||||
|
- Mock API server for faster offline development and testing
|
||||||
|
- Improved API and MQTT commands
|
||||||
|
- Improvements to Dallas temperature sensors
|
||||||
|
- Embedded log tracing in the Web UI
|
||||||
|
|
||||||
[](https://github.com/emsesp/EMS-ESP32/blob/main/CHANGELOG.md)
|
[](https://github.com/emsesp/EMS-ESP32/blob/main/CHANGELOG.md)
|
||||||
[](https://github.com/emsesp/EMS-ESP32/commits/main)
|
[](https://github.com/emsesp/EMS-ESP32/commits/main)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
[](https://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://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://github.com/emsesp/EMS-ESP32/releases)
|
[](https://github.com/emsesp/EMS-ESP32/releases)
|
||||||
[](http://isitmaintained.com/project/emsesp/EMS-ESP32 "Average time to resolve an issue")
|
|
||||||
[](http://isitmaintained.com/project/emsesp/EMS-ESP32 "Percentage of issues still open")
|
|
||||||
<br/>
|
|
||||||
[](https://discord.gg/3J3GgnzpyT)
|
[](https://discord.gg/3J3GgnzpyT)
|
||||||
|
|
||||||
If you like **EMS-ESP**, please give it a star, or fork it and contribute!
|
If you like **EMS-ESP**, please give it a star, or fork it and contribute!
|
||||||
@@ -20,82 +25,104 @@ If you like **EMS-ESP**, please give it a star, or fork it and contribute!
|
|||||||
[](https://github.com/emsesp/EMS-ES32P/network)
|
[](https://github.com/emsesp/EMS-ES32P/network)
|
||||||
[](https://www.paypal.com/paypalme/prderbyshire/2)
|
[](https://www.paypal.com/paypalme/prderbyshire/2)
|
||||||
|
|
||||||
Note, EMS-ESP requires a small hardware circuit that can convert the EMS bus data to be read by the microcontroller. These can be ordered at https://bbqkees-electronics.nl.
|
Note, EMS-ESP requires a small hardware circuit that can convert the EMS bus data to be read by the microcontroller. These can be ordered at <https://bbqkees-electronics.nl> or contact the contributors that can provide the schematic and designs.
|
||||||
|
|
||||||
<img src="media/gateway-integration.jpg" width=40%>
|
<img src="media/gateway-integration.jpg" width=40%>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## **Features**
|
# **Features**
|
||||||
|
|
||||||
- Compatible with both ESP8266 and ESP32
|
|
||||||
- A multi-user secure web interface to change settings and monitor the data
|
- A multi-user secure web interface to change settings and monitor the data
|
||||||
- A console, accessible via Serial and Telnet for more monitoring
|
- A console, accessible via Serial and Telnet for more monitoring
|
||||||
- Native support for Home Assistant via [MQTT Discovery](https://www.home-assistant.io/docs/mqtt/discovery/)
|
- Native support for Home Assistant 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
|
- Can run standalone as an independent WiFi Access Point or join an existing WiFi network
|
||||||
- Easy first-time configuration via a web Captive Portal
|
- Easy first-time configuration via a web Captive Portal
|
||||||
- Support for more than [70 EMS devices](https://emsesp.github.io/docs/#/Supported-EMS-Devices) (boilers, thermostats, solar modules, mixer modules, heat pumps, gateways)
|
- Support for more than [80 EMS devices](https://emsesp.github.io/docs/#/Supported-EMS-Devices) (boilers, thermostats, solar modules, mixer modules, heat pumps, gateways)
|
||||||
|
|
||||||
## **Screenshots**
|
## **Demo**
|
||||||
|
|
||||||
### Web Interface:
|
See a live demo [here](https://ems-esp.derbyshire.nl) using fake data. Log in with any username/password.
|
||||||
|
|
||||||
| | |
|
# **Screenshots**
|
||||||
| --- | --- |
|
|
||||||
| <img src="media/web_settings.PNG"> | <img src="media/web_status.PNG"> |
|
|
||||||
| <img src="media/web_devices.PNG"> | <img src="media/web_mqtt.PNG"> |
|
|
||||||
|
|
||||||
### Telnet Console:
|
## Web Interface
|
||||||
<img src="media/console.PNG" width=80% height=80%>
|
|
||||||
|
|
||||||
### In Home Assistant:
|
| | |
|
||||||
<img src="media/ha_lovelace.PNG" width=80% height=80%>
|
| ---------------------------------- | -------------------------------- |
|
||||||
|
| <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"> |
|
||||||
|
|
||||||
## **Installing**
|
## Telnet Console
|
||||||
|
|
||||||
|
<img src="media/console.png" width=80% height=80%>
|
||||||
|
|
||||||
|
## In Home Assistant
|
||||||
|
|
||||||
|
<img src="media/ha_lovelace.png" width=80% height=80%>
|
||||||
|
|
||||||
|
# **Installing**
|
||||||
|
|
||||||
Refer to the [official documentation](https://emsesp.github.io/docs) to how to install the firmware and configure it. The documentation is being constantly updated as new features and settings are added.
|
Refer to the [official documentation](https://emsesp.github.io/docs) to how to install the firmware and configure it. The documentation is being constantly updated as new features and settings are added.
|
||||||
|
|
||||||
You can choose to use an pre-built firmware image or compile the code yourself:
|
You can choose to use an pre-built firmware image or compile the code yourself:
|
||||||
|
|
||||||
* [Uploading a pre-built firmware build](https://emsesp.github.io/docs/#/Uploading-firmware)
|
- [Uploading a pre-built firmware build](https://emsesp.github.io/docs/#/Uploading-firmware)
|
||||||
* [Building the firmware from source code and flashing manually](https://emsesp.github.io/docs/#/Building-firmware)
|
- [Building the firmware from source code and flashing manually](https://emsesp.github.io/docs/#/Building-firmware)
|
||||||
|
|
||||||
## **Support Information**
|
# **Support Information**
|
||||||
|
|
||||||
If you're looking for support on **EMS-ESP** there are some options available:
|
If you're looking for support on **EMS-ESP** there are some options available:
|
||||||
|
|
||||||
### Documentation
|
## Documentation
|
||||||
|
|
||||||
* [Official EMS-ESP Documentation](https://emsesp.github.io/docs): For information on how to build and upload the firmware
|
- [Official EMS-ESP Documentation](https://emsesp.github.io/docs): For information on how to build and upload the firmware
|
||||||
* [FAQ and Troubleshooting](https://emsesp.github.io/docs/#/Troubleshooting): For information on common problems and solutions. See also [BBQKees's wiki](https://bbqkees-electronics.nl/wiki/gateway/troubleshooting.html)
|
- [FAQ and Troubleshooting](https://emsesp.github.io/docs/#/Troubleshooting): For information on common problems and solutions. See also [BBQKees's wiki](https://bbqkees-electronics.nl/wiki/gateway/troubleshooting.html)
|
||||||
|
|
||||||
### Support Community
|
## Support Community
|
||||||
|
|
||||||
* [Discord Server](https://discord.gg/3J3GgnzpyT): For support, troubleshooting and general questions. You have better chances to get fast answers from members of the community
|
- [Discord Server](https://discord.gg/3J3GgnzpyT): For support, troubleshooting and general questions. You have better chances to get fast answers from members of the community
|
||||||
* [Search in Issues](https://github.com/emsesp/EMS-ESP32/issues): You might find an answer to your question by searching current or closed issues
|
- [Search in Issues](https://github.com/emsesp/EMS-ESP32/issues): You might find an answer to your question by searching current or closed issues
|
||||||
|
|
||||||
### Developer's Community
|
## Developer's Community
|
||||||
|
|
||||||
* [Bug Report](https://github.com/emsesp/EMS-ESP32/issues/new?template=bug_report.md): For reporting Bugs
|
- [Bug Report](https://github.com/emsesp/EMS-ESP32/issues/new?template=bug_report.md): For reporting Bugs
|
||||||
* [Feature Request](https://github.com/emsesp/EMS-ESP32/issues/new?template=feature_request.md): For requesting features/functions
|
- [Feature Request](https://github.com/emsesp/EMS-ESP32/issues/new?template=feature_request.md): For requesting features/functions
|
||||||
* [Troubleshooting](https://github.com/emsesp/EMS-ESP32/issues/new?template=questions---troubleshooting.md): As a last resort, you can open new *Troubleshooting & Question* issue on GitHub if the solution could not be found using the other channels. Just remember: the more info you provide the more chances you'll have to get an accurate answer
|
- [Troubleshooting](https://github.com/emsesp/EMS-ESP32/issues/new?template=questions---troubleshooting.md): As a last resort, you can open new _Troubleshooting & Question_ issue on GitHub if the solution could not be found using the other channels. Just remember: the more info you provide the more chances you'll have to get an accurate answer
|
||||||
|
|
||||||
## **Contributing**
|
# **Contributors ✨**
|
||||||
|
|
||||||
|
EMS-ESP is a project originally created and owned by [proddy](https://github.com/proddy). Key contributors are:
|
||||||
|
|
||||||
|
<!-- prettier-ignore-start -->
|
||||||
|
<!-- markdownlint-disable -->
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/MichaelDvP"><img src="https://avatars.githubusercontent.com/u/59284019?v=3?s=100" width="100px;" alt=""/><br /><sub><b>MichaelDvP</b></sub></a><br /></a> <a href="https://github.com/emsesp/EMS-ESP/commits?author=MichaelDvP" title="v2 Commits">v2</a>
|
||||||
|
<a href="https://github.com/emsesp/EMS-ESP32/commits?author=MichaelDvP" title="v3 Commits">v3</a>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<!-- markdownlint-restore -->
|
||||||
|
<!-- prettier-ignore-end -->
|
||||||
|
|
||||||
|
You can also contribute to EMS-ESP by
|
||||||
|
|
||||||
You can contribute to EMS-ESP by
|
|
||||||
- providing Pull Requests (Features, Fixes, suggestions)
|
- providing Pull Requests (Features, Fixes, suggestions)
|
||||||
- testing new released features and report issues on your EMS equipment
|
- testing new released features and report issues on your EMS equipment
|
||||||
- contributing to missing [Documentation](https://emsesp.github.io/docs)
|
- contributing to missing [Documentation](https://emsesp.github.io/docs)
|
||||||
|
|
||||||
## **Credits**
|
# **Libraries used**
|
||||||
|
|
||||||
A shout out to the people helping EMS-ESP get to where it is today...
|
- [esp8266-react](https://github.com/rjwats/esp8266-react) by @rjwats for the framework that provides the core of the Web UI
|
||||||
- **@MichaelDvP** for all his amazing contributions and patience. Specifically for the improved uart library, thermostat and mixer logic.
|
- [uuid-\*](https://github.com/nomis/mcu-uuid-console) from @nomis. The console, syslog, telnet and logging are based off these open source libraries
|
||||||
- **@BBQKees** for his endless testing and building the awesome circuit boards
|
- [ArduinoJson](https://github.com/bblanchon/ArduinoJson) for JSON
|
||||||
- **@rjwats** for his [esp8266-react](https://github.com/rjwats/esp8266-react) framework that provides the new Web UI
|
- [AsyncMqttClient](https://github.com/marvinroger/async-mqtt-client) for the MQTT client, with custom modifications from @bertmelis and @proddy
|
||||||
- **@nomis** for his core [console](https://github.com/nomis/mcu-uuid-console), telnet and syslog core libraries
|
- ESPAsyncWebServer and AsyncTCP for the Web server and TCP backends, with custom modifications for performance
|
||||||
- plus everyone else providing suggestions, PRs and the odd donation that keeps this project open source. Thanks!
|
|
||||||
|
|
||||||
## **License**
|
# **License**
|
||||||
|
|
||||||
This program is licensed under GPL-3.0
|
This program is licensed under GPL-3.0
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Name, Type, SubType, Offset, Size, Flags
|
# Name, Type, SubType, Offset, Size, Flags
|
||||||
nvs, data, nvs, 0x9000, 0x5000,
|
nvs, data, nvs, 0x9000, 0x5000,
|
||||||
otadata, data, ota, 0xe000, 0x2000,
|
otadata, data, ota, 0xE000, 0x2000,
|
||||||
app0, app, ota_0, 0x10000, 0x1F0000,
|
app0, app, ota_0, 0x10000, 0x1F0000,
|
||||||
app1, app, ota_1, 0x200000, 0x1F0000,
|
app1, app, ota_1, 0x200000, 0x1F0000,
|
||||||
spiffs, data, spiffs, 0x3F0000,0x10000,
|
spiffs, data, spiffs, 0x3F0000, 0x10000,
|
||||||
|
|||||||
|
5
esp32_partition_debug.csv
Normal file
5
esp32_partition_debug.csv
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Name, Type, SubType, Offset, Size, Flags
|
||||||
|
nvs, data, nvs, 0x9000, 0x5000,
|
||||||
|
otadata, data, ota, 0xE000, 0x2000,
|
||||||
|
app0, app, ota_0, 0x10000, 0x210000,
|
||||||
|
spiffs, data, spiffs, 0x220000, 0x10000,
|
||||||
|
@@ -1,11 +1,5 @@
|
|||||||
# Change the IP address to that of your ESP device to enable local development of the UI.
|
# Change the IP address to that of your ESP device to enable local development of the UI
|
||||||
# Remember to also enable CORS in platformio.ini before uploading the code to the device
|
|
||||||
# with -DENABLE_CORS
|
|
||||||
|
|
||||||
# my Wifi
|
# REACT_APP_HTTP_ROOT=http://localhost:3000
|
||||||
#REACT_APP_HTTP_ROOT=http://10.10.10.101
|
# REACT_APP_WEB_SOCKET_ROOT=ws://localhost:3000
|
||||||
#REACT_APP_WEB_SOCKET_ROOT=ws://10.10.10.101
|
|
||||||
|
|
||||||
# my Ethernet
|
|
||||||
REACT_APP_HTTP_ROOT=http://192.168.1.134
|
|
||||||
REACT_APP_WEB_SOCKET_ROOT=ws://http://192.168.1.134
|
|
||||||
|
|||||||
3
interface/.env.hosted
Normal file
3
interface/.env.hosted
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
GENERATE_SOURCEMAP=false
|
||||||
|
|
||||||
|
REACT_APP_HOSTED=true
|
||||||
2
interface/.eslintignore
Normal file
2
interface/.eslintignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# don't ever lint node_modules
|
||||||
|
node_modules
|
||||||
27
interface/.eslintrc
Normal file
27
interface/.eslintrc
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"plugins": [
|
||||||
|
"@typescript-eslint",
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/eslint-recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
// 0 = ignore, 1 = warning, 2 = error
|
||||||
|
"no-console": 0,
|
||||||
|
"prettier/prettier": ["error", { endOfLine: "auto" }],
|
||||||
|
"explicit-function-return-type": 0,
|
||||||
|
"@typescript-eslint/explicit-module-boundary-types": 0,
|
||||||
|
"@typescript-eslint/no-var-requires": 0,
|
||||||
|
"@typescript-eslint/ban-types": 0,
|
||||||
|
"@typescript-eslint/no-non-null-asserted-optional-chain": 0,
|
||||||
|
"@typescript-eslint/no-non-null-assertion": 0,
|
||||||
|
"@typescript-eslint/no-explicit-any": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
6
interface/.prettierrc
Normal file
6
interface/.prettierrc
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"semi": true,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"printWidth": 80
|
||||||
|
}
|
||||||
@@ -4,34 +4,49 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
|||||||
const CompressionPlugin = require('compression-webpack-plugin');
|
const CompressionPlugin = require('compression-webpack-plugin');
|
||||||
const ProgmemGenerator = require('./progmem-generator.js');
|
const ProgmemGenerator = require('./progmem-generator.js');
|
||||||
|
|
||||||
const path = require('path');
|
|
||||||
const fs = require('fs');
|
|
||||||
|
|
||||||
module.exports = function override(config, env) {
|
module.exports = function override(config, env) {
|
||||||
if (env === "production") {
|
const hosted = process.env.REACT_APP_HOSTED;
|
||||||
// rename the ouput file, we need it's path to be short, for SPIFFS
|
|
||||||
|
if (env === 'production' && !hosted) {
|
||||||
|
console.log('Custom webpack...');
|
||||||
|
|
||||||
|
// rename the output file, we need it's path to be short for LittleFS
|
||||||
config.output.filename = 'js/[id].[chunkhash:4].js';
|
config.output.filename = 'js/[id].[chunkhash:4].js';
|
||||||
config.output.chunkFilename = 'js/[id].[chunkhash:4].js';
|
config.output.chunkFilename = 'js/[id].[chunkhash:4].js';
|
||||||
|
|
||||||
// take out the manifest and service worker plugins
|
// take out the manifest and service worker plugins
|
||||||
config.plugins = config.plugins.filter(plugin => !(plugin instanceof ManifestPlugin));
|
config.plugins = config.plugins.filter(
|
||||||
config.plugins = config.plugins.filter(plugin => !(plugin instanceof WorkboxWebpackPlugin.GenerateSW));
|
(plugin) => !(plugin instanceof ManifestPlugin)
|
||||||
|
);
|
||||||
|
config.plugins = config.plugins.filter(
|
||||||
|
(plugin) => !(plugin instanceof WorkboxWebpackPlugin.GenerateSW)
|
||||||
|
);
|
||||||
|
|
||||||
// shorten css filenames
|
// shorten css filenames
|
||||||
const miniCssExtractPlugin = config.plugins.find((plugin) => plugin instanceof MiniCssExtractPlugin);
|
const miniCssExtractPlugin = config.plugins.find(
|
||||||
miniCssExtractPlugin.options.filename = "css/[id].[contenthash:4].css";
|
(plugin) => plugin instanceof MiniCssExtractPlugin
|
||||||
miniCssExtractPlugin.options.chunkFilename = "css/[id].[contenthash:4].c.css";
|
);
|
||||||
|
miniCssExtractPlugin.options.filename = 'css/[id].[contenthash:4].css';
|
||||||
|
miniCssExtractPlugin.options.chunkFilename =
|
||||||
|
'css/[id].[contenthash:4].c.css';
|
||||||
|
|
||||||
// build progmem data files
|
// build progmem data files
|
||||||
config.plugins.push(new ProgmemGenerator({ outputPath: "../lib/framework/WWWData.h", bytesPerLine: 20 }));
|
config.plugins.push(
|
||||||
|
new ProgmemGenerator({
|
||||||
|
outputPath: '../lib/framework/WWWData.h',
|
||||||
|
bytesPerLine: 20
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// add compression plugin, compress javascript
|
// add compression plugin, compress javascript
|
||||||
config.plugins.push(new CompressionPlugin({
|
config.plugins.push(
|
||||||
filename: "[path].gz[query]",
|
new CompressionPlugin({
|
||||||
algorithm: "gzip",
|
filename: '[path].gz[query]',
|
||||||
test: /\.(js)$/,
|
algorithm: 'gzip',
|
||||||
deleteOriginalAssets: true
|
test: /\.(js)$/,
|
||||||
}));
|
deleteOriginalAssets: true
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return config;
|
return config;
|
||||||
}
|
};
|
||||||
|
|||||||
11590
interface/package-lock.json
generated
11590
interface/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,39 +1,47 @@
|
|||||||
{
|
{
|
||||||
"name": "esp8266-react",
|
"name": "emsesp-react",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@material-ui/core": "^4.11.3",
|
"@material-ui/core": "^4.12.3",
|
||||||
"@material-ui/icons": "^4.11.2",
|
"@material-ui/icons": "^4.11.2",
|
||||||
"@types/lodash": "^4.14.168",
|
"@msgpack/msgpack": "^2.7.0",
|
||||||
"@types/node": "^12.20.4",
|
"@types/lodash": "^4.14.172",
|
||||||
"@types/react": "^17.0.3",
|
"@types/node": "^12.20.20",
|
||||||
"@types/react-dom": "^17.0.1",
|
"@types/react": "^17.0.19",
|
||||||
|
"@types/react-dom": "^17.0.9",
|
||||||
"@types/react-material-ui-form-validator": "^2.1.0",
|
"@types/react-material-ui-form-validator": "^2.1.0",
|
||||||
"@types/react-router": "^5.1.12",
|
"@types/react-router": "^5.1.13",
|
||||||
"@types/react-router-dom": "^5.1.6",
|
"@types/react-router-dom": "^5.1.7",
|
||||||
"compression-webpack-plugin": "^4.0.0",
|
"compression-webpack-plugin": "^5.0.2",
|
||||||
|
"env-cmd": "^10.1.0",
|
||||||
|
"express": "^4.17.1",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mime-types": "^2.1.29",
|
"mime-types": "^2.1.30",
|
||||||
"notistack": "^1.0.5",
|
"notistack": "^1.0.6",
|
||||||
"parse-ms": "^2.1.0",
|
"parse-ms": "^3.0.0",
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.2",
|
||||||
"react-dropzone": "^11.3.1",
|
"react-dropzone": "^11.3.2",
|
||||||
"react-form-validator-core": "^1.1.1",
|
"react-form-validator-core": "^1.1.1",
|
||||||
"react-material-ui-form-validator": "^2.1.4",
|
"react-material-ui-form-validator": "^2.1.4",
|
||||||
"react-router": "^5.2.0",
|
"react-router": "^5.2.0",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-scripts": "4.0.1",
|
"react-scripts": "4.0.3",
|
||||||
"sockette": "^2.0.6",
|
"sockette": "^2.0.6",
|
||||||
"typescript": "4.0.5",
|
"typescript": "4.3.5",
|
||||||
"zlib": "^1.0.5"
|
"zlib": "^1.0.5"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-app-rewired start",
|
"start": "react-app-rewired start",
|
||||||
"build": "react-app-rewired build",
|
"build": "react-app-rewired build",
|
||||||
"eject": "react-scripts eject"
|
"format": "prettier --write '**/*.{ts,tsx,js,css,json,md}'",
|
||||||
|
"build-hosted": "env-cmd -f .env.hosted npm run build",
|
||||||
|
"build-localhost": "PUBLIC_URL=/ react-app-rewired build",
|
||||||
|
"mock-api": "nodemon --watch ../mock-api ../mock-api/server.js",
|
||||||
|
"standalone": "npm-run-all -p start mock-api",
|
||||||
|
"lint": "eslint . --ext .ts,.tsx"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": "react-app"
|
"extends": "react-app"
|
||||||
@@ -51,6 +59,13 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"concurrently": "^6.0.1",
|
||||||
|
"eslint-config-prettier": "^8.3.0",
|
||||||
|
"eslint-plugin-prettier": "^3.4.0",
|
||||||
|
"http-proxy-middleware": "^1.1.1",
|
||||||
|
"nodemon": "^2.0.7",
|
||||||
|
"npm-run-all": "^4.1.5",
|
||||||
|
"prettier": "^2.0.5",
|
||||||
"react-app-rewired": "^2.1.8"
|
"react-app-rewired": "^2.1.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,25 @@
|
|||||||
const { resolve, relative, sep } = require('path');
|
const { resolve, relative, sep } = require('path');
|
||||||
const { readdirSync, existsSync, unlinkSync, readFileSync, createWriteStream } = require('fs');
|
const {
|
||||||
|
readdirSync,
|
||||||
|
existsSync,
|
||||||
|
unlinkSync,
|
||||||
|
readFileSync,
|
||||||
|
createWriteStream
|
||||||
|
} = require('fs');
|
||||||
var zlib = require('zlib');
|
var zlib = require('zlib');
|
||||||
var mime = require('mime-types');
|
var mime = require('mime-types');
|
||||||
|
|
||||||
const ARDUINO_INCLUDES = "#include <Arduino.h>\n\n";
|
const ARDUINO_INCLUDES = '#include <Arduino.h>\n\n';
|
||||||
|
|
||||||
function getFilesSync(dir, files = []) {
|
function getFilesSync(dir, files = []) {
|
||||||
readdirSync(dir, { withFileTypes: true }).forEach(entry => {
|
readdirSync(dir, { withFileTypes: true }).forEach((entry) => {
|
||||||
const entryPath = resolve(dir, entry.name);
|
const entryPath = resolve(dir, entry.name);
|
||||||
if (entry.isDirectory()) {
|
if (entry.isDirectory()) {
|
||||||
getFilesSync(entryPath, files);
|
getFilesSync(entryPath, files);
|
||||||
} else {
|
} else {
|
||||||
files.push(entryPath);
|
files.push(entryPath);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,13 +31,17 @@ function cleanAndOpen(path) {
|
|||||||
if (existsSync(path)) {
|
if (existsSync(path)) {
|
||||||
unlinkSync(path);
|
unlinkSync(path);
|
||||||
}
|
}
|
||||||
return createWriteStream(path, { flags: "w+" });
|
return createWriteStream(path, { flags: 'w+' });
|
||||||
}
|
}
|
||||||
|
|
||||||
class ProgmemGenerator {
|
class ProgmemGenerator {
|
||||||
|
|
||||||
constructor(options = {}) {
|
constructor(options = {}) {
|
||||||
const { outputPath, bytesPerLine = 20, indent = " ", includes = ARDUINO_INCLUDES } = options;
|
const {
|
||||||
|
outputPath,
|
||||||
|
bytesPerLine = 20,
|
||||||
|
indent = ' ',
|
||||||
|
includes = ARDUINO_INCLUDES
|
||||||
|
} = options;
|
||||||
this.options = { outputPath, bytesPerLine, indent, includes };
|
this.options = { outputPath, bytesPerLine, indent, includes };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,30 +51,34 @@ class ProgmemGenerator {
|
|||||||
(compilation, callback) => {
|
(compilation, callback) => {
|
||||||
const { outputPath, bytesPerLine, indent, includes } = this.options;
|
const { outputPath, bytesPerLine, indent, includes } = this.options;
|
||||||
const fileInfo = [];
|
const fileInfo = [];
|
||||||
const writeStream = cleanAndOpen(resolve(compilation.options.context, outputPath));
|
const writeStream = cleanAndOpen(
|
||||||
|
resolve(compilation.options.context, outputPath)
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
const writeIncludes = () => {
|
const writeIncludes = () => {
|
||||||
writeStream.write(includes);
|
writeStream.write(includes);
|
||||||
}
|
};
|
||||||
|
|
||||||
const writeFile = (relativeFilePath, buffer) => {
|
const writeFile = (relativeFilePath, buffer) => {
|
||||||
const variable = "ESP_REACT_DATA_" + fileInfo.length;
|
const variable = 'ESP_REACT_DATA_' + fileInfo.length;
|
||||||
const mimeType = mime.lookup(relativeFilePath);
|
const mimeType = mime.lookup(relativeFilePath);
|
||||||
var size = 0;
|
var size = 0;
|
||||||
writeStream.write("const uint8_t " + variable + "[] PROGMEM = {");
|
writeStream.write('const uint8_t ' + variable + '[] PROGMEM = {');
|
||||||
const zipBuffer = zlib.gzipSync(buffer);
|
const zipBuffer = zlib.gzipSync(buffer);
|
||||||
zipBuffer.forEach((b) => {
|
zipBuffer.forEach((b) => {
|
||||||
if (!(size % bytesPerLine)) {
|
if (!(size % bytesPerLine)) {
|
||||||
writeStream.write("\n");
|
writeStream.write('\n');
|
||||||
writeStream.write(indent);
|
writeStream.write(indent);
|
||||||
}
|
}
|
||||||
writeStream.write("0x" + ("00" + b.toString(16).toUpperCase()).substr(-2) + ",");
|
writeStream.write(
|
||||||
|
'0x' + ('00' + b.toString(16).toUpperCase()).substr(-2) + ','
|
||||||
|
);
|
||||||
size++;
|
size++;
|
||||||
});
|
});
|
||||||
if (size % bytesPerLine) {
|
if (size % bytesPerLine) {
|
||||||
writeStream.write("\n");
|
writeStream.write('\n');
|
||||||
}
|
}
|
||||||
writeStream.write("};\n\n");
|
writeStream.write('};\n\n');
|
||||||
fileInfo.push({
|
fileInfo.push({
|
||||||
uri: '/' + relativeFilePath.replace(sep, '/'),
|
uri: '/' + relativeFilePath.replace(sep, '/'),
|
||||||
mimeType,
|
mimeType,
|
||||||
@@ -84,25 +98,37 @@ class ProgmemGenerator {
|
|||||||
// process assets
|
// process assets
|
||||||
const { assets } = compilation;
|
const { assets } = compilation;
|
||||||
Object.keys(assets).forEach((relativeFilePath) => {
|
Object.keys(assets).forEach((relativeFilePath) => {
|
||||||
writeFile(relativeFilePath, coherseToBuffer(assets[relativeFilePath].source()));
|
writeFile(
|
||||||
|
relativeFilePath,
|
||||||
|
coherseToBuffer(assets[relativeFilePath].source())
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
const generateWWWClass = () => {
|
const generateWWWClass = () => {
|
||||||
return `typedef std::function<void(const String& uri, const String& contentType, const uint8_t * content, size_t len)> RouteRegistrationHandler;
|
return `typedef std::function<void(const String& uri, const String& contentType, const uint8_t * content, size_t len)> RouteRegistrationHandler;
|
||||||
|
|
||||||
class WWWData {
|
class WWWData {
|
||||||
${indent}public:
|
${indent}public:
|
||||||
${indent.repeat(2)}static void registerRoutes(RouteRegistrationHandler handler) {
|
${indent.repeat(
|
||||||
${fileInfo.map(file => `${indent.repeat(3)}handler("${file.uri}", "${file.mimeType}", ${file.variable}, ${file.size});`).join('\n')}
|
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)}}
|
${indent.repeat(2)}}
|
||||||
};
|
};
|
||||||
`;
|
`;
|
||||||
}
|
};
|
||||||
|
|
||||||
const writeWWWClass = () => {
|
const writeWWWClass = () => {
|
||||||
writeStream.write(generateWWWClass());
|
writeStream.write(generateWWWClass());
|
||||||
}
|
};
|
||||||
|
|
||||||
writeIncludes();
|
writeIncludes();
|
||||||
writeFiles();
|
writeFiles();
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name":"EMS-ESP",
|
"name": "EMS-ESP",
|
||||||
"icons":[
|
"icons": [
|
||||||
{
|
{
|
||||||
"src":"/app/icon.png",
|
"src": "/app/icon.png",
|
||||||
"sizes":"48x48 72x72 96x96 128x128 256x256"
|
"sizes": "48x48 72x72 96x96 128x128 256x256"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"start_url":"/",
|
"start_url": "/",
|
||||||
"display":"fullscreen",
|
"display": "fullscreen",
|
||||||
"orientation":"any"
|
"orientation": "any"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,20 +3,26 @@
|
|||||||
font-family: 'Roboto';
|
font-family: 'Roboto';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
src: local('Roboto Light'), local('Roboto-Light'), url(../fonts/li.woff2) format('woff2');
|
src: local('Roboto Light'), local('Roboto-Light'),
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2212, U+2215;
|
url(../fonts/li.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||||
|
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2212, U+2215;
|
||||||
}
|
}
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Roboto';
|
font-family: 'Roboto';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: local('Roboto'), local('Roboto-Regular'), url(../fonts/re.woff2) format('woff2');
|
src: local('Roboto'), local('Roboto-Regular'),
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2212, U+2215;
|
url(../fonts/re.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||||
|
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2212, U+2215;
|
||||||
}
|
}
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Roboto';
|
font-family: 'Roboto';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
src: local('Roboto Medium'), local('Roboto-Medium'), url(../fonts/me.woff2) format('woff2');
|
src: local('Roboto Medium'), local('Roboto-Medium'),
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2212, U+2215;
|
url(../fonts/me.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||||
|
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2212, U+2215;
|
||||||
}
|
}
|
||||||
@@ -14,7 +14,6 @@ import FeaturesWrapper from './features/FeaturesWrapper';
|
|||||||
const unauthorizedRedirect = () => <Redirect to="/" />;
|
const unauthorizedRedirect = () => <Redirect to="/" />;
|
||||||
|
|
||||||
class App extends Component {
|
class App extends Component {
|
||||||
|
|
||||||
notistackRef: RefObject<any> = React.createRef();
|
notistackRef: RefObject<any> = React.createRef();
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@@ -23,21 +22,29 @@ class App extends Component {
|
|||||||
|
|
||||||
onClickDismiss = (key: string | number | undefined) => () => {
|
onClickDismiss = (key: string | number | undefined) => () => {
|
||||||
this.notistackRef.current.closeSnackbar(key);
|
this.notistackRef.current.closeSnackbar(key);
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<CustomMuiTheme>
|
<CustomMuiTheme>
|
||||||
<SnackbarProvider autoHideDuration={3000} maxSnack={3} anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
<SnackbarProvider
|
||||||
|
autoHideDuration={3000}
|
||||||
|
maxSnack={3}
|
||||||
|
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
||||||
ref={this.notistackRef}
|
ref={this.notistackRef}
|
||||||
action={(key) => (
|
action={(key) => (
|
||||||
<IconButton onClick={this.onClickDismiss(key)} size="small">
|
<IconButton onClick={this.onClickDismiss(key)} size="small">
|
||||||
<CloseIcon />
|
<CloseIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}>
|
)}
|
||||||
|
>
|
||||||
<FeaturesWrapper>
|
<FeaturesWrapper>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/unauthorized" component={unauthorizedRedirect} />
|
<Route
|
||||||
|
exact
|
||||||
|
path="/unauthorized"
|
||||||
|
component={unauthorizedRedirect}
|
||||||
|
/>
|
||||||
<Route component={AppRouting} />
|
<Route component={AppRouting} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</FeaturesWrapper>
|
</FeaturesWrapper>
|
||||||
@@ -47,4 +54,4 @@ class App extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App
|
export default App;
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ import Mqtt from './mqtt/Mqtt';
|
|||||||
import { withFeatures, WithFeaturesProps } from './features/FeaturesContext';
|
import { withFeatures, WithFeaturesProps } from './features/FeaturesContext';
|
||||||
import { Features } from './features/types';
|
import { Features } from './features/types';
|
||||||
|
|
||||||
export const getDefaultRoute = (features: Features) => features.project ? `/${PROJECT_PATH}/` : "/network/";
|
export const getDefaultRoute = (features: Features) =>
|
||||||
|
features.project ? `/${PROJECT_PATH}/` : '/network/';
|
||||||
class AppRouting extends Component<WithFeaturesProps> {
|
class AppRouting extends Component<WithFeaturesProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
Authentication.clearLoginRedirect();
|
Authentication.clearLoginRedirect();
|
||||||
}
|
}
|
||||||
@@ -35,9 +35,17 @@ class AppRouting extends Component<WithFeaturesProps> {
|
|||||||
<UnauthenticatedRoute exact path="/" component={SignIn} />
|
<UnauthenticatedRoute exact path="/" component={SignIn} />
|
||||||
)}
|
)}
|
||||||
{features.project && (
|
{features.project && (
|
||||||
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/*`} component={ProjectRouting} />
|
<AuthenticatedRoute
|
||||||
|
exact
|
||||||
|
path={`/${PROJECT_PATH}/*`}
|
||||||
|
component={ProjectRouting}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<AuthenticatedRoute exact path="/network/*" component={NetworkConnection} />
|
<AuthenticatedRoute
|
||||||
|
exact
|
||||||
|
path="/network/*"
|
||||||
|
component={NetworkConnection}
|
||||||
|
/>
|
||||||
<AuthenticatedRoute exact path="/ap/*" component={AccessPoint} />
|
<AuthenticatedRoute exact path="/ap/*" component={AccessPoint} />
|
||||||
{features.ntp && (
|
{features.ntp && (
|
||||||
<AuthenticatedRoute exact path="/ntp/*" component={NetworkTime} />
|
<AuthenticatedRoute exact path="/ntp/*" component={NetworkTime} />
|
||||||
@@ -52,7 +60,7 @@ class AppRouting extends Component<WithFeaturesProps> {
|
|||||||
<Redirect to={getDefaultRoute(features)} />
|
<Redirect to={getDefaultRoute(features)} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</AuthenticationWrapper>
|
</AuthenticationWrapper>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
|
|
||||||
import { CssBaseline } from '@material-ui/core';
|
import { CssBaseline } from '@material-ui/core';
|
||||||
import { MuiThemeProvider, createMuiTheme, StylesProvider } from '@material-ui/core/styles';
|
import {
|
||||||
|
MuiThemeProvider,
|
||||||
|
createTheme,
|
||||||
|
StylesProvider
|
||||||
|
} from '@material-ui/core/styles';
|
||||||
import { blueGrey, orange, red, green } from '@material-ui/core/colors';
|
import { blueGrey, orange, red, green } from '@material-ui/core/colors';
|
||||||
|
|
||||||
const theme = createMuiTheme({
|
const theme = createTheme({
|
||||||
palette: {
|
palette: {
|
||||||
type: "dark",
|
type: 'dark',
|
||||||
primary: {
|
primary: {
|
||||||
main: '#33bfff',
|
main: '#33bfff'
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
main: '#3d5afe',
|
main: '#3d5afe'
|
||||||
},
|
},
|
||||||
info: {
|
info: {
|
||||||
main: blueGrey[500]
|
main: blueGrey[500]
|
||||||
@@ -29,7 +33,6 @@ const theme = createMuiTheme({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default class CustomMuiTheme extends Component {
|
export default class CustomMuiTheme extends Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<StylesProvider>
|
<StylesProvider>
|
||||||
@@ -40,5 +43,4 @@ export default class CustomMuiTheme extends Component {
|
|||||||
</StylesProvider>
|
</StylesProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,53 +2,63 @@ import React, { Component } from 'react';
|
|||||||
import { withSnackbar, WithSnackbarProps } from 'notistack';
|
import { withSnackbar, WithSnackbarProps } from 'notistack';
|
||||||
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
|
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
|
||||||
|
|
||||||
import { withStyles, createStyles, Theme, WithStyles } from '@material-ui/core/styles';
|
import {
|
||||||
|
withStyles,
|
||||||
|
createStyles,
|
||||||
|
Theme,
|
||||||
|
WithStyles
|
||||||
|
} from '@material-ui/core/styles';
|
||||||
import { Paper, Typography, Fab } from '@material-ui/core';
|
import { Paper, Typography, Fab } from '@material-ui/core';
|
||||||
import ForwardIcon from '@material-ui/icons/Forward';
|
import ForwardIcon from '@material-ui/icons/Forward';
|
||||||
|
|
||||||
import { withAuthenticationContext, AuthenticationContextProps } from './authentication/AuthenticationContext';
|
import {
|
||||||
import {PasswordValidator} from './components';
|
withAuthenticationContext,
|
||||||
|
AuthenticationContextProps
|
||||||
|
} from './authentication/AuthenticationContext';
|
||||||
|
import { PasswordValidator } from './components';
|
||||||
import { PROJECT_NAME, SIGN_IN_ENDPOINT } from './api';
|
import { PROJECT_NAME, SIGN_IN_ENDPOINT } from './api';
|
||||||
|
|
||||||
const styles = (theme: Theme) => createStyles({
|
const styles = (theme: Theme) =>
|
||||||
signInPage: {
|
createStyles({
|
||||||
display: "flex",
|
signInPage: {
|
||||||
height: "100vh",
|
display: 'flex',
|
||||||
margin: "auto",
|
height: '100vh',
|
||||||
padding: theme.spacing(2),
|
margin: 'auto',
|
||||||
justifyContent: "center",
|
padding: theme.spacing(2),
|
||||||
flexDirection: "column",
|
justifyContent: 'center',
|
||||||
maxWidth: theme.breakpoints.values.sm
|
flexDirection: 'column',
|
||||||
},
|
maxWidth: theme.breakpoints.values.sm
|
||||||
signInPanel: {
|
},
|
||||||
textAlign: "center",
|
signInPanel: {
|
||||||
padding: theme.spacing(2),
|
textAlign: 'center',
|
||||||
paddingTop: "200px",
|
padding: theme.spacing(2),
|
||||||
backgroundImage: 'url("/app/icon.png")',
|
paddingTop: '200px',
|
||||||
backgroundRepeat: "no-repeat",
|
backgroundImage: 'url("/app/icon.png")',
|
||||||
backgroundPosition: "50% " + theme.spacing(2) + "px",
|
backgroundRepeat: 'no-repeat',
|
||||||
backgroundSize: "auto 150px",
|
backgroundPosition: '50% ' + theme.spacing(2) + 'px',
|
||||||
width: "100%"
|
backgroundSize: 'auto 150px',
|
||||||
},
|
width: '100%'
|
||||||
extendedIcon: {
|
},
|
||||||
marginRight: theme.spacing(0.5),
|
extendedIcon: {
|
||||||
},
|
marginRight: theme.spacing(0.5)
|
||||||
button: {
|
},
|
||||||
marginRight: theme.spacing(2),
|
button: {
|
||||||
marginTop: theme.spacing(2),
|
marginRight: theme.spacing(2),
|
||||||
}
|
marginTop: theme.spacing(2)
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
|
||||||
type SignInProps = WithSnackbarProps & WithStyles<typeof styles> & AuthenticationContextProps;
|
type SignInProps = WithSnackbarProps &
|
||||||
|
WithStyles<typeof styles> &
|
||||||
|
AuthenticationContextProps;
|
||||||
|
|
||||||
interface SignInState {
|
interface SignInState {
|
||||||
username: string,
|
username: string;
|
||||||
password: string,
|
password: string;
|
||||||
processing: boolean
|
processing: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class SignIn extends Component<SignInProps, SignInState> {
|
class SignIn extends Component<SignInProps, SignInState> {
|
||||||
|
|
||||||
constructor(props: SignInProps) {
|
constructor(props: SignInProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
@@ -60,10 +70,10 @@ class SignIn extends Component<SignInProps, SignInState> {
|
|||||||
|
|
||||||
updateInputElement = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
updateInputElement = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
const { name, value } = event.currentTarget;
|
const { name, value } = event.currentTarget;
|
||||||
this.setState(prevState => ({
|
this.setState((prevState) => ({
|
||||||
...prevState,
|
...prevState,
|
||||||
[name]: value,
|
[name]: value
|
||||||
}))
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
onSubmit = () => {
|
onSubmit = () => {
|
||||||
@@ -77,20 +87,21 @@ class SignIn extends Component<SignInProps, SignInState> {
|
|||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then((response) => {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
return response.json();
|
return response.json();
|
||||||
} else if (response.status === 401) {
|
} else if (response.status === 401) {
|
||||||
throw Error("Invalid credentials.");
|
throw Error('Invalid credentials.');
|
||||||
} else {
|
} else {
|
||||||
throw Error("Invalid status code: " + response.status);
|
throw Error('Invalid status code: ' + response.status);
|
||||||
}
|
}
|
||||||
}).then(json => {
|
})
|
||||||
|
.then((json) => {
|
||||||
authenticationContext.signIn(json.access_token);
|
authenticationContext.signIn(json.access_token);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
this.props.enqueueSnackbar(error.message, {
|
this.props.enqueueSnackbar(error.message, {
|
||||||
variant: 'warning',
|
variant: 'warning'
|
||||||
});
|
});
|
||||||
this.setState({ processing: false });
|
this.setState({ processing: false });
|
||||||
});
|
});
|
||||||
@@ -116,8 +127,8 @@ class SignIn extends Component<SignInProps, SignInState> {
|
|||||||
onChange={this.updateInputElement}
|
onChange={this.updateInputElement}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
inputProps={{
|
inputProps={{
|
||||||
autoCapitalize: "none",
|
autoCapitalize: 'none',
|
||||||
autoCorrect: "off",
|
autoCorrect: 'off'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<PasswordValidator
|
<PasswordValidator
|
||||||
@@ -132,7 +143,13 @@ class SignIn extends Component<SignInProps, SignInState> {
|
|||||||
onChange={this.updateInputElement}
|
onChange={this.updateInputElement}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<Fab variant="extended" color="primary" className={classes.button} type="submit" disabled={processing}>
|
<Fab
|
||||||
|
variant="extended"
|
||||||
|
color="primary"
|
||||||
|
className={classes.button}
|
||||||
|
type="submit"
|
||||||
|
disabled={processing}
|
||||||
|
>
|
||||||
<ForwardIcon className={classes.extendedIcon} />
|
<ForwardIcon className={classes.extendedIcon} />
|
||||||
Sign In
|
Sign In
|
||||||
</Fab>
|
</Fab>
|
||||||
@@ -141,7 +158,8 @@ class SignIn extends Component<SignInProps, SignInState> {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withAuthenticationContext(withSnackbar(withStyles(styles)(SignIn)));
|
export default withAuthenticationContext(
|
||||||
|
withSnackbar(withStyles(styles)(SignIn))
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { APSettings, APProvisionMode } from "./types";
|
import { APSettings, APProvisionMode } from './types';
|
||||||
|
|
||||||
export const isAPEnabled = ({ provision_mode }: APSettings) => {
|
export const isAPEnabled = ({ provision_mode }: APSettings) => {
|
||||||
return provision_mode === APProvisionMode.AP_MODE_ALWAYS || provision_mode === APProvisionMode.AP_MODE_DISCONNECTED;
|
return (
|
||||||
}
|
provision_mode === APProvisionMode.AP_MODE_ALWAYS ||
|
||||||
|
provision_mode === APProvisionMode.AP_MODE_DISCONNECTED
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
|
|
||||||
import { AP_SETTINGS_ENDPOINT } from '../api';
|
import { AP_SETTINGS_ENDPOINT } from '../api';
|
||||||
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
import {
|
||||||
|
restController,
|
||||||
|
RestControllerProps,
|
||||||
|
RestFormLoader,
|
||||||
|
SectionContent
|
||||||
|
} from '../components';
|
||||||
|
|
||||||
import APSettingsForm from './APSettingsForm';
|
import APSettingsForm from './APSettingsForm';
|
||||||
import { APSettings } from './types';
|
import { APSettings } from './types';
|
||||||
@@ -9,7 +14,6 @@ import { APSettings } from './types';
|
|||||||
type APSettingsControllerProps = RestControllerProps<APSettings>;
|
type APSettingsControllerProps = RestControllerProps<APSettings>;
|
||||||
|
|
||||||
class APSettingsController extends Component<APSettingsControllerProps> {
|
class APSettingsController extends Component<APSettingsControllerProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.loadData();
|
this.props.loadData();
|
||||||
}
|
}
|
||||||
@@ -19,12 +23,11 @@ class APSettingsController extends Component<APSettingsControllerProps> {
|
|||||||
<SectionContent title="Access Point Settings" titleGutter>
|
<SectionContent title="Access Point Settings" titleGutter>
|
||||||
<RestFormLoader
|
<RestFormLoader
|
||||||
{...this.props}
|
{...this.props}
|
||||||
render={formProps => <APSettingsForm {...formProps} />}
|
render={(formProps) => <APSettingsForm {...formProps} />}
|
||||||
/>
|
/>
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default restController(AP_SETTINGS_ENDPOINT, APSettingsController);
|
export default restController(AP_SETTINGS_ENDPOINT, APSettingsController);
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import { TextValidator, ValidatorForm, SelectValidator } from 'react-material-ui-form-validator';
|
import {
|
||||||
|
TextValidator,
|
||||||
|
ValidatorForm,
|
||||||
|
SelectValidator
|
||||||
|
} from 'react-material-ui-form-validator';
|
||||||
|
|
||||||
import MenuItem from '@material-ui/core/MenuItem';
|
import MenuItem from '@material-ui/core/MenuItem';
|
||||||
import SaveIcon from '@material-ui/icons/Save';
|
import SaveIcon from '@material-ui/icons/Save';
|
||||||
|
|
||||||
import { PasswordValidator, RestFormProps, FormActions, FormButton } from '../components';
|
import {
|
||||||
|
PasswordValidator,
|
||||||
|
RestFormProps,
|
||||||
|
FormActions,
|
||||||
|
FormButton
|
||||||
|
} from '../components';
|
||||||
|
|
||||||
import { isAPEnabled } from './APModes';
|
import { isAPEnabled } from './APModes';
|
||||||
import { APSettings, APProvisionMode } from './types';
|
import { APSettings, APProvisionMode } from './types';
|
||||||
@@ -13,7 +22,6 @@ import { isIP } from '../validators';
|
|||||||
type APSettingsFormProps = RestFormProps<APSettings>;
|
type APSettingsFormProps = RestFormProps<APSettings>;
|
||||||
|
|
||||||
class APSettingsForm extends React.Component<APSettingsFormProps> {
|
class APSettingsForm extends React.Component<APSettingsFormProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
ValidatorForm.addValidationRule('isIP', isIP);
|
ValidatorForm.addValidationRule('isIP', isIP);
|
||||||
}
|
}
|
||||||
@@ -22,23 +30,29 @@ class APSettingsForm extends React.Component<APSettingsFormProps> {
|
|||||||
const { data, handleValueChange, saveData } = this.props;
|
const { data, handleValueChange, saveData } = this.props;
|
||||||
return (
|
return (
|
||||||
<ValidatorForm onSubmit={saveData} ref="APSettingsForm">
|
<ValidatorForm onSubmit={saveData} ref="APSettingsForm">
|
||||||
<SelectValidator name="provision_mode"
|
<SelectValidator
|
||||||
|
name="provision_mode"
|
||||||
label="Provide Access Point…"
|
label="Provide Access Point…"
|
||||||
value={data.provision_mode}
|
value={data.provision_mode}
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onChange={handleValueChange('provision_mode')}
|
onChange={handleValueChange('provision_mode')}
|
||||||
margin="normal">
|
margin="normal"
|
||||||
|
>
|
||||||
<MenuItem value={APProvisionMode.AP_MODE_ALWAYS}>Always</MenuItem>
|
<MenuItem value={APProvisionMode.AP_MODE_ALWAYS}>Always</MenuItem>
|
||||||
<MenuItem value={APProvisionMode.AP_MODE_DISCONNECTED}>When Network Disconnected</MenuItem>
|
<MenuItem value={APProvisionMode.AP_MODE_DISCONNECTED}>
|
||||||
|
When Network Disconnected
|
||||||
|
</MenuItem>
|
||||||
<MenuItem value={APProvisionMode.AP_NEVER}>Never</MenuItem>
|
<MenuItem value={APProvisionMode.AP_NEVER}>Never</MenuItem>
|
||||||
</SelectValidator>
|
</SelectValidator>
|
||||||
{
|
{isAPEnabled(data) && (
|
||||||
isAPEnabled(data) &&
|
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['required', 'matchRegexp:^.{1,32}$']}
|
validators={['required', 'matchRegexp:^.{1,32}$']}
|
||||||
errorMessages={['Access Point SSID is required', 'Access Point SSID must be 32 characters or less']}
|
errorMessages={[
|
||||||
|
'Access Point SSID is required',
|
||||||
|
'Access Point SSID must be 32 characters or less'
|
||||||
|
]}
|
||||||
name="ssid"
|
name="ssid"
|
||||||
label="Access Point SSID"
|
label="Access Point SSID"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -49,7 +63,10 @@ class APSettingsForm extends React.Component<APSettingsFormProps> {
|
|||||||
/>
|
/>
|
||||||
<PasswordValidator
|
<PasswordValidator
|
||||||
validators={['required', 'matchRegexp:^.{8,64}$']}
|
validators={['required', 'matchRegexp:^.{8,64}$']}
|
||||||
errorMessages={['Access Point Password is required', 'Access Point Password must be 8-64 characters']}
|
errorMessages={[
|
||||||
|
'Access Point Password is required',
|
||||||
|
'Access Point Password must be 8-64 characters'
|
||||||
|
]}
|
||||||
name="password"
|
name="password"
|
||||||
label="Access Point Password"
|
label="Access Point Password"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -71,7 +88,10 @@ class APSettingsForm extends React.Component<APSettingsFormProps> {
|
|||||||
/>
|
/>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['required', 'isIP']}
|
validators={['required', 'isIP']}
|
||||||
errorMessages={['Gateway IP is required', 'Must be an IP address']}
|
errorMessages={[
|
||||||
|
'Gateway IP is required',
|
||||||
|
'Must be an IP address'
|
||||||
|
]}
|
||||||
name="gateway_ip"
|
name="gateway_ip"
|
||||||
label="Gateway"
|
label="Gateway"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -82,7 +102,10 @@ class APSettingsForm extends React.Component<APSettingsFormProps> {
|
|||||||
/>
|
/>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['required', 'isIP']}
|
validators={['required', 'isIP']}
|
||||||
errorMessages={['Subnet mask is required', 'Must be an IP address']}
|
errorMessages={[
|
||||||
|
'Subnet mask is required',
|
||||||
|
'Must be an IP address'
|
||||||
|
]}
|
||||||
name="subnet_mask"
|
name="subnet_mask"
|
||||||
label="Subnet"
|
label="Subnet"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -92,9 +115,14 @@ class APSettingsForm extends React.Component<APSettingsFormProps> {
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
}
|
)}
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
|
<FormButton
|
||||||
|
startIcon={<SaveIcon />}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
Save
|
Save
|
||||||
</FormButton>
|
</FormButton>
|
||||||
</FormActions>
|
</FormActions>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Theme } from "@material-ui/core";
|
import { Theme } from '@material-ui/core';
|
||||||
import { APStatus, APNetworkStatus } from "./types";
|
import { APStatus, APNetworkStatus } from './types';
|
||||||
|
|
||||||
export const apStatusHighlight = ({ status }: APStatus, theme: Theme) => {
|
export const apStatusHighlight = ({ status }: APStatus, theme: Theme) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
@@ -12,17 +12,17 @@ export const apStatusHighlight = ({ status }: APStatus, theme: Theme) => {
|
|||||||
default:
|
default:
|
||||||
return theme.palette.warning.main;
|
return theme.palette.warning.main;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export const apStatus = ({ status }: APStatus) => {
|
export const apStatus = ({ status }: APStatus) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case APNetworkStatus.ACTIVE:
|
case APNetworkStatus.ACTIVE:
|
||||||
return "Active";
|
return 'Active';
|
||||||
case APNetworkStatus.INACTIVE:
|
case APNetworkStatus.INACTIVE:
|
||||||
return "Inactive";
|
return 'Inactive';
|
||||||
case APNetworkStatus.LINGERING:
|
case APNetworkStatus.LINGERING:
|
||||||
return "Lingering until idle";
|
return 'Lingering until idle';
|
||||||
default:
|
default:
|
||||||
return "Unknown";
|
return 'Unknown';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
|
|
||||||
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
import {
|
||||||
|
restController,
|
||||||
|
RestControllerProps,
|
||||||
|
RestFormLoader,
|
||||||
|
SectionContent
|
||||||
|
} from '../components';
|
||||||
import { AP_STATUS_ENDPOINT } from '../api';
|
import { AP_STATUS_ENDPOINT } from '../api';
|
||||||
|
|
||||||
import APStatusForm from './APStatusForm';
|
import APStatusForm from './APStatusForm';
|
||||||
@@ -9,7 +14,6 @@ import { APStatus } from './types';
|
|||||||
type APStatusControllerProps = RestControllerProps<APStatus>;
|
type APStatusControllerProps = RestControllerProps<APStatus>;
|
||||||
|
|
||||||
class APStatusController extends Component<APStatusControllerProps> {
|
class APStatusController extends Component<APStatusControllerProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.loadData();
|
this.props.loadData();
|
||||||
}
|
}
|
||||||
@@ -19,10 +23,10 @@ class APStatusController extends Component<APStatusControllerProps> {
|
|||||||
<SectionContent title="Access Point Status">
|
<SectionContent title="Access Point Status">
|
||||||
<RestFormLoader
|
<RestFormLoader
|
||||||
{...this.props}
|
{...this.props}
|
||||||
render={formProps => <APStatusForm {...formProps} />}
|
render={(formProps) => <APStatusForm {...formProps} />}
|
||||||
/>
|
/>
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,34 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
|
|
||||||
import { WithTheme, withTheme } from '@material-ui/core/styles';
|
import { WithTheme, withTheme } from '@material-ui/core/styles';
|
||||||
import { Avatar, Divider, List, ListItem, ListItemAvatar, ListItemText } from '@material-ui/core';
|
import {
|
||||||
|
Avatar,
|
||||||
|
Divider,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemAvatar,
|
||||||
|
ListItemText
|
||||||
|
} from '@material-ui/core';
|
||||||
|
|
||||||
import SettingsInputAntennaIcon from '@material-ui/icons/SettingsInputAntenna';
|
import SettingsInputAntennaIcon from '@material-ui/icons/SettingsInputAntenna';
|
||||||
import DeviceHubIcon from '@material-ui/icons/DeviceHub';
|
import DeviceHubIcon from '@material-ui/icons/DeviceHub';
|
||||||
import ComputerIcon from '@material-ui/icons/Computer';
|
import ComputerIcon from '@material-ui/icons/Computer';
|
||||||
import RefreshIcon from '@material-ui/icons/Refresh';
|
import RefreshIcon from '@material-ui/icons/Refresh';
|
||||||
|
|
||||||
import { RestFormProps, FormActions, FormButton, HighlightAvatar } from '../components';
|
import {
|
||||||
|
RestFormProps,
|
||||||
|
FormActions,
|
||||||
|
FormButton,
|
||||||
|
HighlightAvatar
|
||||||
|
} from '../components';
|
||||||
import { apStatusHighlight, apStatus } from './APStatus';
|
import { apStatusHighlight, apStatus } from './APStatus';
|
||||||
import { APStatus } from './types';
|
import { APStatus } from './types';
|
||||||
|
|
||||||
type APStatusFormProps = RestFormProps<APStatus> & WithTheme;
|
type APStatusFormProps = RestFormProps<APStatus> & WithTheme;
|
||||||
|
|
||||||
class APStatusForm extends Component<APStatusFormProps> {
|
class APStatusForm extends Component<APStatusFormProps> {
|
||||||
|
|
||||||
createListItems() {
|
createListItems() {
|
||||||
const { data, theme } = this.props
|
const { data, theme } = this.props;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
@@ -61,18 +72,20 @@ class APStatusForm extends Component<APStatusFormProps> {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<List>
|
<List>{this.createListItems()}</List>
|
||||||
{this.createListItems()}
|
|
||||||
</List>
|
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<FormButton startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={this.props.loadData}>
|
<FormButton
|
||||||
|
startIcon={<RefreshIcon />}
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
onClick={this.props.loadData}
|
||||||
|
>
|
||||||
Refresh
|
Refresh
|
||||||
</FormButton>
|
</FormButton>
|
||||||
</FormActions>
|
</FormActions>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withTheme(APStatusForm);
|
export default withTheme(APStatusForm);
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
|
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
|
||||||
|
|
||||||
import { Tabs, Tab } from '@material-ui/core';
|
import { Tabs, Tab } from '@material-ui/core';
|
||||||
|
|
||||||
import { AuthenticatedContextProps, withAuthenticatedContext, AuthenticatedRoute } from '../authentication';
|
import {
|
||||||
|
AuthenticatedContextProps,
|
||||||
|
withAuthenticatedContext,
|
||||||
|
AuthenticatedRoute
|
||||||
|
} from '../authentication';
|
||||||
import { MenuAppBar } from '../components';
|
import { MenuAppBar } from '../components';
|
||||||
|
|
||||||
import APSettingsController from './APSettingsController';
|
import APSettingsController from './APSettingsController';
|
||||||
@@ -12,8 +16,7 @@ import APStatusController from './APStatusController';
|
|||||||
type AccessPointProps = AuthenticatedContextProps & RouteComponentProps;
|
type AccessPointProps = AuthenticatedContextProps & RouteComponentProps;
|
||||||
|
|
||||||
class AccessPoint extends Component<AccessPointProps> {
|
class AccessPoint extends Component<AccessPointProps> {
|
||||||
|
handleTabChange = (path: string) => {
|
||||||
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
|
|
||||||
this.props.history.push(path);
|
this.props.history.push(path);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -21,17 +24,33 @@ class AccessPoint extends Component<AccessPointProps> {
|
|||||||
const { authenticatedContext } = this.props;
|
const { authenticatedContext } = this.props;
|
||||||
return (
|
return (
|
||||||
<MenuAppBar sectionTitle="Access Point">
|
<MenuAppBar sectionTitle="Access Point">
|
||||||
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
|
<Tabs
|
||||||
|
value={this.props.match.url}
|
||||||
|
onChange={(e, path) => this.handleTabChange(path)}
|
||||||
|
variant="fullWidth"
|
||||||
|
>
|
||||||
<Tab value="/ap/status" label="Access Point Status" />
|
<Tab value="/ap/status" label="Access Point Status" />
|
||||||
<Tab value="/ap/settings" label="Access Point Settings" disabled={!authenticatedContext.me.admin} />
|
<Tab
|
||||||
|
value="/ap/settings"
|
||||||
|
label="Access Point Settings"
|
||||||
|
disabled={!authenticatedContext.me.admin}
|
||||||
|
/>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<Switch>
|
<Switch>
|
||||||
<AuthenticatedRoute exact path="/ap/status" component={APStatusController} />
|
<AuthenticatedRoute
|
||||||
<AuthenticatedRoute exact path="/ap/settings" component={APSettingsController} />
|
exact
|
||||||
|
path="/ap/status"
|
||||||
|
component={APStatusController}
|
||||||
|
/>
|
||||||
|
<AuthenticatedRoute
|
||||||
|
exact
|
||||||
|
path="/ap/settings"
|
||||||
|
component={APSettingsController}
|
||||||
|
/>
|
||||||
<Redirect to="/ap/status" />
|
<Redirect to="/ap/status" />
|
||||||
</Switch>
|
</Switch>
|
||||||
</MenuAppBar>
|
</MenuAppBar>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,24 @@
|
|||||||
import { ENDPOINT_ROOT } from './Env';
|
import { ENDPOINT_ROOT } from './Env';
|
||||||
|
|
||||||
export const FEATURES_ENDPOINT = ENDPOINT_ROOT + "features";
|
export const FEATURES_ENDPOINT = ENDPOINT_ROOT + 'features';
|
||||||
export const NTP_STATUS_ENDPOINT = ENDPOINT_ROOT + "ntpStatus";
|
export const NTP_STATUS_ENDPOINT = ENDPOINT_ROOT + 'ntpStatus';
|
||||||
export const NTP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "ntpSettings";
|
export const NTP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'ntpSettings';
|
||||||
export const TIME_ENDPOINT = ENDPOINT_ROOT + "time";
|
export const TIME_ENDPOINT = ENDPOINT_ROOT + 'time';
|
||||||
export const AP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "apSettings";
|
export const AP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'apSettings';
|
||||||
export const AP_STATUS_ENDPOINT = ENDPOINT_ROOT + "apStatus";
|
export const AP_STATUS_ENDPOINT = ENDPOINT_ROOT + 'apStatus';
|
||||||
export const SCAN_NETWORKS_ENDPOINT = ENDPOINT_ROOT + "scanNetworks";
|
export const SCAN_NETWORKS_ENDPOINT = ENDPOINT_ROOT + 'scanNetworks';
|
||||||
export const LIST_NETWORKS_ENDPOINT = ENDPOINT_ROOT + "listNetworks";
|
export const LIST_NETWORKS_ENDPOINT = ENDPOINT_ROOT + 'listNetworks';
|
||||||
export const NETWORK_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "networkSettings";
|
export const NETWORK_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'networkSettings';
|
||||||
export const NETWORK_STATUS_ENDPOINT = ENDPOINT_ROOT + "networkStatus";
|
export const NETWORK_STATUS_ENDPOINT = ENDPOINT_ROOT + 'networkStatus';
|
||||||
export const OTA_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "otaSettings";
|
export const OTA_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'otaSettings';
|
||||||
export const UPLOAD_FIRMWARE_ENDPOINT = ENDPOINT_ROOT + "uploadFirmware";
|
export const UPLOAD_FIRMWARE_ENDPOINT = ENDPOINT_ROOT + 'uploadFirmware';
|
||||||
export const MQTT_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "mqttSettings";
|
export const MQTT_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'mqttSettings';
|
||||||
export const MQTT_STATUS_ENDPOINT = ENDPOINT_ROOT + "mqttStatus";
|
export const MQTT_STATUS_ENDPOINT = ENDPOINT_ROOT + 'mqttStatus';
|
||||||
export const SYSTEM_STATUS_ENDPOINT = ENDPOINT_ROOT + "systemStatus";
|
export const SYSTEM_STATUS_ENDPOINT = ENDPOINT_ROOT + 'systemStatus';
|
||||||
export const SIGN_IN_ENDPOINT = ENDPOINT_ROOT + "signIn";
|
export const SIGN_IN_ENDPOINT = ENDPOINT_ROOT + 'signIn';
|
||||||
export const VERIFY_AUTHORIZATION_ENDPOINT = ENDPOINT_ROOT + "verifyAuthorization";
|
export const VERIFY_AUTHORIZATION_ENDPOINT =
|
||||||
export const SECURITY_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "securitySettings";
|
ENDPOINT_ROOT + 'verifyAuthorization';
|
||||||
export const RESTART_ENDPOINT = ENDPOINT_ROOT + "restart";
|
export const SECURITY_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'securitySettings';
|
||||||
export const FACTORY_RESET_ENDPOINT = ENDPOINT_ROOT + "factoryReset";
|
export const GENERATE_TOKEN_ENDPOINT = ENDPOINT_ROOT + 'generateToken';
|
||||||
|
export const RESTART_ENDPOINT = ENDPOINT_ROOT + 'restart';
|
||||||
|
export const FACTORY_RESET_ENDPOINT = ENDPOINT_ROOT + 'factoryReset';
|
||||||
|
|||||||
@@ -1,24 +1,26 @@
|
|||||||
export const PROJECT_NAME = process.env.REACT_APP_PROJECT_NAME!;
|
export const PROJECT_NAME = process.env.REACT_APP_PROJECT_NAME!;
|
||||||
export const PROJECT_PATH = process.env.REACT_APP_PROJECT_PATH!;
|
export const PROJECT_PATH = process.env.REACT_APP_PROJECT_PATH!;
|
||||||
|
|
||||||
export const ENDPOINT_ROOT = calculateEndpointRoot("/rest/");
|
export const ENDPOINT_ROOT = calculateEndpointRoot('/rest/');
|
||||||
export const WEB_SOCKET_ROOT = calculateWebSocketRoot("/ws/");
|
export const WEB_SOCKET_ROOT = calculateWebSocketRoot('/ws/');
|
||||||
|
export const EVENT_SOURCE_ROOT = calculateEndpointRoot('/es/');
|
||||||
|
export const API_ENDPOINT_ROOT = calculateEndpointRoot('/api/');
|
||||||
|
|
||||||
function calculateEndpointRoot(endpointPath: string) {
|
function calculateEndpointRoot(endpointPath: string) {
|
||||||
const httpRoot = process.env.REACT_APP_HTTP_ROOT;
|
const httpRoot = process.env.REACT_APP_HTTP_ROOT;
|
||||||
if (httpRoot) {
|
if (httpRoot) {
|
||||||
return httpRoot + endpointPath;
|
return httpRoot + endpointPath;
|
||||||
}
|
}
|
||||||
const location = window.location;
|
const location = window.location;
|
||||||
return location.protocol + "//" + location.host + endpointPath;
|
return location.protocol + '//' + location.host + endpointPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateWebSocketRoot(webSocketPath: string) {
|
function calculateWebSocketRoot(webSocketPath: string) {
|
||||||
const webSocketRoot = process.env.REACT_APP_WEB_SOCKET_ROOT;
|
const webSocketRoot = process.env.REACT_APP_WEB_SOCKET_ROOT;
|
||||||
if (webSocketRoot) {
|
if (webSocketRoot) {
|
||||||
return webSocketRoot + webSocketPath;
|
return webSocketRoot + webSocketPath;
|
||||||
}
|
}
|
||||||
const location = window.location;
|
const location = window.location;
|
||||||
const webProtocol = location.protocol === "https:" ? "wss:" : "ws:";
|
const webProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
return webProtocol + "//" + location.host + webSocketPath;
|
return webProtocol + '//' + location.host + webSocketPath;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export * from './Env'
|
export * from './Env';
|
||||||
export * from './Endpoints'
|
export * from './Endpoints';
|
||||||
|
|||||||
@@ -1,42 +1,56 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Redirect, Route, RouteProps, RouteComponentProps } from "react-router-dom";
|
import {
|
||||||
|
Redirect,
|
||||||
|
Route,
|
||||||
|
RouteProps,
|
||||||
|
RouteComponentProps
|
||||||
|
} from 'react-router-dom';
|
||||||
import { withSnackbar, WithSnackbarProps } from 'notistack';
|
import { withSnackbar, WithSnackbarProps } from 'notistack';
|
||||||
|
|
||||||
import * as Authentication from './Authentication';
|
import * as Authentication from './Authentication';
|
||||||
import { withAuthenticationContext, AuthenticationContextProps, AuthenticatedContext, AuthenticatedContextValue } from './AuthenticationContext';
|
import {
|
||||||
|
withAuthenticationContext,
|
||||||
|
AuthenticationContextProps,
|
||||||
|
AuthenticatedContext,
|
||||||
|
AuthenticatedContextValue
|
||||||
|
} from './AuthenticationContext';
|
||||||
|
|
||||||
type ChildComponent = React.ComponentType<RouteComponentProps<any>> | React.ComponentType<any>;
|
interface AuthenticatedRouteProps
|
||||||
|
extends RouteProps,
|
||||||
interface AuthenticatedRouteProps extends RouteProps, WithSnackbarProps, AuthenticationContextProps {
|
WithSnackbarProps,
|
||||||
component: ChildComponent;
|
AuthenticationContextProps {
|
||||||
|
component:
|
||||||
|
| React.ComponentType<RouteComponentProps<any>>
|
||||||
|
| React.ComponentType<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type RenderComponent = (props: RouteComponentProps<any>) => React.ReactNode;
|
type RenderComponent = (props: RouteComponentProps<any>) => React.ReactNode;
|
||||||
|
|
||||||
export class AuthenticatedRoute extends React.Component<AuthenticatedRouteProps> {
|
export class AuthenticatedRoute extends React.Component<AuthenticatedRouteProps> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { enqueueSnackbar, authenticationContext, component: Component, ...rest } = this.props;
|
const {
|
||||||
|
enqueueSnackbar,
|
||||||
|
authenticationContext,
|
||||||
|
component: Component,
|
||||||
|
...rest
|
||||||
|
} = this.props;
|
||||||
const { location } = this.props;
|
const { location } = this.props;
|
||||||
const renderComponent: RenderComponent = (props) => {
|
const renderComponent: RenderComponent = (props) => {
|
||||||
if (authenticationContext.me) {
|
if (authenticationContext.me) {
|
||||||
return (
|
return (
|
||||||
<AuthenticatedContext.Provider value={authenticationContext as AuthenticatedContextValue}>
|
<AuthenticatedContext.Provider
|
||||||
|
value={authenticationContext as AuthenticatedContextValue}
|
||||||
|
>
|
||||||
<Component {...props} />
|
<Component {...props} />
|
||||||
</AuthenticatedContext.Provider>
|
</AuthenticatedContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Authentication.storeLoginRedirect(location);
|
Authentication.storeLoginRedirect(location);
|
||||||
enqueueSnackbar("Please sign in to continue.", { variant: 'info' });
|
enqueueSnackbar('Please sign in to continue', { variant: 'info' });
|
||||||
return (
|
return <Redirect to="/" />;
|
||||||
<Redirect to='/' />
|
};
|
||||||
);
|
return <Route {...rest} render={renderComponent} />;
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Route {...rest} render={renderComponent} />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withSnackbar(withAuthenticationContext(AuthenticatedRoute));
|
export default withSnackbar(withAuthenticationContext(AuthenticatedRoute));
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ export function clearLoginRedirect() {
|
|||||||
getStorage().removeItem(SIGN_IN_SEARCH);
|
getStorage().removeItem(SIGN_IN_SEARCH);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchLoginRedirect(features: Features): H.LocationDescriptorObject {
|
export function fetchLoginRedirect(
|
||||||
|
features: Features
|
||||||
|
): H.LocationDescriptorObject {
|
||||||
const signInPathname = getStorage().getItem(SIGN_IN_PATHNAME);
|
const signInPathname = getStorage().getItem(SIGN_IN_PATHNAME);
|
||||||
const signInSearch = getStorage().getItem(SIGN_IN_SEARCH);
|
const signInSearch = getStorage().getItem(SIGN_IN_SEARCH);
|
||||||
clearLoginRedirect();
|
clearLoginRedirect();
|
||||||
@@ -38,16 +40,19 @@ export function fetchLoginRedirect(features: Features): H.LocationDescriptorObje
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wraps the normal fetch routene with one with provides the access token if present.
|
* Wraps the normal fetch routine with one with provides the access token if present.
|
||||||
*/
|
*/
|
||||||
export function authorizedFetch(url: RequestInfo, params?: RequestInit): Promise<Response> {
|
export function authorizedFetch(
|
||||||
|
url: RequestInfo,
|
||||||
|
params?: RequestInit
|
||||||
|
): Promise<Response> {
|
||||||
const accessToken = getStorage().getItem(ACCESS_TOKEN);
|
const accessToken = getStorage().getItem(ACCESS_TOKEN);
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
params = params || {};
|
params = params || {};
|
||||||
params.credentials = 'include';
|
params.credentials = 'include';
|
||||||
params.headers = {
|
params.headers = {
|
||||||
...params.headers,
|
...params.headers,
|
||||||
"Authorization": 'Bearer ' + accessToken
|
Authorization: 'Bearer ' + accessToken
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return fetch(url, params);
|
return fetch(url, params);
|
||||||
@@ -55,26 +60,31 @@ export function authorizedFetch(url: RequestInfo, params?: RequestInit): Promise
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* fetch() does not yet support upload progress, this wrapper allows us to configure the xhr request
|
* fetch() does not yet support upload progress, this wrapper allows us to configure the xhr request
|
||||||
* for a single file upload and takes care of adding the Authroization header and redirecting on
|
* for a single file upload and takes care of adding the Authorization header and redirecting on
|
||||||
* authroization errors as we do for normal fetch operations.
|
* authorization errors as we do for normal fetch operations.
|
||||||
*/
|
*/
|
||||||
export function redirectingAuthorizedUpload(xhr: XMLHttpRequest, url: string, file: File, onProgress: (event: ProgressEvent<EventTarget>) => void): Promise<void> {
|
export function redirectingAuthorizedUpload(
|
||||||
|
xhr: XMLHttpRequest,
|
||||||
|
url: string,
|
||||||
|
file: File,
|
||||||
|
onProgress: (event: ProgressEvent<EventTarget>) => void
|
||||||
|
): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
xhr.open("POST", url, true);
|
xhr.open('POST', url, true);
|
||||||
const accessToken = getStorage().getItem(ACCESS_TOKEN);
|
const accessToken = getStorage().getItem(ACCESS_TOKEN);
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
xhr.withCredentials = true;
|
xhr.withCredentials = true;
|
||||||
xhr.setRequestHeader("Authorization", 'Bearer ' + accessToken);
|
xhr.setRequestHeader('Authorization', 'Bearer ' + accessToken);
|
||||||
}
|
}
|
||||||
xhr.upload.onprogress = onProgress;
|
xhr.upload.onprogress = onProgress;
|
||||||
xhr.onload = function () {
|
xhr.onload = function () {
|
||||||
if (xhr.status === 401 || xhr.status === 403) {
|
if (xhr.status === 401 || xhr.status === 403) {
|
||||||
history.push("/unauthorized");
|
history.push('/unauthorized');
|
||||||
} else {
|
} else {
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
xhr.onerror = function (event: ProgressEvent<EventTarget>) {
|
xhr.onerror = function () {
|
||||||
reject(new DOMException('Error', 'UploadError'));
|
reject(new DOMException('Error', 'UploadError'));
|
||||||
};
|
};
|
||||||
xhr.onabort = function () {
|
xhr.onabort = function () {
|
||||||
@@ -87,19 +97,24 @@ export function redirectingAuthorizedUpload(xhr: XMLHttpRequest, url: string, fi
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wraps the normal fetch routene which redirects on 401 response.
|
* Wraps the normal fetch routine which redirects on 401 response.
|
||||||
*/
|
*/
|
||||||
export function redirectingAuthorizedFetch(url: RequestInfo, params?: RequestInit): Promise<Response> {
|
export function redirectingAuthorizedFetch(
|
||||||
|
url: RequestInfo,
|
||||||
|
params?: RequestInit
|
||||||
|
): Promise<Response> {
|
||||||
return new Promise<Response>((resolve, reject) => {
|
return new Promise<Response>((resolve, reject) => {
|
||||||
authorizedFetch(url, params).then(response => {
|
authorizedFetch(url, params)
|
||||||
if (response.status === 401 || response.status === 403) {
|
.then((response) => {
|
||||||
history.push("/unauthorized");
|
if (response.status === 401 || response.status === 403) {
|
||||||
} else {
|
history.push('/unauthorized');
|
||||||
resolve(response);
|
} else {
|
||||||
}
|
resolve(response);
|
||||||
}).catch(error => {
|
}
|
||||||
reject(error);
|
})
|
||||||
});
|
.catch((error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import * as React from "react";
|
import * as React from 'react';
|
||||||
|
|
||||||
export interface Me {
|
export interface Me {
|
||||||
username: string;
|
username: string;
|
||||||
admin: boolean;
|
admin: boolean;
|
||||||
version: string; // proddy added
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthenticationContextValue {
|
export interface AuthenticationContextValue {
|
||||||
@@ -13,7 +12,7 @@ export interface AuthenticationContextValue {
|
|||||||
me?: Me;
|
me?: Me;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthenticationContextDefaultValue = {} as AuthenticationContextValue
|
const AuthenticationContextDefaultValue = {} as AuthenticationContextValue;
|
||||||
export const AuthenticationContext = React.createContext(
|
export const AuthenticationContext = React.createContext(
|
||||||
AuthenticationContextDefaultValue
|
AuthenticationContextDefaultValue
|
||||||
);
|
);
|
||||||
@@ -22,12 +21,21 @@ export interface AuthenticationContextProps {
|
|||||||
authenticationContext: AuthenticationContextValue;
|
authenticationContext: AuthenticationContextValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function withAuthenticationContext<T extends AuthenticationContextProps>(Component: React.ComponentType<T>) {
|
export function withAuthenticationContext<T extends AuthenticationContextProps>(
|
||||||
return class extends React.Component<Omit<T, keyof AuthenticationContextProps>> {
|
Component: React.ComponentType<T>
|
||||||
|
) {
|
||||||
|
return class extends React.Component<
|
||||||
|
Omit<T, keyof AuthenticationContextProps>
|
||||||
|
> {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<AuthenticationContext.Consumer>
|
<AuthenticationContext.Consumer>
|
||||||
{authenticationContext => <Component {...this.props as T} authenticationContext={authenticationContext} />}
|
{(authenticationContext) => (
|
||||||
|
<Component
|
||||||
|
{...(this.props as T)}
|
||||||
|
authenticationContext={authenticationContext}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</AuthenticationContext.Consumer>
|
</AuthenticationContext.Consumer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -38,7 +46,7 @@ export interface AuthenticatedContextValue extends AuthenticationContextValue {
|
|||||||
me: Me;
|
me: Me;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthenticatedContextDefaultValue = {} as AuthenticatedContextValue
|
const AuthenticatedContextDefaultValue = {} as AuthenticatedContextValue;
|
||||||
export const AuthenticatedContext = React.createContext(
|
export const AuthenticatedContext = React.createContext(
|
||||||
AuthenticatedContextDefaultValue
|
AuthenticatedContextDefaultValue
|
||||||
);
|
);
|
||||||
@@ -47,12 +55,21 @@ export interface AuthenticatedContextProps {
|
|||||||
authenticatedContext: AuthenticatedContextValue;
|
authenticatedContext: AuthenticatedContextValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function withAuthenticatedContext<T extends AuthenticatedContextProps>(Component: React.ComponentType<T>) {
|
export function withAuthenticatedContext<T extends AuthenticatedContextProps>(
|
||||||
return class extends React.Component<Omit<T, keyof AuthenticatedContextProps>> {
|
Component: React.ComponentType<T>
|
||||||
|
) {
|
||||||
|
return class extends React.Component<
|
||||||
|
Omit<T, keyof AuthenticatedContextProps>
|
||||||
|
> {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<AuthenticatedContext.Consumer>
|
<AuthenticatedContext.Consumer>
|
||||||
{authenticatedContext => <Component {...this.props as T} authenticatedContext={authenticatedContext} />}
|
{(authenticatedContext) => (
|
||||||
|
<Component
|
||||||
|
{...(this.props as T)}
|
||||||
|
authenticatedContext={authenticatedContext}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</AuthenticatedContext.Consumer>
|
</AuthenticatedContext.Consumer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,19 @@ import * as React from 'react';
|
|||||||
import { withSnackbar, WithSnackbarProps } from 'notistack';
|
import { withSnackbar, WithSnackbarProps } from 'notistack';
|
||||||
import jwtDecode from 'jwt-decode';
|
import jwtDecode from 'jwt-decode';
|
||||||
|
|
||||||
import history from '../history'
|
import history from '../history';
|
||||||
import { VERIFY_AUTHORIZATION_ENDPOINT } from '../api';
|
import { VERIFY_AUTHORIZATION_ENDPOINT } from '../api';
|
||||||
import { ACCESS_TOKEN, authorizedFetch, getStorage } from './Authentication';
|
import { ACCESS_TOKEN, authorizedFetch, getStorage } from './Authentication';
|
||||||
import { AuthenticationContext, AuthenticationContextValue, Me } from './AuthenticationContext';
|
import {
|
||||||
|
AuthenticationContext,
|
||||||
|
AuthenticationContextValue,
|
||||||
|
Me
|
||||||
|
} from './AuthenticationContext';
|
||||||
import FullScreenLoading from '../components/FullScreenLoading';
|
import FullScreenLoading from '../components/FullScreenLoading';
|
||||||
import { withFeatures, WithFeaturesProps } from '../features/FeaturesContext';
|
import { withFeatures, WithFeaturesProps } from '../features/FeaturesContext';
|
||||||
|
|
||||||
export const decodeMeJWT = (accessToken: string): Me => jwtDecode(accessToken) as Me;
|
export const decodeMeJWT = (accessToken: string): Me =>
|
||||||
|
jwtDecode(accessToken) as Me;
|
||||||
|
|
||||||
interface AuthenticationWrapperState {
|
interface AuthenticationWrapperState {
|
||||||
context: AuthenticationContextValue;
|
context: AuthenticationContextValue;
|
||||||
@@ -18,15 +23,17 @@ interface AuthenticationWrapperState {
|
|||||||
|
|
||||||
type AuthenticationWrapperProps = WithSnackbarProps & WithFeaturesProps;
|
type AuthenticationWrapperProps = WithSnackbarProps & WithFeaturesProps;
|
||||||
|
|
||||||
class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps, AuthenticationWrapperState> {
|
class AuthenticationWrapper extends React.Component<
|
||||||
|
AuthenticationWrapperProps,
|
||||||
|
AuthenticationWrapperState
|
||||||
|
> {
|
||||||
constructor(props: AuthenticationWrapperProps) {
|
constructor(props: AuthenticationWrapperProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
context: {
|
context: {
|
||||||
refresh: this.refresh,
|
refresh: this.refresh,
|
||||||
signIn: this.signIn,
|
signIn: this.signIn,
|
||||||
signOut: this.signOut,
|
signOut: this.signOut
|
||||||
},
|
},
|
||||||
initialized: false
|
initialized: false
|
||||||
};
|
};
|
||||||
@@ -39,7 +46,9 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{this.state.initialized ? this.renderContent() : this.renderContentLoading()}
|
{this.state.initialized
|
||||||
|
? this.renderContent()
|
||||||
|
: this.renderContentLoading()}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -53,9 +62,7 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderContentLoading() {
|
renderContentLoading() {
|
||||||
return (
|
return <FullScreenLoading />;
|
||||||
<FullScreenLoading />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
refresh = () => {
|
refresh = () => {
|
||||||
@@ -64,34 +71,53 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
|
|||||||
// this.setState({ initialized: true, context: { ...this.state.context, me: { admin: true, username: "admin" } } });
|
// this.setState({ initialized: true, context: { ...this.state.context, me: { admin: true, username: "admin" } } });
|
||||||
// return;
|
// return;
|
||||||
// }
|
// }
|
||||||
const accessToken = getStorage().getItem(ACCESS_TOKEN)
|
const accessToken = getStorage().getItem(ACCESS_TOKEN);
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
authorizedFetch(VERIFY_AUTHORIZATION_ENDPOINT)
|
authorizedFetch(VERIFY_AUTHORIZATION_ENDPOINT)
|
||||||
.then(response => {
|
.then((response) => {
|
||||||
const me = response.status === 200 ? decodeMeJWT(accessToken) : undefined;
|
const me =
|
||||||
this.setState({ initialized: true, context: { ...this.state.context, me } });
|
response.status === 200 ? decodeMeJWT(accessToken) : undefined;
|
||||||
}).catch(error => {
|
this.setState({
|
||||||
this.setState({ initialized: true, context: { ...this.state.context, me: undefined } });
|
initialized: true,
|
||||||
this.props.enqueueSnackbar("Error verifying authorization: " + error.message, {
|
context: { ...this.state.context, me }
|
||||||
variant: 'error',
|
|
||||||
});
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.setState({
|
||||||
|
initialized: true,
|
||||||
|
context: { ...this.state.context, me: undefined }
|
||||||
|
});
|
||||||
|
this.props.enqueueSnackbar(
|
||||||
|
'Error verifying authorization: ' + error.message,
|
||||||
|
{
|
||||||
|
variant: 'error'
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.setState({ initialized: true, context: { ...this.state.context, me: undefined } });
|
this.setState({
|
||||||
|
initialized: true,
|
||||||
|
context: { ...this.state.context, me: undefined }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
signIn = (accessToken: string) => {
|
signIn = (accessToken: string) => {
|
||||||
try {
|
try {
|
||||||
getStorage().setItem(ACCESS_TOKEN, accessToken);
|
getStorage().setItem(ACCESS_TOKEN, accessToken);
|
||||||
const me: Me = decodeMeJWT(accessToken);
|
const me: Me = decodeMeJWT(accessToken);
|
||||||
this.setState({ context: { ...this.state.context, me } });
|
this.setState({ context: { ...this.state.context, me } });
|
||||||
this.props.enqueueSnackbar(`Logged in as ${me.username}`, { variant: 'success' });
|
this.props.enqueueSnackbar(`Logged in as ${me.username}`, {
|
||||||
|
variant: 'success'
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState({ initialized: true, context: { ...this.state.context, me: undefined } });
|
this.setState({
|
||||||
throw new Error("Failed to parse JWT " + err.message);
|
initialized: true,
|
||||||
|
context: { ...this.state.context, me: undefined }
|
||||||
|
});
|
||||||
|
throw new Error('Failed to parse JWT ' + err.message);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
signOut = () => {
|
signOut = () => {
|
||||||
getStorage().removeItem(ACCESS_TOKEN);
|
getStorage().removeItem(ACCESS_TOKEN);
|
||||||
@@ -101,10 +127,9 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
|
|||||||
me: undefined
|
me: undefined
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.props.enqueueSnackbar("You have signed out.", { variant: 'success', });
|
this.props.enqueueSnackbar('You have signed out', { variant: 'success' });
|
||||||
history.push('/');
|
history.push('/');
|
||||||
}
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withFeatures(withSnackbar(AuthenticationWrapper))
|
export default withFeatures(withSnackbar(AuthenticationWrapper));
|
||||||
|
|||||||
@@ -1,29 +1,34 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Redirect, Route, RouteProps, RouteComponentProps } from "react-router-dom";
|
import {
|
||||||
|
Redirect,
|
||||||
|
Route,
|
||||||
|
RouteProps,
|
||||||
|
RouteComponentProps
|
||||||
|
} from 'react-router-dom';
|
||||||
|
|
||||||
import { withAuthenticationContext, AuthenticationContextProps } from './AuthenticationContext';
|
import {
|
||||||
|
withAuthenticationContext,
|
||||||
|
AuthenticationContextProps
|
||||||
|
} from './AuthenticationContext';
|
||||||
import * as Authentication from './Authentication';
|
import * as Authentication from './Authentication';
|
||||||
import { WithFeaturesProps, withFeatures } from '../features/FeaturesContext';
|
import { WithFeaturesProps, withFeatures } from '../features/FeaturesContext';
|
||||||
|
|
||||||
interface UnauthenticatedRouteProps extends RouteProps, AuthenticationContextProps, WithFeaturesProps {
|
interface UnauthenticatedRouteProps
|
||||||
component: React.ComponentType<RouteComponentProps<any>> | React.ComponentType<any>;
|
extends RouteProps,
|
||||||
|
AuthenticationContextProps,
|
||||||
|
WithFeaturesProps {
|
||||||
|
component:
|
||||||
|
| React.ComponentType<RouteComponentProps<any>>
|
||||||
|
| React.ComponentType<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type RenderComponent = (props: RouteComponentProps<any>) => React.ReactNode;
|
|
||||||
|
|
||||||
class UnauthenticatedRoute extends Route<UnauthenticatedRouteProps> {
|
class UnauthenticatedRoute extends Route<UnauthenticatedRouteProps> {
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const { authenticationContext, component: Component, features, ...rest } = this.props;
|
const { authenticationContext, features, ...rest } = this.props;
|
||||||
const renderComponent: RenderComponent = (props) => {
|
if (authenticationContext.me) {
|
||||||
if (authenticationContext.me) {
|
return <Redirect to={Authentication.fetchLoginRedirect(features)} />;
|
||||||
return (<Redirect to={Authentication.fetchLoginRedirect(features)} />);
|
|
||||||
}
|
|
||||||
return (<Component {...props} />);
|
|
||||||
}
|
}
|
||||||
return (
|
return <Route {...rest} />;
|
||||||
<Route {...rest} render={renderComponent} />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,25 @@
|
|||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { makeStyles } from '@material-ui/styles';
|
import { makeStyles } from '@material-ui/styles';
|
||||||
import { Paper, Typography, Box, CssBaseline } from "@material-ui/core";
|
import { Paper, Typography, Box, CssBaseline } from '@material-ui/core';
|
||||||
import WarningIcon from "@material-ui/icons/Warning"
|
import WarningIcon from '@material-ui/icons/Warning';
|
||||||
|
|
||||||
const styles = makeStyles(
|
const styles = makeStyles({
|
||||||
{
|
siteErrorPage: {
|
||||||
siteErrorPage: {
|
display: 'flex',
|
||||||
display: "flex",
|
height: '100vh',
|
||||||
height: "100vh",
|
justifyContent: 'center',
|
||||||
justifyContent: "center",
|
flexDirection: 'column'
|
||||||
flexDirection: "column"
|
},
|
||||||
},
|
siteErrorPagePanel: {
|
||||||
siteErrorPagePanel: {
|
textAlign: 'center',
|
||||||
textAlign: "center",
|
padding: '280px 0 40px 0',
|
||||||
padding: "280px 0 40px 0",
|
backgroundImage: 'url("/app/icon.png")',
|
||||||
backgroundImage: 'url("/app/icon.png")',
|
backgroundRepeat: 'no-repeat',
|
||||||
backgroundRepeat: "no-repeat",
|
backgroundPosition: '50% 40px',
|
||||||
backgroundPosition: "50% 40px",
|
backgroundSize: '200px auto',
|
||||||
backgroundSize: "200px auto",
|
width: '100%'
|
||||||
width: "100%",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
|
|
||||||
interface ApplicationErrorProps {
|
interface ApplicationErrorProps {
|
||||||
error?: string;
|
error?: string;
|
||||||
@@ -33,27 +31,29 @@ const ApplicationError: FC<ApplicationErrorProps> = ({ error }) => {
|
|||||||
<div className={classes.siteErrorPage}>
|
<div className={classes.siteErrorPage}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<Paper className={classes.siteErrorPagePanel} elevation={10}>
|
<Paper className={classes.siteErrorPagePanel} elevation={10}>
|
||||||
<Box display="flex" flexDirection="row" justifyContent="center" alignItems="center" mb={2}>
|
<Box
|
||||||
|
display="flex"
|
||||||
|
flexDirection="row"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
mb={2}
|
||||||
|
>
|
||||||
<WarningIcon fontSize="large" color="error" />
|
<WarningIcon fontSize="large" color="error" />
|
||||||
<Box ml={2}>
|
<Box ml={2}>
|
||||||
<Typography variant="h4">
|
<Typography variant="h4">Application error</Typography>
|
||||||
Application error
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Typography variant="subtitle1" gutterBottom>
|
<Typography variant="subtitle1" gutterBottom>
|
||||||
Failed to configure the application, please refresh to try again.
|
Failed to configure the application, please refresh to try again.
|
||||||
</Typography>
|
</Typography>
|
||||||
{error &&
|
{error && (
|
||||||
(
|
<Typography variant="subtitle2" gutterBottom>
|
||||||
<Typography variant="subtitle2" gutterBottom>
|
Error: {error}
|
||||||
Error: {error}
|
</Typography>
|
||||||
</Typography>
|
)}
|
||||||
)
|
|
||||||
}
|
|
||||||
</Paper>
|
</Paper>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ApplicationError;
|
export default ApplicationError;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import React, { FC } from "react";
|
import { FC } from 'react';
|
||||||
import { FormControlLabel, FormControlLabelProps } from "@material-ui/core";
|
import { FormControlLabel, FormControlLabelProps } from '@material-ui/core';
|
||||||
|
|
||||||
const BlockFormControlLabel: FC<FormControlLabelProps> = (props) => (
|
const BlockFormControlLabel: FC<FormControlLabelProps> = (props) => (
|
||||||
<div>
|
<div>
|
||||||
<FormControlLabel {...props} />
|
<FormControlLabel {...props} />
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
|
|
||||||
export default BlockFormControlLabel;
|
export default BlockFormControlLabel;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Button, styled } from "@material-ui/core";
|
import { Button, styled } from '@material-ui/core';
|
||||||
|
|
||||||
const ErrorButton = styled(Button)(({ theme }) => ({
|
const ErrorButton = styled(Button)(({ theme }) => ({
|
||||||
color: theme.palette.getContrastText(theme.palette.error.main),
|
color: theme.palette.getContrastText(theme.palette.error.main),
|
||||||
backgroundColor: theme.palette.error.main,
|
backgroundColor: theme.palette.error.main,
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
backgroundColor: theme.palette.error.dark,
|
backgroundColor: theme.palette.error.dark
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { styled, Box } from "@material-ui/core";
|
import { styled, Box } from '@material-ui/core';
|
||||||
|
|
||||||
const FormActions = styled(Box)(({ theme }) => ({
|
const FormActions = styled(Box)(({ theme }) => ({
|
||||||
marginTop: theme.spacing(1)
|
marginTop: theme.spacing(1)
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Button, styled } from "@material-ui/core";
|
import { Button, styled } from '@material-ui/core';
|
||||||
|
|
||||||
const FormButton = styled(Button)(({ theme }) => ({
|
const FormButton = styled(Button)(({ theme }) => ({
|
||||||
margin: theme.spacing(0, 1),
|
margin: theme.spacing(0, 1),
|
||||||
'&:last-child': {
|
'&:last-child': {
|
||||||
marginRight: 0,
|
marginRight: 0
|
||||||
},
|
},
|
||||||
'&:first-child': {
|
'&:first-child': {
|
||||||
marginLeft: 0,
|
marginLeft: 0
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
56
interface/src/components/FormLoader.tsx
Normal file
56
interface/src/components/FormLoader.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
|
|
||||||
|
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles';
|
||||||
|
import { Button, LinearProgress, Typography } from '@material-ui/core';
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
|
loadingSettings: {
|
||||||
|
margin: theme.spacing(0.5)
|
||||||
|
},
|
||||||
|
loadingSettingsDetails: {
|
||||||
|
margin: theme.spacing(4),
|
||||||
|
textAlign: 'center'
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
marginRight: theme.spacing(2),
|
||||||
|
marginTop: theme.spacing(2)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
interface FormLoaderProps {
|
||||||
|
errorMessage?: string;
|
||||||
|
loadData: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FormLoader: FC<FormLoaderProps> = ({ errorMessage, loadData }) => {
|
||||||
|
const classes = useStyles();
|
||||||
|
if (errorMessage) {
|
||||||
|
return (
|
||||||
|
<div className={classes.loadingSettings}>
|
||||||
|
<Typography variant="h6" className={classes.loadingSettingsDetails}>
|
||||||
|
{errorMessage}
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
className={classes.button}
|
||||||
|
onClick={loadData}
|
||||||
|
>
|
||||||
|
Retry
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={classes.loadingSettings}>
|
||||||
|
<LinearProgress className={classes.loadingSettingsDetails} />
|
||||||
|
<Typography variant="h6" className={classes.loadingSettingsDetails}>
|
||||||
|
Loading…
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FormLoader;
|
||||||
@@ -3,30 +3,30 @@ import CircularProgress from '@material-ui/core/CircularProgress';
|
|||||||
import { Typography, Theme } from '@material-ui/core';
|
import { Typography, Theme } from '@material-ui/core';
|
||||||
import { makeStyles, createStyles } from '@material-ui/styles';
|
import { makeStyles, createStyles } from '@material-ui/styles';
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) => createStyles({
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
fullScreenLoading: {
|
createStyles({
|
||||||
padding: theme.spacing(2),
|
fullScreenLoading: {
|
||||||
display: "flex",
|
padding: theme.spacing(2),
|
||||||
alignItems: "center",
|
display: 'flex',
|
||||||
justifyContent: "center",
|
alignItems: 'center',
|
||||||
height: "100vh",
|
justifyContent: 'center',
|
||||||
flexDirection: "column"
|
height: '100vh',
|
||||||
},
|
flexDirection: 'column'
|
||||||
progress: {
|
},
|
||||||
margin: theme.spacing(4),
|
progress: {
|
||||||
}
|
margin: theme.spacing(4)
|
||||||
}));
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const FullScreenLoading = () => {
|
const FullScreenLoading = () => {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
return (
|
return (
|
||||||
<div className={classes.fullScreenLoading}>
|
<div className={classes.fullScreenLoading}>
|
||||||
<CircularProgress className={classes.progress} size={100} />
|
<CircularProgress className={classes.progress} size={100} />
|
||||||
<Typography variant="h4">
|
<Typography variant="h4">Loading…</Typography>
|
||||||
Loading…
|
|
||||||
</Typography>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default FullScreenLoading;
|
export default FullScreenLoading;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Avatar, makeStyles } from "@material-ui/core";
|
import { Avatar, makeStyles } from '@material-ui/core';
|
||||||
import React, { FC } from "react";
|
import { FC } from 'react';
|
||||||
|
|
||||||
interface HighlightAvatarProps {
|
interface HighlightAvatarProps {
|
||||||
color: string;
|
color: string;
|
||||||
@@ -13,11 +13,7 @@ const useStyles = makeStyles({
|
|||||||
|
|
||||||
const HighlightAvatar: FC<HighlightAvatarProps> = (props) => {
|
const HighlightAvatar: FC<HighlightAvatarProps> = (props) => {
|
||||||
const classes = useStyles(props);
|
const classes = useStyles(props);
|
||||||
return (
|
return <Avatar className={classes.root}>{props.children}</Avatar>;
|
||||||
<Avatar className={classes.root}>
|
};
|
||||||
{props.children}
|
|
||||||
</Avatar>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default HighlightAvatar;
|
export default HighlightAvatar;
|
||||||
|
|||||||
@@ -1,14 +1,41 @@
|
|||||||
import React, { RefObject, Fragment } from 'react';
|
import React, { RefObject, Fragment } from 'react';
|
||||||
import { Link, withRouter, RouteComponentProps } from 'react-router-dom';
|
import { Link, withRouter, RouteComponentProps } from 'react-router-dom';
|
||||||
|
|
||||||
import { Drawer, AppBar, Toolbar, Avatar, Divider, Button, Box, IconButton } from '@material-ui/core';
|
import {
|
||||||
import { ClickAwayListener, Popper, Hidden, Typography } from '@material-ui/core';
|
Drawer,
|
||||||
import { List, ListItem, ListItemIcon, ListItemText, ListItemAvatar } from '@material-ui/core';
|
AppBar,
|
||||||
|
Toolbar,
|
||||||
|
Avatar,
|
||||||
|
Divider,
|
||||||
|
Button,
|
||||||
|
Box,
|
||||||
|
IconButton
|
||||||
|
} from '@material-ui/core';
|
||||||
|
import {
|
||||||
|
ClickAwayListener,
|
||||||
|
Popper,
|
||||||
|
Hidden,
|
||||||
|
Typography
|
||||||
|
} from '@material-ui/core';
|
||||||
|
import {
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemIcon,
|
||||||
|
ListItemText,
|
||||||
|
ListItemAvatar
|
||||||
|
} from '@material-ui/core';
|
||||||
import { Card, CardContent, CardActions } from '@material-ui/core';
|
import { Card, CardContent, CardActions } from '@material-ui/core';
|
||||||
|
|
||||||
import { withStyles, createStyles, Theme, WithTheme, WithStyles, withTheme } from '@material-ui/core/styles';
|
import {
|
||||||
|
withStyles,
|
||||||
|
createStyles,
|
||||||
|
Theme,
|
||||||
|
WithTheme,
|
||||||
|
WithStyles,
|
||||||
|
withTheme
|
||||||
|
} from '@material-ui/core/styles';
|
||||||
|
|
||||||
import WifiIcon from '@material-ui/icons/Wifi';
|
import SettingsEthernetIcon from '@material-ui/icons/SettingsEthernet';
|
||||||
import SettingsIcon from '@material-ui/icons/Settings';
|
import SettingsIcon from '@material-ui/icons/Settings';
|
||||||
import AccessTimeIcon from '@material-ui/icons/AccessTime';
|
import AccessTimeIcon from '@material-ui/icons/AccessTime';
|
||||||
import AccountCircleIcon from '@material-ui/icons/AccountCircle';
|
import AccountCircleIcon from '@material-ui/icons/AccountCircle';
|
||||||
@@ -19,76 +46,84 @@ import MenuIcon from '@material-ui/icons/Menu';
|
|||||||
|
|
||||||
import ProjectMenu from '../project/ProjectMenu';
|
import ProjectMenu from '../project/ProjectMenu';
|
||||||
import { PROJECT_NAME } from '../api';
|
import { PROJECT_NAME } from '../api';
|
||||||
import { withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
|
import {
|
||||||
|
withAuthenticatedContext,
|
||||||
|
AuthenticatedContextProps
|
||||||
|
} from '../authentication';
|
||||||
import { withFeatures, WithFeaturesProps } from '../features/FeaturesContext';
|
import { withFeatures, WithFeaturesProps } from '../features/FeaturesContext';
|
||||||
|
|
||||||
const drawerWidth = 290;
|
const drawerWidth = 290;
|
||||||
|
|
||||||
const styles = (theme: Theme) => createStyles({
|
const styles = (theme: Theme) =>
|
||||||
root: {
|
createStyles({
|
||||||
display: 'flex',
|
root: {
|
||||||
},
|
display: 'flex'
|
||||||
drawer: {
|
|
||||||
[theme.breakpoints.up('md')]: {
|
|
||||||
width: drawerWidth,
|
|
||||||
flexShrink: 0,
|
|
||||||
},
|
},
|
||||||
},
|
drawer: {
|
||||||
title: {
|
[theme.breakpoints.up('md')]: {
|
||||||
flexGrow: 1
|
width: drawerWidth,
|
||||||
},
|
flexShrink: 0
|
||||||
appBar: {
|
}
|
||||||
marginLeft: drawerWidth,
|
|
||||||
[theme.breakpoints.up('md')]: {
|
|
||||||
width: `calc(100% - ${drawerWidth}px)`,
|
|
||||||
},
|
},
|
||||||
},
|
title: {
|
||||||
toolbarImage: {
|
flexGrow: 1
|
||||||
[theme.breakpoints.up('xs')]: {
|
|
||||||
height: 24,
|
|
||||||
marginRight: theme.spacing(2)
|
|
||||||
},
|
},
|
||||||
[theme.breakpoints.up('sm')]: {
|
appBar: {
|
||||||
height: 36,
|
marginLeft: drawerWidth,
|
||||||
marginRight: theme.spacing(3)
|
[theme.breakpoints.up('md')]: {
|
||||||
|
width: `calc(100% - ${drawerWidth}px)`
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
toolbarImage: {
|
||||||
menuButton: {
|
[theme.breakpoints.up('xs')]: {
|
||||||
marginRight: theme.spacing(2),
|
height: 24,
|
||||||
[theme.breakpoints.up('md')]: {
|
marginRight: theme.spacing(2)
|
||||||
display: 'none',
|
},
|
||||||
|
[theme.breakpoints.up('sm')]: {
|
||||||
|
height: 36,
|
||||||
|
marginRight: theme.spacing(3)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
menuButton: {
|
||||||
toolbar: theme.mixins.toolbar,
|
marginRight: theme.spacing(2),
|
||||||
drawerPaper: {
|
[theme.breakpoints.up('md')]: {
|
||||||
width: drawerWidth,
|
display: 'none'
|
||||||
},
|
}
|
||||||
content: {
|
},
|
||||||
flexGrow: 1
|
toolbar: theme.mixins.toolbar,
|
||||||
},
|
drawerPaper: {
|
||||||
authMenu: {
|
width: drawerWidth
|
||||||
zIndex: theme.zIndex.tooltip,
|
},
|
||||||
maxWidth: 400,
|
content: {
|
||||||
},
|
flexGrow: 1
|
||||||
authMenuActions: {
|
},
|
||||||
padding: theme.spacing(2),
|
authMenu: {
|
||||||
"& > * + *": {
|
zIndex: theme.zIndex.tooltip,
|
||||||
marginLeft: theme.spacing(2),
|
maxWidth: 400
|
||||||
|
},
|
||||||
|
authMenuActions: {
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
'& > * + *': {
|
||||||
|
marginLeft: theme.spacing(2)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
});
|
|
||||||
|
|
||||||
interface MenuAppBarState {
|
interface MenuAppBarState {
|
||||||
mobileOpen: boolean;
|
mobileOpen: boolean;
|
||||||
authMenuOpen: boolean;
|
authMenuOpen: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MenuAppBarProps extends WithFeaturesProps, AuthenticatedContextProps, WithTheme, WithStyles<typeof styles>, RouteComponentProps {
|
interface MenuAppBarProps
|
||||||
|
extends WithFeaturesProps,
|
||||||
|
AuthenticatedContextProps,
|
||||||
|
WithTheme,
|
||||||
|
WithStyles<typeof styles>,
|
||||||
|
RouteComponentProps {
|
||||||
sectionTitle: string;
|
sectionTitle: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
||||||
|
|
||||||
constructor(props: MenuAppBarProps) {
|
constructor(props: MenuAppBarProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
@@ -101,38 +136,48 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
|||||||
|
|
||||||
handleToggle = () => {
|
handleToggle = () => {
|
||||||
this.setState({ authMenuOpen: !this.state.authMenuOpen });
|
this.setState({ authMenuOpen: !this.state.authMenuOpen });
|
||||||
}
|
};
|
||||||
|
|
||||||
handleClose = (event: React.MouseEvent<Document>) => {
|
handleClose = (event: React.MouseEvent<Document>) => {
|
||||||
if (this.anchorRef.current && this.anchorRef.current.contains(event.currentTarget)) {
|
if (
|
||||||
|
this.anchorRef.current &&
|
||||||
|
this.anchorRef.current.contains(event.currentTarget)
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setState({ authMenuOpen: false });
|
this.setState({ authMenuOpen: false });
|
||||||
}
|
};
|
||||||
|
|
||||||
handleDrawerToggle = () => {
|
handleDrawerToggle = () => {
|
||||||
this.setState({ mobileOpen: !this.state.mobileOpen });
|
this.setState({ mobileOpen: !this.state.mobileOpen });
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { classes, theme, children, sectionTitle, authenticatedContext, features } = this.props;
|
const {
|
||||||
|
classes,
|
||||||
|
theme,
|
||||||
|
children,
|
||||||
|
sectionTitle,
|
||||||
|
authenticatedContext,
|
||||||
|
features
|
||||||
|
} = this.props;
|
||||||
const { mobileOpen, authMenuOpen } = this.state;
|
const { mobileOpen, authMenuOpen } = this.state;
|
||||||
const path = this.props.match.url;
|
const path = this.props.match.url;
|
||||||
const drawer = (
|
const drawer = (
|
||||||
<div>
|
<div>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<Box display="flex">
|
<Box display="flex">
|
||||||
<img src="/app/icon.png" className={classes.toolbarImage} alt={PROJECT_NAME} />
|
<img
|
||||||
|
src="/app/icon.png"
|
||||||
|
className={classes.toolbarImage}
|
||||||
|
alt={PROJECT_NAME}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Typography variant="h6" color="textPrimary">
|
<Typography variant="h6" color="textPrimary">
|
||||||
{PROJECT_NAME}
|
{PROJECT_NAME}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography align="right" variant="caption" color="textPrimary">
|
|
||||||
v{authenticatedContext.me.version}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Divider absolute />
|
<Divider absolute />
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
|
|
||||||
@@ -144,20 +189,35 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<List>
|
<List>
|
||||||
<ListItem to='/network/' selected={path.startsWith('/network/')} button component={Link}>
|
<ListItem
|
||||||
|
to="/network/"
|
||||||
|
selected={path.startsWith('/network/')}
|
||||||
|
button
|
||||||
|
component={Link}
|
||||||
|
>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<WifiIcon />
|
<SettingsEthernetIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary="Network Connection" />
|
<ListItemText primary="Network Connection" />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem to='/ap/' selected={path.startsWith('/ap/')} button component={Link}>
|
<ListItem
|
||||||
|
to="/ap/"
|
||||||
|
selected={path.startsWith('/ap/')}
|
||||||
|
button
|
||||||
|
component={Link}
|
||||||
|
>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<SettingsInputAntennaIcon />
|
<SettingsInputAntennaIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary="Access Point" />
|
<ListItemText primary="Access Point" />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
{features.ntp && (
|
{features.ntp && (
|
||||||
<ListItem to='/ntp/' selected={path.startsWith('/ntp/')} button component={Link}>
|
<ListItem
|
||||||
|
to="/ntp/"
|
||||||
|
selected={path.startsWith('/ntp/')}
|
||||||
|
button
|
||||||
|
component={Link}
|
||||||
|
>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<AccessTimeIcon />
|
<AccessTimeIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
@@ -165,7 +225,12 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
)}
|
)}
|
||||||
{features.mqtt && (
|
{features.mqtt && (
|
||||||
<ListItem to='/mqtt/' selected={path.startsWith('/mqtt/')} button component={Link}>
|
<ListItem
|
||||||
|
to="/mqtt/"
|
||||||
|
selected={path.startsWith('/mqtt/')}
|
||||||
|
button
|
||||||
|
component={Link}
|
||||||
|
>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<DeviceHubIcon />
|
<DeviceHubIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
@@ -173,14 +238,25 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
)}
|
)}
|
||||||
{features.security && (
|
{features.security && (
|
||||||
<ListItem to='/security/' selected={path.startsWith('/security/')} button component={Link} disabled={!authenticatedContext.me.admin}>
|
<ListItem
|
||||||
|
to="/security/"
|
||||||
|
selected={path.startsWith('/security/')}
|
||||||
|
button
|
||||||
|
component={Link}
|
||||||
|
disabled={!authenticatedContext.me.admin}
|
||||||
|
>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<LockIcon />
|
<LockIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary="Security" />
|
<ListItemText primary="Security" />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
)}
|
)}
|
||||||
<ListItem to='/system/' selected={path.startsWith('/system/')} button component={Link} >
|
<ListItem
|
||||||
|
to="/system/"
|
||||||
|
selected={path.startsWith('/system/')}
|
||||||
|
button
|
||||||
|
component={Link}
|
||||||
|
>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<SettingsIcon />
|
<SettingsIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
@@ -201,7 +277,12 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
|||||||
>
|
>
|
||||||
<AccountCircleIcon />
|
<AccountCircleIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Popper open={authMenuOpen} anchorEl={this.anchorRef.current} transition className={classes.authMenu}>
|
<Popper
|
||||||
|
open={authMenuOpen}
|
||||||
|
anchorEl={this.anchorRef.current}
|
||||||
|
transition
|
||||||
|
className={classes.authMenu}
|
||||||
|
>
|
||||||
<ClickAwayListener onClickAway={this.handleClose}>
|
<ClickAwayListener onClickAway={this.handleClose}>
|
||||||
<Card id="menu-list-grow">
|
<Card id="menu-list-grow">
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -212,13 +293,27 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
|||||||
<AccountCircleIcon />
|
<AccountCircleIcon />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary={"Signed in as: " + authenticatedContext.me.username} secondary={authenticatedContext.me.admin ? "Admin User" : undefined} />
|
<ListItemText
|
||||||
|
primary={
|
||||||
|
'Signed in as: ' + authenticatedContext.me.username
|
||||||
|
}
|
||||||
|
secondary={
|
||||||
|
authenticatedContext.me.admin ? 'Admin User' : undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</List>
|
</List>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<Divider />
|
<Divider />
|
||||||
<CardActions className={classes.authMenuActions}>
|
<CardActions className={classes.authMenuActions}>
|
||||||
<Button variant="contained" fullWidth color="primary" onClick={authenticatedContext.signOut}>Sign Out</Button>
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
fullWidth
|
||||||
|
color="primary"
|
||||||
|
onClick={authenticatedContext.signOut}
|
||||||
|
>
|
||||||
|
Sign Out
|
||||||
|
</Button>
|
||||||
</CardActions>
|
</CardActions>
|
||||||
</Card>
|
</Card>
|
||||||
</ClickAwayListener>
|
</ClickAwayListener>
|
||||||
@@ -239,7 +334,12 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
|||||||
>
|
>
|
||||||
<MenuIcon />
|
<MenuIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Typography variant="h6" color="inherit" noWrap className={classes.title}>
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
color="inherit"
|
||||||
|
noWrap
|
||||||
|
className={classes.title}
|
||||||
|
>
|
||||||
{sectionTitle}
|
{sectionTitle}
|
||||||
</Typography>
|
</Typography>
|
||||||
{features.security && userMenu}
|
{features.security && userMenu}
|
||||||
@@ -253,10 +353,10 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
|||||||
open={mobileOpen}
|
open={mobileOpen}
|
||||||
onClose={this.handleDrawerToggle}
|
onClose={this.handleDrawerToggle}
|
||||||
classes={{
|
classes={{
|
||||||
paper: classes.drawerPaper,
|
paper: classes.drawerPaper
|
||||||
}}
|
}}
|
||||||
ModalProps={{
|
ModalProps={{
|
||||||
keepMounted: true,
|
keepMounted: true
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{drawer}
|
{drawer}
|
||||||
@@ -265,7 +365,7 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
|||||||
<Hidden smDown implementation="css">
|
<Hidden smDown implementation="css">
|
||||||
<Drawer
|
<Drawer
|
||||||
classes={{
|
classes={{
|
||||||
paper: classes.drawerPaper,
|
paper: classes.drawerPaper
|
||||||
}}
|
}}
|
||||||
variant="permanent"
|
variant="permanent"
|
||||||
open
|
open
|
||||||
@@ -285,10 +385,6 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
|||||||
|
|
||||||
export default withRouter(
|
export default withRouter(
|
||||||
withTheme(
|
withTheme(
|
||||||
withFeatures(
|
withFeatures(withAuthenticatedContext(withStyles(styles)(MenuAppBar)))
|
||||||
withAuthenticatedContext(
|
|
||||||
withStyles(styles)(MenuAppBar)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,26 +1,32 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { TextValidator, ValidatorComponentProps } from 'react-material-ui-form-validator';
|
import {
|
||||||
|
TextValidator,
|
||||||
|
ValidatorComponentProps
|
||||||
|
} from 'react-material-ui-form-validator';
|
||||||
|
|
||||||
import { withStyles, WithStyles, createStyles } from '@material-ui/core/styles';
|
import { withStyles, WithStyles, createStyles } from '@material-ui/core/styles';
|
||||||
import { InputAdornment, IconButton } from '@material-ui/core';
|
import { InputAdornment, IconButton } from '@material-ui/core';
|
||||||
import {Visibility,VisibilityOff } from '@material-ui/icons';
|
import { Visibility, VisibilityOff } from '@material-ui/icons';
|
||||||
|
|
||||||
const styles = createStyles({
|
const styles = createStyles({
|
||||||
input: {
|
input: {
|
||||||
"&::-ms-reveal": {
|
'&::-ms-reveal': {
|
||||||
display: "none"
|
display: 'none'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
type PasswordValidatorProps = WithStyles<typeof styles> & Exclude<ValidatorComponentProps, "type" | "InputProps">;
|
type PasswordValidatorProps = WithStyles<typeof styles> &
|
||||||
|
Exclude<ValidatorComponentProps, 'type' | 'InputProps'>;
|
||||||
|
|
||||||
interface PasswordValidatorState {
|
interface PasswordValidatorState {
|
||||||
showPassword: boolean;
|
showPassword: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class PasswordValidator extends React.Component<PasswordValidatorProps, PasswordValidatorState> {
|
class PasswordValidator extends React.Component<
|
||||||
|
PasswordValidatorProps,
|
||||||
|
PasswordValidatorState
|
||||||
|
> {
|
||||||
state = {
|
state = {
|
||||||
showPassword: false
|
showPassword: false
|
||||||
};
|
};
|
||||||
@@ -29,7 +35,7 @@ class PasswordValidator extends React.Component<PasswordValidatorProps, Password
|
|||||||
this.setState({
|
this.setState({
|
||||||
showPassword: !this.state.showPassword
|
showPassword: !this.state.showPassword
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { classes, ...rest } = this.props;
|
const { classes, ...rest } = this.props;
|
||||||
@@ -39,7 +45,7 @@ class PasswordValidator extends React.Component<PasswordValidatorProps, Password
|
|||||||
type={this.state.showPassword ? 'text' : 'password'}
|
type={this.state.showPassword ? 'text' : 'password'}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
classes,
|
classes,
|
||||||
endAdornment:
|
endAdornment: (
|
||||||
<InputAdornment position="end">
|
<InputAdornment position="end">
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label="Toggle password visibility"
|
aria-label="Toggle password visibility"
|
||||||
@@ -48,11 +54,11 @@ class PasswordValidator extends React.Component<PasswordValidatorProps, Password
|
|||||||
{this.state.showPassword ? <Visibility /> : <VisibilityOff />}
|
{this.state.showPassword ? <Visibility /> : <VisibilityOff />}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles)(PasswordValidator);
|
export default withStyles(styles)(PasswordValidator);
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import { withSnackbar, WithSnackbarProps } from 'notistack';
|
|||||||
import { redirectingAuthorizedFetch } from '../authentication';
|
import { redirectingAuthorizedFetch } from '../authentication';
|
||||||
|
|
||||||
export interface RestControllerProps<D> extends WithSnackbarProps {
|
export interface RestControllerProps<D> extends WithSnackbarProps {
|
||||||
handleValueChange: (name: keyof D) => (event: React.ChangeEvent<HTMLInputElement>) => void;
|
handleValueChange: (
|
||||||
|
name: keyof D
|
||||||
|
) => (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
|
||||||
setData: (data: D, callback?: () => void) => void;
|
setData: (data: D, callback?: () => void) => void;
|
||||||
saveData: () => void;
|
saveData: () => void;
|
||||||
@@ -15,16 +17,18 @@ export interface RestControllerProps<D> extends WithSnackbarProps {
|
|||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const extractEventValue = (event: React.ChangeEvent<HTMLInputElement>) => {
|
export const extractEventValue = (
|
||||||
|
event: React.ChangeEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
switch (event.target.type) {
|
switch (event.target.type) {
|
||||||
case "number":
|
case 'number':
|
||||||
return event.target.valueAsNumber;
|
return event.target.valueAsNumber;
|
||||||
case "checkbox":
|
case 'checkbox':
|
||||||
return event.target.checked;
|
return event.target.checked;
|
||||||
default:
|
default:
|
||||||
return event.target.value
|
return event.target.value;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
interface RestControllerState<D> {
|
interface RestControllerState<D> {
|
||||||
data?: D;
|
data?: D;
|
||||||
@@ -32,10 +36,15 @@ interface RestControllerState<D> {
|
|||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function restController<D, P extends RestControllerProps<D>>(endpointUrl: string, RestController: React.ComponentType<P & RestControllerProps<D>>) {
|
export function restController<D, P extends RestControllerProps<D>>(
|
||||||
|
endpointUrl: string,
|
||||||
|
RestController: React.ComponentType<P & RestControllerProps<D>>
|
||||||
|
) {
|
||||||
return withSnackbar(
|
return withSnackbar(
|
||||||
class extends React.Component<Omit<P, keyof RestControllerProps<D>> & WithSnackbarProps, RestControllerState<D>> {
|
class extends React.Component<
|
||||||
|
Omit<P, keyof RestControllerProps<D>> & WithSnackbarProps,
|
||||||
|
RestControllerState<D>
|
||||||
|
> {
|
||||||
state: RestControllerState<D> = {
|
state: RestControllerState<D> = {
|
||||||
data: undefined,
|
data: undefined,
|
||||||
loading: false,
|
loading: false,
|
||||||
@@ -43,12 +52,15 @@ export function restController<D, P extends RestControllerProps<D>>(endpointUrl:
|
|||||||
};
|
};
|
||||||
|
|
||||||
setData = (data: D, callback?: () => void) => {
|
setData = (data: D, callback?: () => void) => {
|
||||||
this.setState({
|
this.setState(
|
||||||
data,
|
{
|
||||||
loading: false,
|
data,
|
||||||
errorMessage: undefined
|
loading: false,
|
||||||
}, callback);
|
errorMessage: undefined
|
||||||
}
|
},
|
||||||
|
callback
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
loadData = () => {
|
loadData = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -56,19 +68,24 @@ export function restController<D, P extends RestControllerProps<D>>(endpointUrl:
|
|||||||
loading: true,
|
loading: true,
|
||||||
errorMessage: undefined
|
errorMessage: undefined
|
||||||
});
|
});
|
||||||
redirectingAuthorizedFetch(endpointUrl).then(response => {
|
redirectingAuthorizedFetch(endpointUrl)
|
||||||
if (response.status === 200) {
|
.then((response) => {
|
||||||
return response.json();
|
if (response.status === 200) {
|
||||||
}
|
return response.json();
|
||||||
throw Error("Invalid status code: " + response.status);
|
}
|
||||||
}).then(json => {
|
throw Error('Invalid status code: ' + response.status);
|
||||||
this.setState({ data: json, loading: false })
|
})
|
||||||
}).catch(error => {
|
.then((json) => {
|
||||||
const errorMessage = error.message || "Unknown error";
|
this.setState({ data: json, loading: false });
|
||||||
this.props.enqueueSnackbar("Problem fetching: " + errorMessage, { variant: 'error' });
|
})
|
||||||
this.setState({ data: undefined, loading: false, errorMessage });
|
.catch((error) => {
|
||||||
});
|
const errorMessage = error.message || 'Unknown error';
|
||||||
}
|
this.props.enqueueSnackbar('Problem fetching: ' + errorMessage, {
|
||||||
|
variant: 'error'
|
||||||
|
});
|
||||||
|
this.setState({ data: undefined, loading: false, errorMessage });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
saveData = () => {
|
saveData = () => {
|
||||||
this.setState({ loading: true });
|
this.setState({ loading: true });
|
||||||
@@ -78,36 +95,47 @@ export function restController<D, P extends RestControllerProps<D>>(endpointUrl:
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
}).then(response => {
|
})
|
||||||
if (response.status === 200) {
|
.then((response) => {
|
||||||
return response.json();
|
if (response.status === 200) {
|
||||||
}
|
return response.json();
|
||||||
throw Error("Invalid status code: " + response.status);
|
}
|
||||||
}).then(json => {
|
throw Error('Invalid status code: ' + response.status);
|
||||||
this.props.enqueueSnackbar("Update successful.", { variant: 'success' });
|
})
|
||||||
this.setState({ data: json, loading: false });
|
.then((json) => {
|
||||||
}).catch(error => {
|
this.props.enqueueSnackbar('Update successful.', {
|
||||||
const errorMessage = error.message || "Unknown error";
|
variant: 'success'
|
||||||
this.props.enqueueSnackbar("Problem updating: " + errorMessage, { variant: 'error' });
|
});
|
||||||
this.setState({ data: undefined, loading: false, errorMessage });
|
this.setState({ data: json, loading: false });
|
||||||
});
|
})
|
||||||
}
|
.catch((error) => {
|
||||||
|
const errorMessage = error.message || 'Unknown error';
|
||||||
|
this.props.enqueueSnackbar('Problem updating: ' + errorMessage, {
|
||||||
|
variant: 'error'
|
||||||
|
});
|
||||||
|
this.setState({ data: undefined, loading: false, errorMessage });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
handleValueChange = (name: keyof D) => (event: React.ChangeEvent<HTMLInputElement>) => {
|
handleValueChange = (name: keyof D) => (
|
||||||
|
event: React.ChangeEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
const data = { ...this.state.data!, [name]: extractEventValue(event) };
|
const data = { ...this.state.data!, [name]: extractEventValue(event) };
|
||||||
this.setState({ data });
|
this.setState({ data });
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <RestController
|
return (
|
||||||
{...this.state}
|
<RestController
|
||||||
{...this.props as P}
|
{...this.state}
|
||||||
handleValueChange={this.handleValueChange}
|
{...(this.props as P)}
|
||||||
setData={this.setData}
|
handleValueChange={this.handleValueChange}
|
||||||
saveData={this.saveData}
|
setData={this.setData}
|
||||||
loadData={this.loadData}
|
saveData={this.saveData}
|
||||||
/>;
|
loadData={this.loadData}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,20 +8,23 @@ import { RestControllerProps } from '.';
|
|||||||
const useStyles = makeStyles((theme: Theme) =>
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
createStyles({
|
createStyles({
|
||||||
loadingSettings: {
|
loadingSettings: {
|
||||||
margin: theme.spacing(0.5),
|
margin: theme.spacing(0.5)
|
||||||
},
|
},
|
||||||
loadingSettingsDetails: {
|
loadingSettingsDetails: {
|
||||||
margin: theme.spacing(4),
|
margin: theme.spacing(4),
|
||||||
textAlign: "center"
|
textAlign: 'center'
|
||||||
},
|
},
|
||||||
button: {
|
button: {
|
||||||
marginRight: theme.spacing(2),
|
marginRight: theme.spacing(2),
|
||||||
marginTop: theme.spacing(2),
|
marginTop: theme.spacing(2)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
export type RestFormProps<D> = Omit<RestControllerProps<D>, "loading" | "errorMessage"> & { data: D };
|
export type RestFormProps<D> = Omit<
|
||||||
|
RestControllerProps<D>,
|
||||||
|
'loading' | 'errorMessage'
|
||||||
|
> & { data: D };
|
||||||
|
|
||||||
interface RestFormLoaderProps<D> extends RestControllerProps<D> {
|
interface RestFormLoaderProps<D> extends RestControllerProps<D> {
|
||||||
render: (props: RestFormProps<D>) => JSX.Element;
|
render: (props: RestFormProps<D>) => JSX.Element;
|
||||||
@@ -46,7 +49,12 @@ export default function RestFormLoader<D>(props: RestFormLoaderProps<D>) {
|
|||||||
<Typography variant="h6" className={classes.loadingSettingsDetails}>
|
<Typography variant="h6" className={classes.loadingSettingsDetails}>
|
||||||
{errorMessage}
|
{errorMessage}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Button variant="contained" color="secondary" className={classes.button} onClick={loadData}>
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
className={classes.button}
|
||||||
|
onClick={loadData}
|
||||||
|
>
|
||||||
Retry
|
Retry
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const useStyles = makeStyles((theme: Theme) =>
|
|||||||
createStyles({
|
createStyles({
|
||||||
content: {
|
content: {
|
||||||
padding: theme.spacing(2),
|
padding: theme.spacing(2),
|
||||||
margin: theme.spacing(3),
|
margin: theme.spacing(3)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -15,13 +15,14 @@ const useStyles = makeStyles((theme: Theme) =>
|
|||||||
interface SectionContentProps {
|
interface SectionContentProps {
|
||||||
title: string;
|
title: string;
|
||||||
titleGutter?: boolean;
|
titleGutter?: boolean;
|
||||||
|
id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SectionContent: React.FC<SectionContentProps> = (props) => {
|
const SectionContent: React.FC<SectionContentProps> = (props) => {
|
||||||
const { children, title, titleGutter } = props;
|
const { children, title, titleGutter, id } = props;
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
return (
|
return (
|
||||||
<Paper className={classes.content}>
|
<Paper id={id} className={classes.content}>
|
||||||
<Typography variant="h6" gutterBottom={titleGutter}>
|
<Typography variant="h6" gutterBottom={titleGutter}>
|
||||||
{title}
|
{title}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|||||||
@@ -4,13 +4,20 @@ import { useDropzone, DropzoneState } from 'react-dropzone';
|
|||||||
import { makeStyles, createStyles } from '@material-ui/styles';
|
import { makeStyles, createStyles } from '@material-ui/styles';
|
||||||
import CloudUploadIcon from '@material-ui/icons/CloudUpload';
|
import CloudUploadIcon from '@material-ui/icons/CloudUpload';
|
||||||
import CancelIcon from '@material-ui/icons/Cancel';
|
import CancelIcon from '@material-ui/icons/Cancel';
|
||||||
import { Theme, Box, Typography, LinearProgress, Button } from '@material-ui/core';
|
import {
|
||||||
|
Theme,
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
LinearProgress,
|
||||||
|
Button
|
||||||
|
} from '@material-ui/core';
|
||||||
|
|
||||||
interface SingleUploadStyleProps extends DropzoneState {
|
interface SingleUploadStyleProps extends DropzoneState {
|
||||||
uploading: boolean;
|
uploading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const progressPercentage = (progress: ProgressEvent) => Math.round((progress.loaded * 100) / progress.total);
|
const progressPercentage = (progress: ProgressEvent) =>
|
||||||
|
Math.round((progress.loaded * 100) / progress.total);
|
||||||
|
|
||||||
const getBorderColor = (theme: Theme, props: SingleUploadStyleProps) => {
|
const getBorderColor = (theme: Theme, props: SingleUploadStyleProps) => {
|
||||||
if (props.isDragAccept) {
|
if (props.isDragAccept) {
|
||||||
@@ -23,21 +30,25 @@ const getBorderColor = (theme: Theme, props: SingleUploadStyleProps) => {
|
|||||||
return theme.palette.info.main;
|
return theme.palette.info.main;
|
||||||
}
|
}
|
||||||
return theme.palette.grey[700];
|
return theme.palette.grey[700];
|
||||||
}
|
};
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) => createStyles({
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
dropzone: {
|
createStyles({
|
||||||
padding: theme.spacing(8, 2),
|
dropzone: {
|
||||||
borderWidth: 2,
|
padding: theme.spacing(8, 2),
|
||||||
borderRadius: 2,
|
borderWidth: 2,
|
||||||
borderStyle: 'dashed',
|
borderRadius: 2,
|
||||||
color: theme.palette.grey[700],
|
borderStyle: 'dashed',
|
||||||
transition: 'border .24s ease-in-out',
|
color: theme.palette.grey[700],
|
||||||
cursor: (props: SingleUploadStyleProps) => props.uploading ? 'default' : 'pointer',
|
transition: 'border .24s ease-in-out',
|
||||||
width: '100%',
|
cursor: (props: SingleUploadStyleProps) =>
|
||||||
borderColor: (props: SingleUploadStyleProps) => getBorderColor(theme, props)
|
props.uploading ? 'default' : 'pointer',
|
||||||
}
|
width: '100%',
|
||||||
}));
|
borderColor: (props: SingleUploadStyleProps) =>
|
||||||
|
getBorderColor(theme, props)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
export interface SingleUploadProps {
|
export interface SingleUploadProps {
|
||||||
onDrop: (acceptedFiles: File[]) => void;
|
onDrop: (acceptedFiles: File[]) => void;
|
||||||
@@ -47,26 +58,44 @@ export interface SingleUploadProps {
|
|||||||
progress?: ProgressEvent;
|
progress?: ProgressEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, accept, uploading, progress }) => {
|
const SingleUpload: FC<SingleUploadProps> = ({
|
||||||
const dropzoneState = useDropzone({ onDrop, accept, disabled: uploading, multiple: false });
|
onDrop,
|
||||||
|
onCancel,
|
||||||
|
accept,
|
||||||
|
uploading,
|
||||||
|
progress
|
||||||
|
}) => {
|
||||||
|
const dropzoneState = useDropzone({
|
||||||
|
onDrop,
|
||||||
|
accept,
|
||||||
|
disabled: uploading,
|
||||||
|
multiple: false
|
||||||
|
});
|
||||||
const { getRootProps, getInputProps } = dropzoneState;
|
const { getRootProps, getInputProps } = dropzoneState;
|
||||||
const classes = useStyles({ ...dropzoneState, uploading });
|
const classes = useStyles({ ...dropzoneState, uploading });
|
||||||
|
|
||||||
|
|
||||||
const renderProgressText = () => {
|
const renderProgressText = () => {
|
||||||
if (uploading) {
|
if (uploading) {
|
||||||
if (progress?.lengthComputable) {
|
if (progress?.lengthComputable) {
|
||||||
return `Uploading: ${progressPercentage(progress)}%`;
|
return `Uploading: ${progressPercentage(progress)}%`;
|
||||||
}
|
}
|
||||||
return "Uploading\u2026";
|
return 'Uploading\u2026';
|
||||||
}
|
}
|
||||||
return "Drop file or click here";
|
return 'Drop file or click here';
|
||||||
}
|
};
|
||||||
|
|
||||||
const renderProgress = (progress?: ProgressEvent) => (
|
const renderProgress = (progress?: ProgressEvent) => (
|
||||||
<LinearProgress
|
<LinearProgress
|
||||||
variant={!progress || progress.lengthComputable ? "determinate" : "indeterminate"}
|
variant={
|
||||||
value={!progress ? 0 : progress.lengthComputable ? progressPercentage(progress) : 0}
|
!progress || progress.lengthComputable ? 'determinate' : 'indeterminate'
|
||||||
|
}
|
||||||
|
value={
|
||||||
|
!progress
|
||||||
|
? 0
|
||||||
|
: progress.lengthComputable
|
||||||
|
? progressPercentage(progress)
|
||||||
|
: 0
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -74,16 +103,19 @@ const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, accept, uploadi
|
|||||||
<div {...getRootProps({ className: classes.dropzone })}>
|
<div {...getRootProps({ className: classes.dropzone })}>
|
||||||
<input {...getInputProps()} />
|
<input {...getInputProps()} />
|
||||||
<Box flexDirection="column" display="flex" alignItems="center">
|
<Box flexDirection="column" display="flex" alignItems="center">
|
||||||
<CloudUploadIcon fontSize='large' />
|
<CloudUploadIcon fontSize="large" />
|
||||||
<Typography variant="h6">
|
<Typography variant="h6">{renderProgressText()}</Typography>
|
||||||
{renderProgressText()}
|
|
||||||
</Typography>
|
|
||||||
{uploading && (
|
{uploading && (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Box width="100%" p={2}>
|
<Box width="100%" p={2}>
|
||||||
{renderProgress(progress)}
|
{renderProgress(progress)}
|
||||||
</Box>
|
</Box>
|
||||||
<Button startIcon={<CancelIcon />} variant="contained" color="secondary" onClick={onCancel}>
|
<Button
|
||||||
|
startIcon={<CancelIcon />}
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
onClick={onCancel}
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
@@ -91,6 +123,6 @@ const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, accept, uploadi
|
|||||||
</Box>
|
</Box>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default SingleUpload;
|
export default SingleUpload;
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import { addAccessTokenParameter } from '../authentication';
|
|||||||
import { extractEventValue } from '.';
|
import { extractEventValue } from '.';
|
||||||
|
|
||||||
export interface WebSocketControllerProps<D> extends WithSnackbarProps {
|
export interface WebSocketControllerProps<D> extends WithSnackbarProps {
|
||||||
handleValueChange: (name: keyof D) => (event: React.ChangeEvent<HTMLInputElement>) => void;
|
handleValueChange: (
|
||||||
|
name: keyof D
|
||||||
|
) => (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
|
||||||
setData: (data: D, callback?: () => void) => void;
|
setData: (data: D, callback?: () => void) => void;
|
||||||
saveData: () => void;
|
saveData: () => void;
|
||||||
@@ -25,8 +27,8 @@ interface WebSocketControllerState<D> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum WebSocketMessageType {
|
enum WebSocketMessageType {
|
||||||
ID = "id",
|
ID = 'id',
|
||||||
PAYLOAD = "payload"
|
PAYLOAD = 'payload'
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WebSocketIdMessage {
|
interface WebSocketIdMessage {
|
||||||
@@ -40,21 +42,32 @@ interface WebSocketPayloadMessage<D> {
|
|||||||
payload: D;
|
payload: D;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WebSocketMessage<D> = WebSocketIdMessage | WebSocketPayloadMessage<D>;
|
export type WebSocketMessage<D> =
|
||||||
|
| WebSocketIdMessage
|
||||||
|
| WebSocketPayloadMessage<D>;
|
||||||
|
|
||||||
export function webSocketController<D, P extends WebSocketControllerProps<D>>(wsUrl: string, wsThrottle: number, WebSocketController: React.ComponentType<P & WebSocketControllerProps<D>>) {
|
export function webSocketController<D, P extends WebSocketControllerProps<D>>(
|
||||||
|
wsUrl: string,
|
||||||
|
wsThrottle: number,
|
||||||
|
WebSocketController: React.ComponentType<P & WebSocketControllerProps<D>>
|
||||||
|
) {
|
||||||
return withSnackbar(
|
return withSnackbar(
|
||||||
class extends React.Component<Omit<P, keyof WebSocketControllerProps<D>> & WithSnackbarProps, WebSocketControllerState<D>> {
|
class extends React.Component<
|
||||||
constructor(props: Omit<P, keyof WebSocketControllerProps<D>> & WithSnackbarProps) {
|
Omit<P, keyof WebSocketControllerProps<D>> & WithSnackbarProps,
|
||||||
|
WebSocketControllerState<D>
|
||||||
|
> {
|
||||||
|
constructor(
|
||||||
|
props: Omit<P, keyof WebSocketControllerProps<D>> & WithSnackbarProps
|
||||||
|
) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
ws: new Sockette(addAccessTokenParameter(wsUrl), {
|
ws: new Sockette(addAccessTokenParameter(wsUrl), {
|
||||||
onmessage: this.onMessage,
|
onmessage: this.onMessage,
|
||||||
onopen: this.onOpen,
|
onopen: this.onOpen,
|
||||||
onclose: this.onClose,
|
onclose: this.onClose
|
||||||
}),
|
}),
|
||||||
connected: false
|
connected: false
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
@@ -64,37 +77,42 @@ export function webSocketController<D, P extends WebSocketControllerProps<D>>(ws
|
|||||||
onMessage = (event: MessageEvent) => {
|
onMessage = (event: MessageEvent) => {
|
||||||
const rawData = event.data;
|
const rawData = event.data;
|
||||||
if (typeof rawData === 'string' || rawData instanceof String) {
|
if (typeof rawData === 'string' || rawData instanceof String) {
|
||||||
this.handleMessage(JSON.parse(rawData as string) as WebSocketMessage<D>);
|
this.handleMessage(
|
||||||
|
JSON.parse(rawData as string) as WebSocketMessage<D>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
handleMessage = (message: WebSocketMessage<D>) => {
|
handleMessage = (message: WebSocketMessage<D>) => {
|
||||||
|
const { clientId, data } = this.state;
|
||||||
|
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case WebSocketMessageType.ID:
|
case WebSocketMessageType.ID:
|
||||||
this.setState({ clientId: message.id });
|
this.setState({ clientId: message.id });
|
||||||
break;
|
break;
|
||||||
case WebSocketMessageType.PAYLOAD:
|
case WebSocketMessageType.PAYLOAD:
|
||||||
const { clientId, data } = this.state;
|
|
||||||
if (clientId && (!data || clientId !== message.origin_id)) {
|
if (clientId && (!data || clientId !== message.origin_id)) {
|
||||||
this.setState(
|
this.setState({ data: message.payload });
|
||||||
{ data: message.payload }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
onOpen = () => {
|
onOpen = () => {
|
||||||
this.setState({ connected: true });
|
this.setState({ connected: true });
|
||||||
}
|
};
|
||||||
|
|
||||||
onClose = () => {
|
onClose = () => {
|
||||||
this.setState({ connected: false, clientId: undefined, data: undefined });
|
this.setState({
|
||||||
}
|
connected: false,
|
||||||
|
clientId: undefined,
|
||||||
|
data: undefined
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
setData = (data: D, callback?: () => void) => {
|
setData = (data: D, callback?: () => void) => {
|
||||||
this.setState({ data }, callback);
|
this.setState({ data }, callback);
|
||||||
}
|
};
|
||||||
|
|
||||||
saveData = throttle(() => {
|
saveData = throttle(() => {
|
||||||
const { ws, connected, data } = this.state;
|
const { ws, connected, data } = this.state;
|
||||||
@@ -106,28 +124,35 @@ export function webSocketController<D, P extends WebSocketControllerProps<D>>(ws
|
|||||||
saveDataAndClear = throttle(() => {
|
saveDataAndClear = throttle(() => {
|
||||||
const { ws, connected, data } = this.state;
|
const { ws, connected, data } = this.state;
|
||||||
if (connected) {
|
if (connected) {
|
||||||
this.setState({
|
this.setState(
|
||||||
data: undefined
|
{
|
||||||
}, () => ws.json(data));
|
data: undefined
|
||||||
|
},
|
||||||
|
() => ws.json(data)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, wsThrottle);
|
}, wsThrottle);
|
||||||
|
|
||||||
handleValueChange = (name: keyof D) => (event: React.ChangeEvent<HTMLInputElement>) => {
|
handleValueChange = (name: keyof D) => (
|
||||||
|
event: React.ChangeEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
const data = { ...this.state.data!, [name]: extractEventValue(event) };
|
const data = { ...this.state.data!, [name]: extractEventValue(event) };
|
||||||
this.setState({ data });
|
this.setState({ data });
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <WebSocketController
|
return (
|
||||||
{...this.props as P}
|
<WebSocketController
|
||||||
handleValueChange={this.handleValueChange}
|
{...(this.props as P)}
|
||||||
setData={this.setData}
|
handleValueChange={this.handleValueChange}
|
||||||
saveData={this.saveData}
|
setData={this.setData}
|
||||||
saveDataAndClear={this.saveDataAndClear}
|
saveData={this.saveData}
|
||||||
connected={this.state.connected}
|
saveDataAndClear={this.saveDataAndClear}
|
||||||
data={this.state.data}
|
connected={this.state.connected}
|
||||||
/>;
|
data={this.state.data}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles';
|
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles';
|
||||||
import { LinearProgress, Typography } from '@material-ui/core';
|
import { LinearProgress, Typography } from '@material-ui/core';
|
||||||
|
|
||||||
@@ -8,22 +6,27 @@ import { WebSocketControllerProps } from '.';
|
|||||||
const useStyles = makeStyles((theme: Theme) =>
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
createStyles({
|
createStyles({
|
||||||
loadingSettings: {
|
loadingSettings: {
|
||||||
margin: theme.spacing(0.5),
|
margin: theme.spacing(0.5)
|
||||||
},
|
},
|
||||||
loadingSettingsDetails: {
|
loadingSettingsDetails: {
|
||||||
margin: theme.spacing(4),
|
margin: theme.spacing(4),
|
||||||
textAlign: "center"
|
textAlign: 'center'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
export type WebSocketFormProps<D> = Omit<WebSocketControllerProps<D>, "connected"> & { data: D };
|
export type WebSocketFormProps<D> = Omit<
|
||||||
|
WebSocketControllerProps<D>,
|
||||||
|
'connected'
|
||||||
|
> & { data: D };
|
||||||
|
|
||||||
interface WebSocketFormLoaderProps<D> extends WebSocketControllerProps<D> {
|
interface WebSocketFormLoaderProps<D> extends WebSocketControllerProps<D> {
|
||||||
render: (props: WebSocketFormProps<D>) => JSX.Element;
|
render: (props: WebSocketFormProps<D>) => JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function WebSocketFormLoader<D>(props: WebSocketFormLoaderProps<D>) {
|
export default function WebSocketFormLoader<D>(
|
||||||
|
props: WebSocketFormLoaderProps<D>
|
||||||
|
) {
|
||||||
const { connected, render, data, ...rest } = props;
|
const { connected, render, data, ...rest } = props;
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
if (!connected || !data) {
|
if (!connected || !data) {
|
||||||
|
|||||||
14
interface/src/components/WindowSize.tsx
Normal file
14
interface/src/components/WindowSize.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { useLayoutEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export function useWindowSize() {
|
||||||
|
const [size, setSize] = useState([0, 0]);
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
function updateSize() {
|
||||||
|
setSize([window.innerWidth, window.innerHeight]);
|
||||||
|
}
|
||||||
|
window.addEventListener('resize', updateSize);
|
||||||
|
updateSize();
|
||||||
|
return () => window.removeEventListener('resize', updateSize);
|
||||||
|
}, []);
|
||||||
|
return size;
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ export { default as HighlightAvatar } from './HighlightAvatar';
|
|||||||
export { default as MenuAppBar } from './MenuAppBar';
|
export { default as MenuAppBar } from './MenuAppBar';
|
||||||
export { default as PasswordValidator } from './PasswordValidator';
|
export { default as PasswordValidator } from './PasswordValidator';
|
||||||
export { default as RestFormLoader } from './RestFormLoader';
|
export { default as RestFormLoader } from './RestFormLoader';
|
||||||
|
export { default as FormLoader } from './FormLoader';
|
||||||
export { default as SectionContent } from './SectionContent';
|
export { default as SectionContent } from './SectionContent';
|
||||||
export { default as WebSocketFormLoader } from './WebSocketFormLoader';
|
export { default as WebSocketFormLoader } from './WebSocketFormLoader';
|
||||||
export { default as ErrorButton } from './ErrorButton';
|
export { default as ErrorButton } from './ErrorButton';
|
||||||
@@ -15,3 +16,5 @@ export * from './RestController';
|
|||||||
|
|
||||||
export * from './WebSocketFormLoader';
|
export * from './WebSocketFormLoader';
|
||||||
export * from './WebSocketController';
|
export * from './WebSocketController';
|
||||||
|
|
||||||
|
export * from './WindowSize';
|
||||||
|
|||||||
@@ -5,21 +5,26 @@ export interface FeaturesContextValue {
|
|||||||
features: Features;
|
features: Features;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FeaturesContextDefaultValue = {} as FeaturesContextValue
|
const FeaturesContextDefaultValue = {} as FeaturesContextValue;
|
||||||
export const FeaturesContext = React.createContext(
|
export const FeaturesContext = React.createContext(FeaturesContextDefaultValue);
|
||||||
FeaturesContextDefaultValue
|
|
||||||
);
|
|
||||||
|
|
||||||
export interface WithFeaturesProps {
|
export interface WithFeaturesProps {
|
||||||
features: Features;
|
features: Features;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function withFeatures<T extends WithFeaturesProps>(Component: React.ComponentType<T>) {
|
export function withFeatures<T extends WithFeaturesProps>(
|
||||||
|
Component: React.ComponentType<T>
|
||||||
|
) {
|
||||||
return class extends React.Component<Omit<T, keyof WithFeaturesProps>> {
|
return class extends React.Component<Omit<T, keyof WithFeaturesProps>> {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<FeaturesContext.Consumer>
|
<FeaturesContext.Consumer>
|
||||||
{featuresContext => <Component {...this.props as T} features={featuresContext.features} />}
|
{(featuresContext) => (
|
||||||
|
<Component
|
||||||
|
{...(this.props as T)}
|
||||||
|
features={featuresContext.features}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</FeaturesContext.Consumer>
|
</FeaturesContext.Consumer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,61 +1,31 @@
|
|||||||
import React, { Component } from 'react';
|
import { FC } from 'react';
|
||||||
|
|
||||||
import { Features } from './types';
|
|
||||||
import { FeaturesContext } from './FeaturesContext';
|
|
||||||
import FullScreenLoading from '../components/FullScreenLoading';
|
import FullScreenLoading from '../components/FullScreenLoading';
|
||||||
import ApplicationError from '../components/ApplicationError';
|
import ApplicationError from '../components/ApplicationError';
|
||||||
import { FEATURES_ENDPOINT } from '../api';
|
import { FEATURES_ENDPOINT } from '../api';
|
||||||
|
import { useRest } from '../hooks';
|
||||||
|
|
||||||
interface FeaturesWrapperState {
|
import { Features } from './types';
|
||||||
features?: Features;
|
import { FeaturesContext } from './FeaturesContext';
|
||||||
error?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
class FeaturesWrapper extends Component<{}, FeaturesWrapperState> {
|
const FeaturesWrapper: FC = ({ children }) => {
|
||||||
|
const { data: features, errorMessage: error } = useRest<Features>({
|
||||||
|
endpoint: FEATURES_ENDPOINT
|
||||||
|
});
|
||||||
|
|
||||||
state: FeaturesWrapperState = {};
|
if (features) {
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.fetchFeaturesDetails();
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchFeaturesDetails = () => {
|
|
||||||
fetch(FEATURES_ENDPOINT)
|
|
||||||
.then(response => {
|
|
||||||
if (response.status === 200) {
|
|
||||||
return response.json();
|
|
||||||
} else {
|
|
||||||
throw Error("Unexpected status code: " + response.status);
|
|
||||||
}
|
|
||||||
}).then(features => {
|
|
||||||
this.setState({ features });
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
this.setState({ error: error.message });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { features, error } = this.state;
|
|
||||||
if (features) {
|
|
||||||
return (
|
|
||||||
<FeaturesContext.Provider value={{
|
|
||||||
features
|
|
||||||
}}>
|
|
||||||
{this.props.children}
|
|
||||||
</FeaturesContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<ApplicationError error={error} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<FullScreenLoading />
|
<FeaturesContext.Provider value={{ features }}>
|
||||||
|
{children}
|
||||||
|
</FeaturesContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
if (error) {
|
||||||
|
return <ApplicationError error={error} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <FullScreenLoading />;
|
||||||
|
};
|
||||||
|
|
||||||
export default FeaturesWrapper;
|
export default FeaturesWrapper;
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ import { createBrowserHistory } from 'history';
|
|||||||
|
|
||||||
export default createBrowserHistory({
|
export default createBrowserHistory({
|
||||||
/* pass a configuration object here if needed */
|
/* pass a configuration object here if needed */
|
||||||
})
|
});
|
||||||
|
|||||||
2
interface/src/hooks/index.ts
Normal file
2
interface/src/hooks/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as useRest } from './useRest';
|
||||||
|
export { default as useAuthorizedRest } from './useAuthorizedRest';
|
||||||
12
interface/src/hooks/useAuthorizedRest.ts
Normal file
12
interface/src/hooks/useAuthorizedRest.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { redirectingAuthorizedFetch } from '../authentication';
|
||||||
|
import useRest, { RestRequestOptions } from './useRest';
|
||||||
|
|
||||||
|
const useAuthorizedRest = <D>({
|
||||||
|
endpoint
|
||||||
|
}: Omit<RestRequestOptions, 'fetchFunction'>) =>
|
||||||
|
useRest<D>({
|
||||||
|
endpoint,
|
||||||
|
fetchFunction: redirectingAuthorizedFetch
|
||||||
|
});
|
||||||
|
|
||||||
|
export default useAuthorizedRest;
|
||||||
79
interface/src/hooks/useRest.ts
Normal file
79
interface/src/hooks/useRest.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useSnackbar } from 'notistack';
|
||||||
|
|
||||||
|
export interface RestRequestOptions {
|
||||||
|
endpoint: string;
|
||||||
|
fetchFunction?: typeof fetch;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useRest = <D>({
|
||||||
|
endpoint,
|
||||||
|
fetchFunction = fetch
|
||||||
|
}: RestRequestOptions) => {
|
||||||
|
const { enqueueSnackbar } = useSnackbar();
|
||||||
|
|
||||||
|
const [saving, setSaving] = useState<boolean>(false);
|
||||||
|
const [data, setData] = useState<D>();
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string>();
|
||||||
|
|
||||||
|
const handleError = useCallback(
|
||||||
|
(error: any) => {
|
||||||
|
const errorMessage = error.message || 'Unknown error';
|
||||||
|
enqueueSnackbar('Problem fetching: ' + errorMessage, {
|
||||||
|
variant: 'error'
|
||||||
|
});
|
||||||
|
setErrorMessage(errorMessage);
|
||||||
|
},
|
||||||
|
[enqueueSnackbar]
|
||||||
|
);
|
||||||
|
|
||||||
|
const loadData = useCallback(async () => {
|
||||||
|
setData(undefined);
|
||||||
|
setErrorMessage(undefined);
|
||||||
|
try {
|
||||||
|
const response = await fetchFunction(endpoint);
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw Error('Invalid status code: ' + response.status);
|
||||||
|
}
|
||||||
|
setData(await response.json());
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error);
|
||||||
|
}
|
||||||
|
}, [handleError, fetchFunction, endpoint]);
|
||||||
|
|
||||||
|
const save = useCallback(
|
||||||
|
async (data: D) => {
|
||||||
|
setSaving(true);
|
||||||
|
setErrorMessage(undefined);
|
||||||
|
try {
|
||||||
|
const response = await fetchFunction(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw Error('Invalid status code: ' + response.status);
|
||||||
|
}
|
||||||
|
enqueueSnackbar('Update successful.', { variant: 'success' });
|
||||||
|
setData(await response.json());
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error);
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[enqueueSnackbar, handleError, fetchFunction, endpoint]
|
||||||
|
);
|
||||||
|
|
||||||
|
const saveData = () => data && save(data);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadData();
|
||||||
|
}, [loadData]);
|
||||||
|
|
||||||
|
return { loadData, saveData, saving, setData, data, errorMessage } as const;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useRest;
|
||||||
@@ -6,8 +6,9 @@ import { Router } from 'react-router';
|
|||||||
|
|
||||||
import App from './App';
|
import App from './App';
|
||||||
|
|
||||||
render((
|
render(
|
||||||
<Router history={history}>
|
<Router history={history}>
|
||||||
<App/>
|
<App />
|
||||||
</Router>
|
</Router>,
|
||||||
), document.getElementById("root"))
|
document.getElementById('root')
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
|
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
|
||||||
|
|
||||||
import { Tabs, Tab } from '@material-ui/core';
|
import { Tabs, Tab } from '@material-ui/core';
|
||||||
|
|
||||||
import { AuthenticatedContextProps, withAuthenticatedContext, AuthenticatedRoute } from '../authentication';
|
import {
|
||||||
|
AuthenticatedContextProps,
|
||||||
|
withAuthenticatedContext,
|
||||||
|
AuthenticatedRoute
|
||||||
|
} from '../authentication';
|
||||||
import { MenuAppBar } from '../components';
|
import { MenuAppBar } from '../components';
|
||||||
import MqttStatusController from './MqttStatusController';
|
import MqttStatusController from './MqttStatusController';
|
||||||
import MqttSettingsController from './MqttSettingsController';
|
import MqttSettingsController from './MqttSettingsController';
|
||||||
@@ -11,8 +15,7 @@ import MqttSettingsController from './MqttSettingsController';
|
|||||||
type MqttProps = AuthenticatedContextProps & RouteComponentProps;
|
type MqttProps = AuthenticatedContextProps & RouteComponentProps;
|
||||||
|
|
||||||
class Mqtt extends Component<MqttProps> {
|
class Mqtt extends Component<MqttProps> {
|
||||||
|
handleTabChange = (path: string) => {
|
||||||
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
|
|
||||||
this.props.history.push(path);
|
this.props.history.push(path);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -20,17 +23,33 @@ class Mqtt extends Component<MqttProps> {
|
|||||||
const { authenticatedContext } = this.props;
|
const { authenticatedContext } = this.props;
|
||||||
return (
|
return (
|
||||||
<MenuAppBar sectionTitle="MQTT">
|
<MenuAppBar sectionTitle="MQTT">
|
||||||
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
|
<Tabs
|
||||||
|
value={this.props.match.url}
|
||||||
|
onChange={(e, path) => this.handleTabChange(path)}
|
||||||
|
variant="fullWidth"
|
||||||
|
>
|
||||||
<Tab value="/mqtt/status" label="MQTT Status" />
|
<Tab value="/mqtt/status" label="MQTT Status" />
|
||||||
<Tab value="/mqtt/settings" label="MQTT Settings" disabled={!authenticatedContext.me.admin} />
|
<Tab
|
||||||
|
value="/mqtt/settings"
|
||||||
|
label="MQTT Settings"
|
||||||
|
disabled={!authenticatedContext.me.admin}
|
||||||
|
/>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<Switch>
|
<Switch>
|
||||||
<AuthenticatedRoute exact path="/mqtt/status" component={MqttStatusController} />
|
<AuthenticatedRoute
|
||||||
<AuthenticatedRoute exact path="/mqtt/settings" component={MqttSettingsController} />
|
exact
|
||||||
|
path="/mqtt/status"
|
||||||
|
component={MqttStatusController}
|
||||||
|
/>
|
||||||
|
<AuthenticatedRoute
|
||||||
|
exact
|
||||||
|
path="/mqtt/settings"
|
||||||
|
component={MqttSettingsController}
|
||||||
|
/>
|
||||||
<Redirect to="/mqtt/status" />
|
<Redirect to="/mqtt/status" />
|
||||||
</Switch>
|
</Switch>
|
||||||
</MenuAppBar>
|
</MenuAppBar>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
import {
|
||||||
|
restController,
|
||||||
|
RestControllerProps,
|
||||||
|
RestFormLoader,
|
||||||
|
SectionContent
|
||||||
|
} from '../components';
|
||||||
import { MQTT_SETTINGS_ENDPOINT } from '../api';
|
import { MQTT_SETTINGS_ENDPOINT } from '../api';
|
||||||
|
|
||||||
import MqttSettingsForm from './MqttSettingsForm';
|
import MqttSettingsForm from './MqttSettingsForm';
|
||||||
@@ -9,7 +14,6 @@ import { MqttSettings } from './types';
|
|||||||
type MqttSettingsControllerProps = RestControllerProps<MqttSettings>;
|
type MqttSettingsControllerProps = RestControllerProps<MqttSettings>;
|
||||||
|
|
||||||
class MqttSettingsController extends Component<MqttSettingsControllerProps> {
|
class MqttSettingsController extends Component<MqttSettingsControllerProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.loadData();
|
this.props.loadData();
|
||||||
}
|
}
|
||||||
@@ -19,12 +23,11 @@ class MqttSettingsController extends Component<MqttSettingsControllerProps> {
|
|||||||
<SectionContent title="MQTT Settings" titleGutter>
|
<SectionContent title="MQTT Settings" titleGutter>
|
||||||
<RestFormLoader
|
<RestFormLoader
|
||||||
{...this.props}
|
{...this.props}
|
||||||
render={formProps => <MqttSettingsForm {...formProps} />}
|
render={(formProps) => <MqttSettingsForm {...formProps} />}
|
||||||
/>
|
/>
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default restController(MQTT_SETTINGS_ENDPOINT, MqttSettingsController);
|
export default restController(MQTT_SETTINGS_ENDPOINT, MqttSettingsController);
|
||||||
|
|||||||
@@ -1,11 +1,21 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { TextValidator, ValidatorForm, SelectValidator } from 'react-material-ui-form-validator';
|
import {
|
||||||
|
TextValidator,
|
||||||
|
ValidatorForm,
|
||||||
|
SelectValidator
|
||||||
|
} from 'react-material-ui-form-validator';
|
||||||
|
|
||||||
import { Checkbox, TextField, Typography } from '@material-ui/core';
|
import { Checkbox, TextField, Typography } from '@material-ui/core';
|
||||||
import SaveIcon from '@material-ui/icons/Save';
|
import SaveIcon from '@material-ui/icons/Save';
|
||||||
import MenuItem from '@material-ui/core/MenuItem';
|
import MenuItem from '@material-ui/core/MenuItem';
|
||||||
|
|
||||||
import { RestFormProps, FormActions, FormButton, BlockFormControlLabel, PasswordValidator } from '../components';
|
import {
|
||||||
|
RestFormProps,
|
||||||
|
FormActions,
|
||||||
|
FormButton,
|
||||||
|
BlockFormControlLabel,
|
||||||
|
PasswordValidator
|
||||||
|
} from '../components';
|
||||||
import { isIP, isHostname, or, isPath } from '../validators';
|
import { isIP, isHostname, or, isPath } from '../validators';
|
||||||
|
|
||||||
import { MqttSettings } from './types';
|
import { MqttSettings } from './types';
|
||||||
@@ -13,7 +23,6 @@ import { MqttSettings } from './types';
|
|||||||
type MqttSettingsFormProps = RestFormProps<MqttSettings>;
|
type MqttSettingsFormProps = RestFormProps<MqttSettings>;
|
||||||
|
|
||||||
class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
ValidatorForm.addValidationRule('isIPOrHostname', or(isIP, isHostname));
|
ValidatorForm.addValidationRule('isIPOrHostname', or(isIP, isHostname));
|
||||||
ValidatorForm.addValidationRule('isPath', isPath);
|
ValidatorForm.addValidationRule('isPath', isPath);
|
||||||
@@ -35,7 +44,10 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
/>
|
/>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['required', 'isIPOrHostname']}
|
validators={['required', 'isIPOrHostname']}
|
||||||
errorMessages={['Host is required', "Not a valid IP address or hostname"]}
|
errorMessages={[
|
||||||
|
'Host is required',
|
||||||
|
'Not a valid IP address or hostname'
|
||||||
|
]}
|
||||||
name="host"
|
name="host"
|
||||||
label="Host"
|
label="Host"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -45,8 +57,18 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:65535']}
|
validators={[
|
||||||
errorMessages={['Port is required', "Must be a number", "Must be greater than 0 ", "Max value is 65535"]}
|
'required',
|
||||||
|
'isNumber',
|
||||||
|
'minNumber:0',
|
||||||
|
'maxNumber:65535'
|
||||||
|
]}
|
||||||
|
errorMessages={[
|
||||||
|
'Port is required',
|
||||||
|
'Must be a number',
|
||||||
|
'Must be greater than 0 ',
|
||||||
|
'Max value is 65535'
|
||||||
|
]}
|
||||||
name="port"
|
name="port"
|
||||||
label="Port"
|
label="Port"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -58,7 +80,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
/>
|
/>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['required', 'isPath']}
|
validators={['required', 'isPath']}
|
||||||
errorMessages={['Base is required', "Not a valid Path"]}
|
errorMessages={['Base is required', 'Not a valid Path']}
|
||||||
name="base"
|
name="base"
|
||||||
label="Base"
|
label="Base"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -87,7 +109,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
name="client_id"
|
name="client_id"
|
||||||
label="Client ID (optional)"
|
label="Client ID"
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={data.client_id}
|
value={data.client_id}
|
||||||
@@ -95,8 +117,18 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['required', 'isNumber', 'minNumber:1', 'maxNumber:65535']}
|
validators={[
|
||||||
errorMessages={['Keep alive is required', "Must be a number", "Must be greater than 0", "Max value is 65535"]}
|
'required',
|
||||||
|
'isNumber',
|
||||||
|
'minNumber:1',
|
||||||
|
'maxNumber:65535'
|
||||||
|
]}
|
||||||
|
errorMessages={[
|
||||||
|
'Keep alive is required',
|
||||||
|
'Must be a number',
|
||||||
|
'Must be greater than 0',
|
||||||
|
'Max value is 65535'
|
||||||
|
]}
|
||||||
name="keep_alive"
|
name="keep_alive"
|
||||||
label="Keep Alive (seconds)"
|
label="Keep Alive (seconds)"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -106,13 +138,15 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
onChange={handleValueChange('keep_alive')}
|
onChange={handleValueChange('keep_alive')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<SelectValidator name="mqtt_qos"
|
<SelectValidator
|
||||||
|
name="mqtt_qos"
|
||||||
label="QoS"
|
label="QoS"
|
||||||
value={data.mqtt_qos}
|
value={data.mqtt_qos}
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onChange={handleValueChange('mqtt_qos')}
|
onChange={handleValueChange('mqtt_qos')}
|
||||||
margin="normal">
|
margin="normal"
|
||||||
|
>
|
||||||
<MenuItem value={0}>0 (default)</MenuItem>
|
<MenuItem value={0}>0 (default)</MenuItem>
|
||||||
<MenuItem value={1}>1</MenuItem>
|
<MenuItem value={1}>1</MenuItem>
|
||||||
<MenuItem value={2}>2</MenuItem>
|
<MenuItem value={2}>2</MenuItem>
|
||||||
@@ -125,7 +159,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
value="clean_session"
|
value="clean_session"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label="Clean Session"
|
label="Set Clean Session"
|
||||||
/>
|
/>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={
|
control={
|
||||||
@@ -135,55 +169,34 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
value="mqtt_retain"
|
value="mqtt_retain"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label="Retain Flag"
|
label="Use Retain Flag"
|
||||||
/>
|
/>
|
||||||
<br></br>
|
<br></br>
|
||||||
<Typography variant="h6" color="primary" >
|
<Typography variant="h6" color="primary">
|
||||||
Formatting
|
Formatting
|
||||||
</Typography>
|
</Typography>
|
||||||
|
<SelectValidator
|
||||||
|
name="nested_format"
|
||||||
|
label="Topic/Payload Format"
|
||||||
|
value={data.nested_format}
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
onChange={handleValueChange('nested_format')}
|
||||||
|
margin="normal"
|
||||||
|
>
|
||||||
|
<MenuItem value={1}>Nested on a single topic</MenuItem>
|
||||||
|
<MenuItem value={2}>As individual topics</MenuItem>
|
||||||
|
</SelectValidator>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={data.nested_format}
|
checked={data.send_response}
|
||||||
onChange={handleValueChange('nested_format')}
|
onChange={handleValueChange('send_response')}
|
||||||
value="nested_format"
|
value="send_response"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label="Nested format (Thermostat & Mixer only)"
|
label="Publish command output to a 'response' topic"
|
||||||
/>
|
/>
|
||||||
<SelectValidator name="dallas_format"
|
|
||||||
label="Dallas Sensor Payload Grouping"
|
|
||||||
value={data.dallas_format}
|
|
||||||
fullWidth
|
|
||||||
variant="outlined"
|
|
||||||
onChange={handleValueChange('dallas_format')}
|
|
||||||
margin="normal">
|
|
||||||
<MenuItem value={1}>by Sensor ID</MenuItem>
|
|
||||||
<MenuItem value={2}>by Number</MenuItem>
|
|
||||||
</SelectValidator>
|
|
||||||
<SelectValidator name="bool_format"
|
|
||||||
label="Boolean Format"
|
|
||||||
value={data.bool_format}
|
|
||||||
fullWidth
|
|
||||||
variant="outlined"
|
|
||||||
onChange={handleValueChange('bool_format')}
|
|
||||||
margin="normal">
|
|
||||||
<MenuItem value={1}>"on"/"off"</MenuItem>
|
|
||||||
<MenuItem value={2}>true/false</MenuItem>
|
|
||||||
<MenuItem value={3}>1/0</MenuItem>
|
|
||||||
<MenuItem value={4}>"ON"/"OFF"</MenuItem>
|
|
||||||
</SelectValidator>
|
|
||||||
<SelectValidator name="subscribe_format"
|
|
||||||
label="Subscribe Format"
|
|
||||||
value={data.subscribe_format}
|
|
||||||
fullWidth
|
|
||||||
variant="outlined"
|
|
||||||
onChange={handleValueChange('subscribe_format')}
|
|
||||||
margin="normal">
|
|
||||||
<MenuItem value={0}>general device topic</MenuItem>
|
|
||||||
<MenuItem value={1}>individual topics, main heating circuit</MenuItem>
|
|
||||||
<MenuItem value={2}>individual topics, all heating circuits</MenuItem>
|
|
||||||
</SelectValidator>
|
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
@@ -192,28 +205,40 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
value="ha_enabled"
|
value="ha_enabled"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label="Home Assistant MQTT Discovery"
|
label="Use Home Assistant MQTT Discovery"
|
||||||
/>
|
/>
|
||||||
{ data.ha_enabled &&
|
{data.ha_enabled && (
|
||||||
<SelectValidator name="ha_climate_format"
|
<SelectValidator
|
||||||
|
name="ha_climate_format"
|
||||||
label="Thermostat Room Temperature"
|
label="Thermostat Room Temperature"
|
||||||
value={data.ha_climate_format}
|
value={data.ha_climate_format}
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onChange={handleValueChange('ha_climate_format')}
|
onChange={handleValueChange('ha_climate_format')}
|
||||||
margin="normal">
|
margin="normal"
|
||||||
<MenuItem value={1}>use Current temperature (default)</MenuItem>
|
>
|
||||||
<MenuItem value={2}>use Setpoint temperature</MenuItem>
|
<MenuItem value={1}>Use Current temperature</MenuItem>
|
||||||
<MenuItem value={3}>Fix to 0</MenuItem>
|
<MenuItem value={2}>Use Setpoint temperature</MenuItem>
|
||||||
|
<MenuItem value={3}>Always set to 0</MenuItem>
|
||||||
</SelectValidator>
|
</SelectValidator>
|
||||||
}
|
)}
|
||||||
<br></br>
|
<br></br>
|
||||||
<Typography variant="h6" color="primary" >
|
<Typography variant="h6" color="primary">
|
||||||
Publish Intervals
|
Publish Intervals
|
||||||
</Typography>
|
</Typography>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:65535']}
|
validators={[
|
||||||
errorMessages={['Publish time is required', "Must be a number", "Must be 0 or greater", "Max value is 65535"]}
|
'required',
|
||||||
|
'isNumber',
|
||||||
|
'minNumber:0',
|
||||||
|
'maxNumber:65535'
|
||||||
|
]}
|
||||||
|
errorMessages={[
|
||||||
|
'Publish time is required',
|
||||||
|
'Must be a number',
|
||||||
|
'Must be 0 or greater',
|
||||||
|
'Max value is 65535'
|
||||||
|
]}
|
||||||
name="publish_time_boiler"
|
name="publish_time_boiler"
|
||||||
label="Boiler Publish Interval (seconds, 0=on change)"
|
label="Boiler Publish Interval (seconds, 0=on change)"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -224,8 +249,18 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:65535']}
|
validators={[
|
||||||
errorMessages={['Publish time is required', "Must be a number", "Must be 0 or greater", "Max value is 65535"]}
|
'required',
|
||||||
|
'isNumber',
|
||||||
|
'minNumber:0',
|
||||||
|
'maxNumber:65535'
|
||||||
|
]}
|
||||||
|
errorMessages={[
|
||||||
|
'Publish time is required',
|
||||||
|
'Must be a number',
|
||||||
|
'Must be 0 or greater',
|
||||||
|
'Max value is 65535'
|
||||||
|
]}
|
||||||
name="publish_time_thermostat"
|
name="publish_time_thermostat"
|
||||||
label="Thermostat Publish Interval (seconds, 0=on change)"
|
label="Thermostat Publish Interval (seconds, 0=on change)"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -236,8 +271,18 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:65535']}
|
validators={[
|
||||||
errorMessages={['Publish time is required', "Must be a number", "Must be 0 or greater", "Max value is 65535"]}
|
'required',
|
||||||
|
'isNumber',
|
||||||
|
'minNumber:0',
|
||||||
|
'maxNumber:65535'
|
||||||
|
]}
|
||||||
|
errorMessages={[
|
||||||
|
'Publish time is required',
|
||||||
|
'Must be a number',
|
||||||
|
'Must be 0 or greater',
|
||||||
|
'Max value is 65535'
|
||||||
|
]}
|
||||||
name="publish_time_solar"
|
name="publish_time_solar"
|
||||||
label="Solar Publish Interval (seconds, 0=on change)"
|
label="Solar Publish Interval (seconds, 0=on change)"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -248,8 +293,18 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:65535']}
|
validators={[
|
||||||
errorMessages={['Publish time is required', "Must be a number", "Must be 0 or greater", "Max value is 65535"]}
|
'required',
|
||||||
|
'isNumber',
|
||||||
|
'minNumber:0',
|
||||||
|
'maxNumber:65535'
|
||||||
|
]}
|
||||||
|
errorMessages={[
|
||||||
|
'Publish time is required',
|
||||||
|
'Must be a number',
|
||||||
|
'Must be 0 or greater',
|
||||||
|
'Max value is 65535'
|
||||||
|
]}
|
||||||
name="publish_time_mixer"
|
name="publish_time_mixer"
|
||||||
label="Mixer Publish Interval (seconds, 0=on change)"
|
label="Mixer Publish Interval (seconds, 0=on change)"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -260,8 +315,18 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:65535']}
|
validators={[
|
||||||
errorMessages={['Publish time is required', "Must be a number", "Must be 0 or greater", "Max value is 65535"]}
|
'required',
|
||||||
|
'isNumber',
|
||||||
|
'minNumber:0',
|
||||||
|
'maxNumber:65535'
|
||||||
|
]}
|
||||||
|
errorMessages={[
|
||||||
|
'Publish time is required',
|
||||||
|
'Must be a number',
|
||||||
|
'Must be 0 or greater',
|
||||||
|
'Max value is 65535'
|
||||||
|
]}
|
||||||
name="publish_time_sensor"
|
name="publish_time_sensor"
|
||||||
label="Sensors Publish Interval (seconds, 0=on change)"
|
label="Sensors Publish Interval (seconds, 0=on change)"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -272,8 +337,18 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:65535']}
|
validators={[
|
||||||
errorMessages={['Publish time is required', "Must be a number", "Must be 0 or greater", "Max value is 65535"]}
|
'required',
|
||||||
|
'isNumber',
|
||||||
|
'minNumber:0',
|
||||||
|
'maxNumber:65535'
|
||||||
|
]}
|
||||||
|
errorMessages={[
|
||||||
|
'Publish time is required',
|
||||||
|
'Must be a number',
|
||||||
|
'Must be 0 or greater',
|
||||||
|
'Max value is 65535'
|
||||||
|
]}
|
||||||
name="publish_time_other"
|
name="publish_time_other"
|
||||||
label="All other Modules Publish Interval (seconds, 0=on change)"
|
label="All other Modules Publish Interval (seconds, 0=on change)"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -284,7 +359,12 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
|
<FormButton
|
||||||
|
startIcon={<SaveIcon />}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
Save
|
Save
|
||||||
</FormButton>
|
</FormButton>
|
||||||
</FormActions>
|
</FormActions>
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { Theme } from "@material-ui/core";
|
import { Theme } from '@material-ui/core';
|
||||||
import { MqttStatus, MqttDisconnectReason } from "./types";
|
import { MqttStatus, MqttDisconnectReason } from './types';
|
||||||
|
|
||||||
export const mqttStatusHighlight = ({ enabled, connected }: MqttStatus, theme: Theme) => {
|
export const mqttStatusHighlight = (
|
||||||
|
{ enabled, connected }: MqttStatus,
|
||||||
|
theme: Theme
|
||||||
|
) => {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return theme.palette.info.main;
|
return theme.palette.info.main;
|
||||||
}
|
}
|
||||||
@@ -9,48 +12,48 @@ export const mqttStatusHighlight = ({ enabled, connected }: MqttStatus, theme: T
|
|||||||
return theme.palette.success.main;
|
return theme.palette.success.main;
|
||||||
}
|
}
|
||||||
return theme.palette.error.main;
|
return theme.palette.error.main;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const mqttStatus = ({ enabled, connected }: MqttStatus) => {
|
export const mqttStatus = ({ enabled, connected }: MqttStatus) => {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return "Not enabled";
|
return 'Not enabled';
|
||||||
}
|
}
|
||||||
if (connected) {
|
if (connected) {
|
||||||
return "Connected";
|
return 'Connected';
|
||||||
}
|
}
|
||||||
return "Disconnected";
|
return 'Disconnected';
|
||||||
}
|
};
|
||||||
|
|
||||||
export const disconnectReason = ({ disconnect_reason }: MqttStatus) => {
|
export const disconnectReason = ({ disconnect_reason }: MqttStatus) => {
|
||||||
switch (disconnect_reason) {
|
switch (disconnect_reason) {
|
||||||
case MqttDisconnectReason.TCP_DISCONNECTED:
|
case MqttDisconnectReason.TCP_DISCONNECTED:
|
||||||
return "TCP disconnected";
|
return 'TCP disconnected';
|
||||||
case MqttDisconnectReason.MQTT_UNACCEPTABLE_PROTOCOL_VERSION:
|
case MqttDisconnectReason.MQTT_UNACCEPTABLE_PROTOCOL_VERSION:
|
||||||
return "Unacceptable protocol version";
|
return 'Unacceptable protocol version';
|
||||||
case MqttDisconnectReason.MQTT_IDENTIFIER_REJECTED:
|
case MqttDisconnectReason.MQTT_IDENTIFIER_REJECTED:
|
||||||
return "Client ID rejected";
|
return 'Client ID rejected';
|
||||||
case MqttDisconnectReason.MQTT_SERVER_UNAVAILABLE:
|
case MqttDisconnectReason.MQTT_SERVER_UNAVAILABLE:
|
||||||
return "Server unavailable";
|
return 'Server unavailable';
|
||||||
case MqttDisconnectReason.MQTT_MALFORMED_CREDENTIALS:
|
case MqttDisconnectReason.MQTT_MALFORMED_CREDENTIALS:
|
||||||
return "Malformed credentials";
|
return 'Malformed credentials';
|
||||||
case MqttDisconnectReason.MQTT_NOT_AUTHORIZED:
|
case MqttDisconnectReason.MQTT_NOT_AUTHORIZED:
|
||||||
return "Not authorized";
|
return 'Not authorized';
|
||||||
case MqttDisconnectReason.ESP8266_NOT_ENOUGH_SPACE:
|
case MqttDisconnectReason.ESP8266_NOT_ENOUGH_SPACE:
|
||||||
return "Device out of memory";
|
return 'Device out of memory';
|
||||||
case MqttDisconnectReason.TLS_BAD_FINGERPRINT:
|
case MqttDisconnectReason.TLS_BAD_FINGERPRINT:
|
||||||
return "Server fingerprint invalid";
|
return 'Server fingerprint invalid';
|
||||||
default:
|
default:
|
||||||
return "Unknown"
|
return 'Unknown';
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export const mqttPublishHighlight = ({ mqtt_fails }: MqttStatus, theme: Theme) => {
|
export const mqttPublishHighlight = (
|
||||||
|
{ mqtt_fails }: MqttStatus,
|
||||||
|
theme: Theme
|
||||||
|
) => {
|
||||||
|
if (mqtt_fails === 0) return theme.palette.success.main;
|
||||||
|
|
||||||
if (mqtt_fails === 0)
|
if (mqtt_fails < 10) return theme.palette.warning.main;
|
||||||
return theme.palette.success.main;
|
|
||||||
|
|
||||||
if (mqtt_fails < 10)
|
|
||||||
return theme.palette.warning.main;
|
|
||||||
|
|
||||||
return theme.palette.error.main;
|
return theme.palette.error.main;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
import {
|
||||||
|
restController,
|
||||||
|
RestControllerProps,
|
||||||
|
RestFormLoader,
|
||||||
|
SectionContent
|
||||||
|
} from '../components';
|
||||||
import { MQTT_STATUS_ENDPOINT } from '../api';
|
import { MQTT_STATUS_ENDPOINT } from '../api';
|
||||||
|
|
||||||
import MqttStatusForm from './MqttStatusForm';
|
import MqttStatusForm from './MqttStatusForm';
|
||||||
@@ -9,7 +14,6 @@ import { MqttStatus } from './types';
|
|||||||
type MqttStatusControllerProps = RestControllerProps<MqttStatus>;
|
type MqttStatusControllerProps = RestControllerProps<MqttStatus>;
|
||||||
|
|
||||||
class MqttStatusController extends Component<MqttStatusControllerProps> {
|
class MqttStatusController extends Component<MqttStatusControllerProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.loadData();
|
this.props.loadData();
|
||||||
}
|
}
|
||||||
@@ -19,10 +23,10 @@ class MqttStatusController extends Component<MqttStatusControllerProps> {
|
|||||||
<SectionContent title="MQTT Status">
|
<SectionContent title="MQTT Status">
|
||||||
<RestFormLoader
|
<RestFormLoader
|
||||||
{...this.props}
|
{...this.props}
|
||||||
render={formProps => <MqttStatusForm {...formProps} />}
|
render={(formProps) => <MqttStatusForm {...formProps} />}
|
||||||
/>
|
/>
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,39 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import { Component, Fragment } from 'react';
|
||||||
|
|
||||||
import { WithTheme, withTheme } from '@material-ui/core/styles';
|
import { WithTheme, withTheme } from '@material-ui/core/styles';
|
||||||
import { Avatar, Divider, List, ListItem, ListItemAvatar, ListItemText } from '@material-ui/core';
|
import {
|
||||||
|
Avatar,
|
||||||
|
Divider,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemAvatar,
|
||||||
|
ListItemText
|
||||||
|
} from '@material-ui/core';
|
||||||
|
|
||||||
import DeviceHubIcon from '@material-ui/icons/DeviceHub';
|
import DeviceHubIcon from '@material-ui/icons/DeviceHub';
|
||||||
import RefreshIcon from '@material-ui/icons/Refresh';
|
import RefreshIcon from '@material-ui/icons/Refresh';
|
||||||
import ReportIcon from '@material-ui/icons/Report';
|
import ReportIcon from '@material-ui/icons/Report';
|
||||||
import SpeakerNotesOffIcon from "@material-ui/icons/SpeakerNotesOff";
|
import SpeakerNotesOffIcon from '@material-ui/icons/SpeakerNotesOff';
|
||||||
|
|
||||||
import { RestFormProps, FormActions, FormButton, HighlightAvatar } from '../components';
|
import {
|
||||||
import { mqttStatusHighlight, mqttStatus, mqttPublishHighlight, disconnectReason } from './MqttStatus';
|
RestFormProps,
|
||||||
|
FormActions,
|
||||||
|
FormButton,
|
||||||
|
HighlightAvatar
|
||||||
|
} from '../components';
|
||||||
|
import {
|
||||||
|
mqttStatusHighlight,
|
||||||
|
mqttStatus,
|
||||||
|
mqttPublishHighlight,
|
||||||
|
disconnectReason
|
||||||
|
} from './MqttStatus';
|
||||||
import { MqttStatus } from './types';
|
import { MqttStatus } from './types';
|
||||||
|
|
||||||
type MqttStatusFormProps = RestFormProps<MqttStatus> & WithTheme;
|
type MqttStatusFormProps = RestFormProps<MqttStatus> & WithTheme;
|
||||||
|
|
||||||
class MqttStatusForm extends Component<MqttStatusFormProps> {
|
class MqttStatusForm extends Component<MqttStatusFormProps> {
|
||||||
|
|
||||||
renderConnectionStatus() {
|
renderConnectionStatus() {
|
||||||
const { data, theme } = this.props
|
const { data, theme } = this.props;
|
||||||
if (data.connected) {
|
if (data.connected) {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
@@ -29,16 +45,16 @@ class MqttStatusForm extends Component<MqttStatusFormProps> {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<HighlightAvatar color={mqttPublishHighlight(data, theme)}>
|
<HighlightAvatar color={mqttPublishHighlight(data, theme)}>
|
||||||
<SpeakerNotesOffIcon />
|
<SpeakerNotesOffIcon />
|
||||||
</HighlightAvatar>
|
</HighlightAvatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary="MQTT Publish Errors"
|
primary="MQTT Publish Errors"
|
||||||
secondary={data.mqtt_fails}
|
secondary={data.mqtt_fails}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -50,7 +66,10 @@ class MqttStatusForm extends Component<MqttStatusFormProps> {
|
|||||||
<ReportIcon />
|
<ReportIcon />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary="Disconnect Reason" secondary={disconnectReason(data)} />
|
<ListItemText
|
||||||
|
primary="Disconnect Reason"
|
||||||
|
secondary={disconnectReason(data)}
|
||||||
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
@@ -58,7 +77,7 @@ class MqttStatusForm extends Component<MqttStatusFormProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createListItems() {
|
createListItems() {
|
||||||
const { data, theme } = this.props
|
const { data, theme } = this.props;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
@@ -78,18 +97,20 @@ class MqttStatusForm extends Component<MqttStatusFormProps> {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<List>
|
<List>{this.createListItems()}</List>
|
||||||
{this.createListItems()}
|
|
||||||
</List>
|
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<FormButton startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={this.props.loadData}>
|
<FormButton
|
||||||
|
startIcon={<RefreshIcon />}
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
onClick={this.props.loadData}
|
||||||
|
>
|
||||||
Refresh
|
Refresh
|
||||||
</FormButton>
|
</FormButton>
|
||||||
</FormActions>
|
</FormActions>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withTheme(MqttStatusForm);
|
export default withTheme(MqttStatusForm);
|
||||||
|
|||||||
@@ -34,12 +34,10 @@ export interface MqttSettings {
|
|||||||
publish_time_mixer: number;
|
publish_time_mixer: number;
|
||||||
publish_time_other: number;
|
publish_time_other: number;
|
||||||
publish_time_sensor: number;
|
publish_time_sensor: number;
|
||||||
dallas_format: number;
|
|
||||||
bool_format: number;
|
|
||||||
mqtt_qos: number;
|
mqtt_qos: number;
|
||||||
mqtt_retain: boolean;
|
mqtt_retain: boolean;
|
||||||
ha_enabled: boolean;
|
ha_enabled: boolean;
|
||||||
ha_climate_format: number;
|
ha_climate_format: number;
|
||||||
nested_format: boolean;
|
nested_format: number;
|
||||||
subscribe_format: number;
|
send_response: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,31 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
|
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
|
||||||
|
|
||||||
import { Tabs, Tab } from '@material-ui/core';
|
import { Tabs, Tab } from '@material-ui/core';
|
||||||
|
|
||||||
import { withAuthenticatedContext, AuthenticatedContextProps, AuthenticatedRoute } from '../authentication';
|
import {
|
||||||
|
withAuthenticatedContext,
|
||||||
|
AuthenticatedContextProps,
|
||||||
|
AuthenticatedRoute
|
||||||
|
} from '../authentication';
|
||||||
import { MenuAppBar } from '../components';
|
import { MenuAppBar } from '../components';
|
||||||
|
|
||||||
import NetworkStatusController from './NetworkStatusController';
|
import NetworkStatusController from './NetworkStatusController';
|
||||||
import NetworkSettingsController from './NetworkSettingsController';
|
import NetworkSettingsController from './NetworkSettingsController';
|
||||||
import WiFiNetworkScanner from './WiFiNetworkScanner';
|
import WiFiNetworkScanner from './WiFiNetworkScanner';
|
||||||
import { NetworkConnectionContext, NetworkConnectionContextValue } from './NetworkConnectionContext';
|
import {
|
||||||
|
NetworkConnectionContext,
|
||||||
|
NetworkConnectionContextValue
|
||||||
|
} from './NetworkConnectionContext';
|
||||||
|
|
||||||
import { WiFiNetwork } from './types';
|
import { WiFiNetwork } from './types';
|
||||||
|
|
||||||
type NetworkConnectionProps = AuthenticatedContextProps & RouteComponentProps;
|
type NetworkConnectionProps = AuthenticatedContextProps & RouteComponentProps;
|
||||||
|
|
||||||
class NetworkConnection extends Component<NetworkConnectionProps, NetworkConnectionContextValue> {
|
class NetworkConnection extends Component<
|
||||||
|
NetworkConnectionProps,
|
||||||
|
NetworkConnectionContextValue
|
||||||
|
> {
|
||||||
constructor(props: NetworkConnectionProps) {
|
constructor(props: NetworkConnectionProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
@@ -28,13 +37,13 @@ class NetworkConnection extends Component<NetworkConnectionProps, NetworkConnect
|
|||||||
selectNetwork = (network: WiFiNetwork) => {
|
selectNetwork = (network: WiFiNetwork) => {
|
||||||
this.setState({ selectedNetwork: network });
|
this.setState({ selectedNetwork: network });
|
||||||
this.props.history.push('/network/settings');
|
this.props.history.push('/network/settings');
|
||||||
}
|
};
|
||||||
|
|
||||||
deselectNetwork = () => {
|
deselectNetwork = () => {
|
||||||
this.setState({ selectedNetwork: undefined });
|
this.setState({ selectedNetwork: undefined });
|
||||||
}
|
};
|
||||||
|
|
||||||
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
|
handleTabChange = (path: string) => {
|
||||||
this.props.history.push(path);
|
this.props.history.push(path);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -43,20 +52,44 @@ class NetworkConnection extends Component<NetworkConnectionProps, NetworkConnect
|
|||||||
return (
|
return (
|
||||||
<NetworkConnectionContext.Provider value={this.state}>
|
<NetworkConnectionContext.Provider value={this.state}>
|
||||||
<MenuAppBar sectionTitle="Network Connection">
|
<MenuAppBar sectionTitle="Network Connection">
|
||||||
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
|
<Tabs
|
||||||
|
value={this.props.match.url}
|
||||||
|
onChange={(e, path) => this.handleTabChange(path)}
|
||||||
|
variant="fullWidth"
|
||||||
|
>
|
||||||
<Tab value="/network/status" label="Network Status" />
|
<Tab value="/network/status" label="Network Status" />
|
||||||
<Tab value="/network/scan" label="Scan WiFi Networks" disabled={!authenticatedContext.me.admin} />
|
<Tab
|
||||||
<Tab value="/network/settings" label="Network Settings" disabled={!authenticatedContext.me.admin} />
|
value="/network/scan"
|
||||||
|
label="Scan WiFi Networks"
|
||||||
|
disabled={!authenticatedContext.me.admin}
|
||||||
|
/>
|
||||||
|
<Tab
|
||||||
|
value="/network/settings"
|
||||||
|
label="Network Settings"
|
||||||
|
disabled={!authenticatedContext.me.admin}
|
||||||
|
/>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<Switch>
|
<Switch>
|
||||||
<AuthenticatedRoute exact path="/network/status" component={NetworkStatusController} />
|
<AuthenticatedRoute
|
||||||
<AuthenticatedRoute exact path="/network/scan" component={WiFiNetworkScanner} />
|
exact
|
||||||
<AuthenticatedRoute exact path="/network/settings" component={NetworkSettingsController} />
|
path="/network/status"
|
||||||
|
component={NetworkStatusController}
|
||||||
|
/>
|
||||||
|
<AuthenticatedRoute
|
||||||
|
exact
|
||||||
|
path="/network/scan"
|
||||||
|
component={WiFiNetworkScanner}
|
||||||
|
/>
|
||||||
|
<AuthenticatedRoute
|
||||||
|
exact
|
||||||
|
path="/network/settings"
|
||||||
|
component={NetworkSettingsController}
|
||||||
|
/>
|
||||||
<Redirect to="/network/status" />
|
<Redirect to="/network/status" />
|
||||||
</Switch>
|
</Switch>
|
||||||
</MenuAppBar>
|
</MenuAppBar>
|
||||||
</NetworkConnectionContext.Provider>
|
</NetworkConnectionContext.Provider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export interface NetworkConnectionContextValue {
|
|||||||
deselectNetwork: () => void;
|
deselectNetwork: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NetworkConnectionContextDefaultValue = {} as NetworkConnectionContextValue
|
const NetworkConnectionContextDefaultValue = {} as NetworkConnectionContextValue;
|
||||||
export const NetworkConnectionContext = React.createContext(
|
export const NetworkConnectionContext = React.createContext(
|
||||||
NetworkConnectionContextDefaultValue
|
NetworkConnectionContextDefaultValue
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
|
|
||||||
import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
import {
|
||||||
|
restController,
|
||||||
|
RestControllerProps,
|
||||||
|
RestFormLoader,
|
||||||
|
SectionContent
|
||||||
|
} from '../components';
|
||||||
import NetworkSettingsForm from './NetworkSettingsForm';
|
import NetworkSettingsForm from './NetworkSettingsForm';
|
||||||
import { NETWORK_SETTINGS_ENDPOINT } from '../api';
|
import { NETWORK_SETTINGS_ENDPOINT } from '../api';
|
||||||
import { NetworkSettings } from './types';
|
import { NetworkSettings } from './types';
|
||||||
@@ -8,7 +13,6 @@ import { NetworkSettings } from './types';
|
|||||||
type NetworkSettingsControllerProps = RestControllerProps<NetworkSettings>;
|
type NetworkSettingsControllerProps = RestControllerProps<NetworkSettings>;
|
||||||
|
|
||||||
class NetworkSettingsController extends Component<NetworkSettingsControllerProps> {
|
class NetworkSettingsController extends Component<NetworkSettingsControllerProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.loadData();
|
this.props.loadData();
|
||||||
}
|
}
|
||||||
@@ -18,12 +22,14 @@ class NetworkSettingsController extends Component<NetworkSettingsControllerProps
|
|||||||
<SectionContent title="Network Settings">
|
<SectionContent title="Network Settings">
|
||||||
<RestFormLoader
|
<RestFormLoader
|
||||||
{...this.props}
|
{...this.props}
|
||||||
render={formProps => <NetworkSettingsForm {...formProps} />}
|
render={(formProps) => <NetworkSettingsForm {...formProps} />}
|
||||||
/>
|
/>
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default restController(NETWORK_SETTINGS_ENDPOINT, NetworkSettingsController);
|
export default restController(
|
||||||
|
NETWORK_SETTINGS_ENDPOINT,
|
||||||
|
NetworkSettingsController
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
|
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
|
||||||
|
|
||||||
import { Checkbox, List, ListItem, ListItemText, ListItemAvatar, ListItemSecondaryAction } from '@material-ui/core';
|
import {
|
||||||
|
Checkbox,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemText,
|
||||||
|
ListItemAvatar,
|
||||||
|
ListItemSecondaryAction
|
||||||
|
} from '@material-ui/core';
|
||||||
|
|
||||||
import Avatar from '@material-ui/core/Avatar';
|
import Avatar from '@material-ui/core/Avatar';
|
||||||
import IconButton from '@material-ui/core/IconButton';
|
import IconButton from '@material-ui/core/IconButton';
|
||||||
@@ -10,31 +17,46 @@ import LockOpenIcon from '@material-ui/icons/LockOpen';
|
|||||||
import DeleteIcon from '@material-ui/icons/Delete';
|
import DeleteIcon from '@material-ui/icons/Delete';
|
||||||
import SaveIcon from '@material-ui/icons/Save';
|
import SaveIcon from '@material-ui/icons/Save';
|
||||||
|
|
||||||
import { RestFormProps, PasswordValidator, BlockFormControlLabel, FormActions, FormButton } from '../components';
|
import {
|
||||||
|
RestFormProps,
|
||||||
|
PasswordValidator,
|
||||||
|
BlockFormControlLabel,
|
||||||
|
FormActions,
|
||||||
|
FormButton
|
||||||
|
} from '../components';
|
||||||
import { isIP, isHostname, optional } from '../validators';
|
import { isIP, isHostname, optional } from '../validators';
|
||||||
|
|
||||||
import { NetworkConnectionContext, NetworkConnectionContextValue } from './NetworkConnectionContext';
|
import {
|
||||||
|
NetworkConnectionContext,
|
||||||
|
NetworkConnectionContextValue
|
||||||
|
} from './NetworkConnectionContext';
|
||||||
import { isNetworkOpen, networkSecurityMode } from './WiFiSecurityModes';
|
import { isNetworkOpen, networkSecurityMode } from './WiFiSecurityModes';
|
||||||
import { NetworkSettings } from './types';
|
import { NetworkSettings } from './types';
|
||||||
|
|
||||||
type NetworkStatusFormProps = RestFormProps<NetworkSettings>;
|
type NetworkStatusFormProps = RestFormProps<NetworkSettings>;
|
||||||
|
|
||||||
class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
|
class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
|
||||||
|
|
||||||
static contextType = NetworkConnectionContext;
|
static contextType = NetworkConnectionContext;
|
||||||
context!: React.ContextType<typeof NetworkConnectionContext>;
|
context!: React.ContextType<typeof NetworkConnectionContext>;
|
||||||
|
|
||||||
constructor(props: NetworkStatusFormProps, context: NetworkConnectionContextValue) {
|
constructor(
|
||||||
|
props: NetworkStatusFormProps,
|
||||||
|
context: NetworkConnectionContextValue
|
||||||
|
) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const { selectedNetwork } = context;
|
const { selectedNetwork } = context;
|
||||||
if (selectedNetwork) {
|
if (selectedNetwork) {
|
||||||
const networkSettings: NetworkSettings = {
|
const networkSettings: NetworkSettings = {
|
||||||
ssid: selectedNetwork.ssid,
|
ssid: selectedNetwork.ssid,
|
||||||
password: "",
|
password: '',
|
||||||
hostname: props.data.hostname,
|
hostname: props.data.hostname,
|
||||||
static_ip_config: false,
|
static_ip_config: false,
|
||||||
}
|
enableIPv6: false,
|
||||||
|
bandwidth20: false,
|
||||||
|
tx_power: 20,
|
||||||
|
nosleep: false
|
||||||
|
};
|
||||||
props.setData(networkSettings);
|
props.setData(networkSettings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -48,7 +70,7 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
|
|||||||
deselectNetworkAndLoadData = () => {
|
deselectNetworkAndLoadData = () => {
|
||||||
this.context.deselectNetwork();
|
this.context.deselectNetwork();
|
||||||
this.props.loadData();
|
this.props.loadData();
|
||||||
}
|
};
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.context.deselectNetwork();
|
this.context.deselectNetwork();
|
||||||
@@ -59,41 +81,51 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
|
|||||||
const { data, handleValueChange, saveData } = this.props;
|
const { data, handleValueChange, saveData } = this.props;
|
||||||
return (
|
return (
|
||||||
<ValidatorForm onSubmit={saveData} ref="NetworkSettingsForm">
|
<ValidatorForm onSubmit={saveData} ref="NetworkSettingsForm">
|
||||||
{
|
{selectedNetwork ? (
|
||||||
selectedNetwork ?
|
<List>
|
||||||
<List>
|
<ListItem>
|
||||||
<ListItem>
|
<ListItemAvatar>
|
||||||
<ListItemAvatar>
|
<Avatar>
|
||||||
<Avatar>
|
{isNetworkOpen(selectedNetwork) ? (
|
||||||
{isNetworkOpen(selectedNetwork) ? <LockOpenIcon /> : <LockIcon />}
|
<LockOpenIcon />
|
||||||
</Avatar>
|
) : (
|
||||||
</ListItemAvatar>
|
<LockIcon />
|
||||||
<ListItemText
|
)}
|
||||||
primary={selectedNetwork.ssid}
|
</Avatar>
|
||||||
secondary={"Security: " + networkSecurityMode(selectedNetwork) + ", Ch: " + selectedNetwork.channel}
|
</ListItemAvatar>
|
||||||
/>
|
<ListItemText
|
||||||
<ListItemSecondaryAction>
|
primary={selectedNetwork.ssid}
|
||||||
<IconButton aria-label="Manual Config" onClick={deselectNetwork}>
|
secondary={
|
||||||
<DeleteIcon />
|
'Security: ' +
|
||||||
</IconButton>
|
networkSecurityMode(selectedNetwork) +
|
||||||
</ListItemSecondaryAction>
|
', Ch: ' +
|
||||||
</ListItem>
|
selectedNetwork.channel
|
||||||
</List>
|
}
|
||||||
:
|
/>
|
||||||
<TextValidator
|
<ListItemSecondaryAction>
|
||||||
validators={['matchRegexp:^.{0,32}$']}
|
<IconButton
|
||||||
errorMessages={['SSID must be 32 characters or less']}
|
aria-label="Manual Config"
|
||||||
name="ssid"
|
onClick={deselectNetwork}
|
||||||
label="SSID (leave blank to disable WiFi)"
|
>
|
||||||
fullWidth
|
<DeleteIcon />
|
||||||
variant="outlined"
|
</IconButton>
|
||||||
value={data.ssid}
|
</ListItemSecondaryAction>
|
||||||
onChange={handleValueChange('ssid')}
|
</ListItem>
|
||||||
margin="normal"
|
</List>
|
||||||
/>
|
) : (
|
||||||
}
|
<TextValidator
|
||||||
{
|
validators={['matchRegexp:^.{0,32}$']}
|
||||||
(!selectedNetwork || !isNetworkOpen(selectedNetwork)) &&
|
errorMessages={['SSID must be 32 characters or less']}
|
||||||
|
name="ssid"
|
||||||
|
label="SSID (leave blank to disable WiFi)"
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={data.ssid}
|
||||||
|
onChange={handleValueChange('ssid')}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{(!selectedNetwork || !isNetworkOpen(selectedNetwork)) && (
|
||||||
<PasswordValidator
|
<PasswordValidator
|
||||||
validators={['matchRegexp:^.{0,64}$']}
|
validators={['matchRegexp:^.{0,64}$']}
|
||||||
errorMessages={['Password must be 64 characters or less']}
|
errorMessages={['Password must be 64 characters or less']}
|
||||||
@@ -105,10 +137,10 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
|
|||||||
onChange={handleValueChange('password')}
|
onChange={handleValueChange('password')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['required', 'isHostname']}
|
validators={['required', 'isHostname']}
|
||||||
errorMessages={['Hostname is required', "Not a valid hostname"]}
|
errorMessages={['Hostname is required', 'Not a valid hostname']}
|
||||||
name="hostname"
|
name="hostname"
|
||||||
label="Hostname"
|
label="Hostname"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -117,18 +149,64 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
|
|||||||
onChange={handleValueChange('hostname')}
|
onChange={handleValueChange('hostname')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
|
<TextValidator
|
||||||
|
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:20']}
|
||||||
|
errorMessages={[
|
||||||
|
'Tx Power is required',
|
||||||
|
'Must be a number',
|
||||||
|
'Must be greater than 0dBm ',
|
||||||
|
'Max value is 20dBm'
|
||||||
|
]}
|
||||||
|
name="tx_power"
|
||||||
|
label="WiFi Tx Power (dBm)"
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={data.tx_power}
|
||||||
|
type="number"
|
||||||
|
onChange={handleValueChange('tx_power')}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
value="enableIPv6"
|
||||||
|
checked={data.enableIPv6}
|
||||||
|
onChange={handleValueChange('enableIPv6')}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Enable IPv6 support"
|
||||||
|
/>
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
value="bandwidth20"
|
||||||
|
checked={data.bandwidth20}
|
||||||
|
onChange={handleValueChange('bandwidth20')}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Use Lower WiFi Bandwidth"
|
||||||
|
/>
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
value="nosleep"
|
||||||
|
checked={data.nosleep}
|
||||||
|
onChange={handleValueChange('nosleep')}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Disable WiFi Sleep Mode"
|
||||||
|
/>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
value="static_ip_config"
|
value="static_ip_config"
|
||||||
checked={data.static_ip_config}
|
checked={data.static_ip_config}
|
||||||
onChange={handleValueChange("static_ip_config")}
|
onChange={handleValueChange('static_ip_config')}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label="Static IP Config"
|
label="Use Static IPs"
|
||||||
/>
|
/>
|
||||||
{
|
{data.static_ip_config && (
|
||||||
data.static_ip_config &&
|
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['required', 'isIP']}
|
validators={['required', 'isIP']}
|
||||||
@@ -154,7 +232,10 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
|
|||||||
/>
|
/>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['required', 'isIP']}
|
validators={['required', 'isIP']}
|
||||||
errorMessages={['Subnet mask is required', 'Must be an IP address']}
|
errorMessages={[
|
||||||
|
'Subnet mask is required',
|
||||||
|
'Must be an IP address'
|
||||||
|
]}
|
||||||
name="subnet_mask"
|
name="subnet_mask"
|
||||||
label="Subnet"
|
label="Subnet"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -186,9 +267,14 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
}
|
)}
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
|
<FormButton
|
||||||
|
startIcon={<SaveIcon />}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
Save
|
Save
|
||||||
</FormButton>
|
</FormButton>
|
||||||
</FormActions>
|
</FormActions>
|
||||||
|
|||||||
@@ -2,12 +2,21 @@ import { Theme } from '@material-ui/core';
|
|||||||
import { NetworkStatus, NetworkConnectionStatus } from './types';
|
import { NetworkStatus, NetworkConnectionStatus } from './types';
|
||||||
|
|
||||||
export const isConnected = ({ status }: NetworkStatus) => {
|
export const isConnected = ({ status }: NetworkStatus) => {
|
||||||
return ((status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED) || (status === NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED));
|
return (
|
||||||
}
|
status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED ||
|
||||||
|
status === NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const isWiFi = ({ status }: NetworkStatus) => (status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED)
|
export const isWiFi = ({ status }: NetworkStatus) =>
|
||||||
|
status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED;
|
||||||
|
export const isEthernet = ({ status }: NetworkStatus) =>
|
||||||
|
status === NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED;
|
||||||
|
|
||||||
export const networkStatusHighlight = ({ status }: NetworkStatus, theme: Theme) => {
|
export const networkStatusHighlight = (
|
||||||
|
{ status }: NetworkStatus,
|
||||||
|
theme: Theme
|
||||||
|
) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case NetworkConnectionStatus.WIFI_STATUS_IDLE:
|
case NetworkConnectionStatus.WIFI_STATUS_IDLE:
|
||||||
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
|
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
|
||||||
@@ -22,27 +31,27 @@ export const networkStatusHighlight = ({ status }: NetworkStatus, theme: Theme)
|
|||||||
default:
|
default:
|
||||||
return theme.palette.warning.main;
|
return theme.palette.warning.main;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export const networkStatus = ({ status }: NetworkStatus) => {
|
export const networkStatus = ({ status }: NetworkStatus) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case NetworkConnectionStatus.WIFI_STATUS_NO_SHIELD:
|
case NetworkConnectionStatus.WIFI_STATUS_NO_SHIELD:
|
||||||
return "Inactive";
|
return 'Inactive';
|
||||||
case NetworkConnectionStatus.WIFI_STATUS_IDLE:
|
case NetworkConnectionStatus.WIFI_STATUS_IDLE:
|
||||||
return "Idle";
|
return 'Idle';
|
||||||
case NetworkConnectionStatus.WIFI_STATUS_NO_SSID_AVAIL:
|
case NetworkConnectionStatus.WIFI_STATUS_NO_SSID_AVAIL:
|
||||||
return "No SSID Available";
|
return 'No SSID Available';
|
||||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECTED:
|
case NetworkConnectionStatus.WIFI_STATUS_CONNECTED:
|
||||||
return "Connected (WiFi)";
|
return 'Connected (WiFi)';
|
||||||
case NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED:
|
case NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED:
|
||||||
return "Connected (Ethernet)";
|
return 'Connected (Ethernet)';
|
||||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECT_FAILED:
|
case NetworkConnectionStatus.WIFI_STATUS_CONNECT_FAILED:
|
||||||
return "Connection Failed";
|
return 'Connection Failed';
|
||||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECTION_LOST:
|
case NetworkConnectionStatus.WIFI_STATUS_CONNECTION_LOST:
|
||||||
return "Connection Lost";
|
return 'Connection Lost';
|
||||||
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
|
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
|
||||||
return "Disconnected";
|
return 'Disconnected';
|
||||||
default:
|
default:
|
||||||
return "Unknown";
|
return 'Unknown';
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
|
|
||||||
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
import {
|
||||||
|
restController,
|
||||||
|
RestControllerProps,
|
||||||
|
RestFormLoader,
|
||||||
|
SectionContent
|
||||||
|
} from '../components';
|
||||||
import NetworkStatusForm from './NetworkStatusForm';
|
import NetworkStatusForm from './NetworkStatusForm';
|
||||||
import { NETWORK_STATUS_ENDPOINT } from '../api';
|
import { NETWORK_STATUS_ENDPOINT } from '../api';
|
||||||
import { NetworkStatus } from './types';
|
import { NetworkStatus } from './types';
|
||||||
@@ -8,7 +13,6 @@ import { NetworkStatus } from './types';
|
|||||||
type NetworkStatusControllerProps = RestControllerProps<NetworkStatus>;
|
type NetworkStatusControllerProps = RestControllerProps<NetworkStatus>;
|
||||||
|
|
||||||
class NetworkStatusController extends Component<NetworkStatusControllerProps> {
|
class NetworkStatusController extends Component<NetworkStatusControllerProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.loadData();
|
this.props.loadData();
|
||||||
}
|
}
|
||||||
@@ -18,12 +22,11 @@ class NetworkStatusController extends Component<NetworkStatusControllerProps> {
|
|||||||
<SectionContent title="Network Status">
|
<SectionContent title="Network Status">
|
||||||
<RestFormLoader
|
<RestFormLoader
|
||||||
{...this.props}
|
{...this.props}
|
||||||
render={formProps => <NetworkStatusForm {...formProps} />}
|
render={(formProps) => <NetworkStatusForm {...formProps} />}
|
||||||
/>
|
/>
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default restController(NETWORK_STATUS_ENDPOINT, NetworkStatusController);
|
export default restController(NETWORK_STATUS_ENDPOINT, NetworkStatusController);
|
||||||
|
|||||||
@@ -1,45 +1,79 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import { Component, Fragment } from 'react';
|
||||||
|
|
||||||
import { WithTheme, withTheme } from '@material-ui/core/styles';
|
import { WithTheme, withTheme } from '@material-ui/core/styles';
|
||||||
import { Avatar, Divider, List, ListItem, ListItemAvatar, ListItemText } from '@material-ui/core';
|
import {
|
||||||
|
Avatar,
|
||||||
|
Divider,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemAvatar,
|
||||||
|
ListItemText
|
||||||
|
} from '@material-ui/core';
|
||||||
|
|
||||||
import DNSIcon from '@material-ui/icons/Dns';
|
import DNSIcon from '@material-ui/icons/Dns';
|
||||||
import WifiIcon from '@material-ui/icons/Wifi';
|
import WifiIcon from '@material-ui/icons/Wifi';
|
||||||
|
import RouterIcon from '@material-ui/icons/Router';
|
||||||
import SettingsInputComponentIcon from '@material-ui/icons/SettingsInputComponent';
|
import SettingsInputComponentIcon from '@material-ui/icons/SettingsInputComponent';
|
||||||
import SettingsInputAntennaIcon from '@material-ui/icons/SettingsInputAntenna';
|
import SettingsInputAntennaIcon from '@material-ui/icons/SettingsInputAntenna';
|
||||||
import DeviceHubIcon from '@material-ui/icons/DeviceHub';
|
import DeviceHubIcon from '@material-ui/icons/DeviceHub';
|
||||||
import RefreshIcon from '@material-ui/icons/Refresh';
|
import RefreshIcon from '@material-ui/icons/Refresh';
|
||||||
|
|
||||||
import { RestFormProps, FormActions, FormButton, HighlightAvatar } from '../components';
|
import {
|
||||||
import { networkStatus, networkStatusHighlight, isConnected, isWiFi } from './NetworkStatus';
|
RestFormProps,
|
||||||
|
FormActions,
|
||||||
|
FormButton,
|
||||||
|
HighlightAvatar
|
||||||
|
} from '../components';
|
||||||
|
import {
|
||||||
|
networkStatus,
|
||||||
|
networkStatusHighlight,
|
||||||
|
isConnected,
|
||||||
|
isWiFi,
|
||||||
|
isEthernet
|
||||||
|
} from './NetworkStatus';
|
||||||
import { NetworkStatus } from './types';
|
import { NetworkStatus } from './types';
|
||||||
|
|
||||||
type NetworkStatusFormProps = RestFormProps<NetworkStatus> & WithTheme;
|
type NetworkStatusFormProps = RestFormProps<NetworkStatus> & WithTheme;
|
||||||
|
|
||||||
class NetworkStatusForm extends Component<NetworkStatusFormProps> {
|
class NetworkStatusForm extends Component<NetworkStatusFormProps> {
|
||||||
|
|
||||||
dnsServers(status: NetworkStatus) {
|
dnsServers(status: NetworkStatus) {
|
||||||
if (!status.dns_ip_1) {
|
if (!status.dns_ip_1) {
|
||||||
return "none";
|
return 'none';
|
||||||
}
|
}
|
||||||
return status.dns_ip_1 + (status.dns_ip_2 ? ',' + status.dns_ip_2 : '');
|
if (!status.dns_ip_2 || status.dns_ip_2 === '0.0.0.0') {
|
||||||
|
return status.dns_ip_1;
|
||||||
|
}
|
||||||
|
return status.dns_ip_1 + ', ' + status.dns_ip_2;
|
||||||
|
}
|
||||||
|
IPs(status: NetworkStatus) {
|
||||||
|
if (
|
||||||
|
!status.local_ipv6 ||
|
||||||
|
status.local_ipv6 === '0000:0000:0000:0000:0000:0000:0000:0000'
|
||||||
|
) {
|
||||||
|
return status.local_ip;
|
||||||
|
}
|
||||||
|
if (!status.local_ip || status.local_ip === '0.0.0.0') {
|
||||||
|
return status.local_ipv6;
|
||||||
|
}
|
||||||
|
return status.local_ip + ', ' + status.local_ipv6;
|
||||||
}
|
}
|
||||||
|
|
||||||
createListItems() {
|
createListItems() {
|
||||||
const { data, theme } = this.props
|
const { data, theme } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<HighlightAvatar color={networkStatusHighlight(data, theme)}>
|
<HighlightAvatar color={networkStatusHighlight(data, theme)}>
|
||||||
<WifiIcon />
|
{isWiFi(data) && <WifiIcon />}
|
||||||
|
{isEthernet(data) && <RouterIcon />}
|
||||||
</HighlightAvatar>
|
</HighlightAvatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary="Status" secondary={networkStatus(data)} />
|
<ListItemText primary="Status" secondary={networkStatus(data)} />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
{
|
{isWiFi(data) && (
|
||||||
isWiFi(data) &&
|
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
@@ -51,14 +85,14 @@ class NetworkStatusForm extends Component<NetworkStatusFormProps> {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
}
|
)}
|
||||||
{ isConnected(data) &&
|
{isConnected(data) && (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<Avatar>IP</Avatar>
|
<Avatar>IP</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary="IP Address" secondary={data.local_ip} />
|
<ListItemText primary="IP Address" secondary={this.IPs(data)} />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
<ListItem>
|
<ListItem>
|
||||||
@@ -67,14 +101,20 @@ class NetworkStatusForm extends Component<NetworkStatusFormProps> {
|
|||||||
<DeviceHubIcon />
|
<DeviceHubIcon />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary="MAC Address" secondary={data.mac_address} />
|
<ListItemText
|
||||||
|
primary="MAC Address"
|
||||||
|
secondary={data.mac_address}
|
||||||
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<Avatar>#</Avatar>
|
<Avatar>#</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary="Subnet Mask" secondary={data.subnet_mask} />
|
<ListItemText
|
||||||
|
primary="Subnet Mask"
|
||||||
|
secondary={data.subnet_mask}
|
||||||
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
<ListItem>
|
<ListItem>
|
||||||
@@ -83,7 +123,10 @@ class NetworkStatusForm extends Component<NetworkStatusFormProps> {
|
|||||||
<SettingsInputComponentIcon />
|
<SettingsInputComponentIcon />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary="Gateway IP" secondary={data.gateway_ip || "none"} />
|
<ListItemText
|
||||||
|
primary="Gateway IP"
|
||||||
|
secondary={data.gateway_ip || 'none'}
|
||||||
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
<ListItem>
|
<ListItem>
|
||||||
@@ -92,11 +135,14 @@ class NetworkStatusForm extends Component<NetworkStatusFormProps> {
|
|||||||
<DNSIcon />
|
<DNSIcon />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary="DNS Server IP" secondary={this.dnsServers(data)} />
|
<ListItemText
|
||||||
|
primary="DNS Server IP"
|
||||||
|
secondary={this.dnsServers(data)}
|
||||||
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
}
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -104,18 +150,20 @@ class NetworkStatusForm extends Component<NetworkStatusFormProps> {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<List>
|
<List>{this.createListItems()}</List>
|
||||||
{this.createListItems()}
|
|
||||||
</List>
|
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<FormButton startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={this.props.loadData}>
|
<FormButton
|
||||||
|
startIcon={<RefreshIcon />}
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
onClick={this.props.loadData}
|
||||||
|
>
|
||||||
Refresh
|
Refresh
|
||||||
</FormButton>
|
</FormButton>
|
||||||
</FormActions>
|
</FormActions>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withTheme(NetworkStatusForm);
|
export default withTheme(NetworkStatusForm);
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import { withSnackbar, WithSnackbarProps } from 'notistack';
|
import { withSnackbar, WithSnackbarProps } from 'notistack';
|
||||||
|
|
||||||
import { createStyles, WithStyles, Theme, withStyles, Typography, LinearProgress } from '@material-ui/core';
|
import {
|
||||||
|
createStyles,
|
||||||
|
WithStyles,
|
||||||
|
Theme,
|
||||||
|
withStyles,
|
||||||
|
Typography,
|
||||||
|
LinearProgress
|
||||||
|
} from '@material-ui/core';
|
||||||
import PermScanWifiIcon from '@material-ui/icons/PermScanWifi';
|
import PermScanWifiIcon from '@material-ui/icons/PermScanWifi';
|
||||||
|
|
||||||
import { FormActions, FormButton, SectionContent } from '../components';
|
import { FormActions, FormButton, SectionContent } from '../components';
|
||||||
@@ -11,9 +18,9 @@ import { SCAN_NETWORKS_ENDPOINT, LIST_NETWORKS_ENDPOINT } from '../api';
|
|||||||
import WiFiNetworkSelector from './WiFiNetworkSelector';
|
import WiFiNetworkSelector from './WiFiNetworkSelector';
|
||||||
import { WiFiNetworkList, WiFiNetwork } from './types';
|
import { WiFiNetworkList, WiFiNetwork } from './types';
|
||||||
|
|
||||||
const NUM_POLLS = 10
|
const NUM_POLLS = 10;
|
||||||
const POLLING_FREQUENCY = 500
|
const POLLING_FREQUENCY = 500;
|
||||||
const RETRY_EXCEPTION_TYPE = "retry"
|
const RETRY_EXCEPTION_TYPE = 'retry';
|
||||||
|
|
||||||
interface WiFiNetworkScannerState {
|
interface WiFiNetworkScannerState {
|
||||||
scanningForNetworks: boolean;
|
scanningForNetworks: boolean;
|
||||||
@@ -21,28 +28,31 @@ interface WiFiNetworkScannerState {
|
|||||||
networkList?: WiFiNetworkList;
|
networkList?: WiFiNetworkList;
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = (theme: Theme) => createStyles({
|
const styles = (theme: Theme) =>
|
||||||
scanningSettings: {
|
createStyles({
|
||||||
margin: theme.spacing(0.5),
|
scanningSettings: {
|
||||||
},
|
margin: theme.spacing(0.5)
|
||||||
scanningSettingsDetails: {
|
},
|
||||||
margin: theme.spacing(4),
|
scanningSettingsDetails: {
|
||||||
textAlign: "center"
|
margin: theme.spacing(4),
|
||||||
},
|
textAlign: 'center'
|
||||||
scanningProgress: {
|
},
|
||||||
margin: theme.spacing(4),
|
scanningProgress: {
|
||||||
textAlign: "center"
|
margin: theme.spacing(4),
|
||||||
}
|
textAlign: 'center'
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
|
||||||
type WiFiNetworkScannerProps = WithSnackbarProps & WithStyles<typeof styles>;
|
type WiFiNetworkScannerProps = WithSnackbarProps & WithStyles<typeof styles>;
|
||||||
|
|
||||||
class WiFiNetworkScanner extends Component<WiFiNetworkScannerProps, WiFiNetworkScannerState> {
|
class WiFiNetworkScanner extends Component<
|
||||||
|
WiFiNetworkScannerProps,
|
||||||
pollCount: number = 0;
|
WiFiNetworkScannerState
|
||||||
|
> {
|
||||||
|
pollCount = 0;
|
||||||
|
|
||||||
state: WiFiNetworkScannerState = {
|
state: WiFiNetworkScannerState = {
|
||||||
scanningForNetworks: false,
|
scanningForNetworks: false
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@@ -54,23 +64,36 @@ class WiFiNetworkScanner extends Component<WiFiNetworkScannerProps, WiFiNetworkS
|
|||||||
if (!scanningForNetworks) {
|
if (!scanningForNetworks) {
|
||||||
this.scanNetworks();
|
this.scanNetworks();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
scanNetworks() {
|
scanNetworks() {
|
||||||
this.pollCount = 0;
|
this.pollCount = 0;
|
||||||
this.setState({ scanningForNetworks: true, networkList: undefined, errorMessage: undefined });
|
this.setState({
|
||||||
redirectingAuthorizedFetch(SCAN_NETWORKS_ENDPOINT).then(response => {
|
scanningForNetworks: true,
|
||||||
if (response.status === 202) {
|
networkList: undefined,
|
||||||
this.schedulePollTimeout();
|
errorMessage: undefined
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw Error("Scanning for networks returned unexpected response code: " + response.status);
|
|
||||||
}).catch(error => {
|
|
||||||
this.props.enqueueSnackbar("Problem scanning: " + error.message, {
|
|
||||||
variant: 'error',
|
|
||||||
});
|
|
||||||
this.setState({ scanningForNetworks: false, networkList: undefined, errorMessage: error.message });
|
|
||||||
});
|
});
|
||||||
|
redirectingAuthorizedFetch(SCAN_NETWORKS_ENDPOINT)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status === 202) {
|
||||||
|
this.schedulePollTimeout();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw Error(
|
||||||
|
'Scanning for networks returned unexpected response code: ' +
|
||||||
|
response.status
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.props.enqueueSnackbar('Problem scanning: ' + error.message, {
|
||||||
|
variant: 'error'
|
||||||
|
});
|
||||||
|
this.setState({
|
||||||
|
scanningForNetworks: false,
|
||||||
|
networkList: undefined,
|
||||||
|
errorMessage: error.message
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
schedulePollTimeout() {
|
schedulePollTimeout() {
|
||||||
@@ -80,21 +103,20 @@ class WiFiNetworkScanner extends Component<WiFiNetworkScannerProps, WiFiNetworkS
|
|||||||
retryError() {
|
retryError() {
|
||||||
return {
|
return {
|
||||||
name: RETRY_EXCEPTION_TYPE,
|
name: RETRY_EXCEPTION_TYPE,
|
||||||
message: "Network list not ready, will retry in " + POLLING_FREQUENCY + "ms."
|
message:
|
||||||
|
'Network list not ready, will retry in ' + POLLING_FREQUENCY + 'ms.'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
compareNetworks(network1: WiFiNetwork, network2: WiFiNetwork) {
|
compareNetworks(network1: WiFiNetwork, network2: WiFiNetwork) {
|
||||||
if (network1.rssi < network2.rssi)
|
if (network1.rssi < network2.rssi) return 1;
|
||||||
return 1;
|
if (network1.rssi > network2.rssi) return -1;
|
||||||
if (network1.rssi > network2.rssi)
|
|
||||||
return -1;
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
pollNetworkList = () => {
|
pollNetworkList = () => {
|
||||||
redirectingAuthorizedFetch(LIST_NETWORKS_ENDPOINT)
|
redirectingAuthorizedFetch(LIST_NETWORKS_ENDPOINT)
|
||||||
.then(response => {
|
.then((response) => {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
@@ -103,24 +125,34 @@ class WiFiNetworkScanner extends Component<WiFiNetworkScannerProps, WiFiNetworkS
|
|||||||
this.schedulePollTimeout();
|
this.schedulePollTimeout();
|
||||||
throw this.retryError();
|
throw this.retryError();
|
||||||
} else {
|
} else {
|
||||||
throw Error("Device did not return network list in timely manner.");
|
throw Error('Device did not return network list in timely manner.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw Error("Device returned unexpected response code: " + response.status);
|
throw Error(
|
||||||
|
'Device returned unexpected response code: ' + response.status
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.then(json => {
|
.then((json) => {
|
||||||
json.networks.sort(this.compareNetworks)
|
json.networks.sort(this.compareNetworks);
|
||||||
this.setState({ scanningForNetworks: false, networkList: json, errorMessage: undefined })
|
this.setState({
|
||||||
|
scanningForNetworks: false,
|
||||||
|
networkList: json,
|
||||||
|
errorMessage: undefined
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
if (error.name !== RETRY_EXCEPTION_TYPE) {
|
if (error.name !== RETRY_EXCEPTION_TYPE) {
|
||||||
this.props.enqueueSnackbar("Problem scanning: " + error.message, {
|
this.props.enqueueSnackbar('Problem scanning: ' + error.message, {
|
||||||
variant: 'error',
|
variant: 'error'
|
||||||
|
});
|
||||||
|
this.setState({
|
||||||
|
scanningForNetworks: false,
|
||||||
|
networkList: undefined,
|
||||||
|
errorMessage: error.message
|
||||||
});
|
});
|
||||||
this.setState({ scanningForNetworks: false, networkList: undefined, errorMessage: error.message });
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
renderNetworkScanner() {
|
renderNetworkScanner() {
|
||||||
const { classes } = this.props;
|
const { classes } = this.props;
|
||||||
@@ -144,9 +176,7 @@ class WiFiNetworkScanner extends Component<WiFiNetworkScannerProps, WiFiNetworkS
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return <WiFiNetworkSelector networkList={networkList} />;
|
||||||
<WiFiNetworkSelector networkList={networkList} />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@@ -155,14 +185,19 @@ class WiFiNetworkScanner extends Component<WiFiNetworkScannerProps, WiFiNetworkS
|
|||||||
<SectionContent title="Network Scanner">
|
<SectionContent title="Network Scanner">
|
||||||
{this.renderNetworkScanner()}
|
{this.renderNetworkScanner()}
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<FormButton startIcon={<PermScanWifiIcon />} variant="contained" color="secondary" onClick={this.requestNetworkScan} disabled={scanningForNetworks}>
|
<FormButton
|
||||||
|
startIcon={<PermScanWifiIcon />}
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
onClick={this.requestNetworkScan}
|
||||||
|
disabled={scanningForNetworks}
|
||||||
|
>
|
||||||
Scan again…
|
Scan again…
|
||||||
</FormButton>
|
</FormButton>
|
||||||
</FormActions>
|
</FormActions>
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withSnackbar(withStyles(styles)(WiFiNetworkScanner));
|
export default withSnackbar(withStyles(styles)(WiFiNetworkScanner));
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
import { Avatar, Badge } from '@material-ui/core';
|
import { Avatar, Badge } from '@material-ui/core';
|
||||||
import { List, ListItem, ListItemIcon, ListItemText, ListItemAvatar } from '@material-ui/core';
|
import {
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemIcon,
|
||||||
|
ListItemText,
|
||||||
|
ListItemAvatar
|
||||||
|
} from '@material-ui/core';
|
||||||
|
|
||||||
import WifiIcon from '@material-ui/icons/Wifi';
|
import WifiIcon from '@material-ui/icons/Wifi';
|
||||||
import LockIcon from '@material-ui/icons/Lock';
|
import LockIcon from '@material-ui/icons/Lock';
|
||||||
@@ -16,13 +22,16 @@ interface WiFiNetworkSelectorProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class WiFiNetworkSelector extends Component<WiFiNetworkSelectorProps> {
|
class WiFiNetworkSelector extends Component<WiFiNetworkSelectorProps> {
|
||||||
|
|
||||||
static contextType = NetworkConnectionContext;
|
static contextType = NetworkConnectionContext;
|
||||||
context!: React.ContextType<typeof NetworkConnectionContext>;
|
context!: React.ContextType<typeof NetworkConnectionContext>;
|
||||||
|
|
||||||
renderNetwork = (network: WiFiNetwork) => {
|
renderNetwork = (network: WiFiNetwork) => {
|
||||||
return (
|
return (
|
||||||
<ListItem key={network.bssid} button onClick={() => this.context.selectNetwork(network)}>
|
<ListItem
|
||||||
|
key={network.bssid}
|
||||||
|
button
|
||||||
|
onClick={() => this.context.selectNetwork(network)}
|
||||||
|
>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<Avatar>
|
<Avatar>
|
||||||
{isNetworkOpen(network) ? <LockOpenIcon /> : <LockIcon />}
|
{isNetworkOpen(network) ? <LockOpenIcon /> : <LockIcon />}
|
||||||
@@ -30,25 +39,27 @@ class WiFiNetworkSelector extends Component<WiFiNetworkSelectorProps> {
|
|||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={network.ssid}
|
primary={network.ssid}
|
||||||
secondary={"Security: " + networkSecurityMode(network) + ", Ch: " + network.channel}
|
secondary={
|
||||||
|
'Security: ' +
|
||||||
|
networkSecurityMode(network) +
|
||||||
|
', Ch: ' +
|
||||||
|
network.channel
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<Badge badgeContent={network.rssi + "db"}>
|
<Badge badgeContent={network.rssi + 'db'}>
|
||||||
<WifiIcon />
|
<WifiIcon />
|
||||||
</Badge>
|
</Badge>
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<List>
|
<List>{this.props.networkList.networks.map(this.renderNetwork)}</List>
|
||||||
{this.props.networkList.networks.map(this.renderNetwork)}
|
|
||||||
</List>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default WiFiNetworkSelector;
|
export default WiFiNetworkSelector;
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
import { WiFiNetwork, WiFiEncryptionType } from "./types";
|
import { WiFiNetwork, WiFiEncryptionType } from './types';
|
||||||
|
|
||||||
export const isNetworkOpen = ({ encryption_type }: WiFiNetwork) => encryption_type === WiFiEncryptionType.WIFI_AUTH_OPEN;
|
export const isNetworkOpen = ({ encryption_type }: WiFiNetwork) =>
|
||||||
|
encryption_type === WiFiEncryptionType.WIFI_AUTH_OPEN;
|
||||||
|
|
||||||
export const networkSecurityMode = ({ encryption_type }: WiFiNetwork) => {
|
export const networkSecurityMode = ({ encryption_type }: WiFiNetwork) => {
|
||||||
switch (encryption_type) {
|
switch (encryption_type) {
|
||||||
case WiFiEncryptionType.WIFI_AUTH_WEP:
|
case WiFiEncryptionType.WIFI_AUTH_WEP:
|
||||||
return "WEP";
|
return 'WEP';
|
||||||
case WiFiEncryptionType.WIFI_AUTH_WPA_PSK:
|
case WiFiEncryptionType.WIFI_AUTH_WPA_PSK:
|
||||||
return "WPA";
|
return 'WPA';
|
||||||
case WiFiEncryptionType.WIFI_AUTH_WPA2_PSK:
|
case WiFiEncryptionType.WIFI_AUTH_WPA2_PSK:
|
||||||
return "WPA2";
|
return 'WPA2';
|
||||||
case WiFiEncryptionType.WIFI_AUTH_WPA_WPA2_PSK:
|
case WiFiEncryptionType.WIFI_AUTH_WPA_WPA2_PSK:
|
||||||
return "WPA/WPA2";
|
return 'WPA/WPA2';
|
||||||
case WiFiEncryptionType.WIFI_AUTH_WPA2_ENTERPRISE:
|
case WiFiEncryptionType.WIFI_AUTH_WPA2_ENTERPRISE:
|
||||||
return "WPA2 Enterprise";
|
return 'WPA2 Enterprise';
|
||||||
case WiFiEncryptionType.WIFI_AUTH_OPEN:
|
case WiFiEncryptionType.WIFI_AUTH_OPEN:
|
||||||
return "None";
|
return 'None';
|
||||||
default:
|
default:
|
||||||
return "Unknown";
|
return 'Unknown';
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export enum WiFiEncryptionType {
|
|||||||
export interface NetworkStatus {
|
export interface NetworkStatus {
|
||||||
status: NetworkConnectionStatus;
|
status: NetworkConnectionStatus;
|
||||||
local_ip: string;
|
local_ip: string;
|
||||||
|
local_ipv6: string;
|
||||||
mac_address: string;
|
mac_address: string;
|
||||||
rssi: number;
|
rssi: number;
|
||||||
ssid: string;
|
ssid: string;
|
||||||
@@ -37,6 +38,10 @@ export interface NetworkSettings {
|
|||||||
password: string;
|
password: string;
|
||||||
hostname: string;
|
hostname: string;
|
||||||
static_ip_config: boolean;
|
static_ip_config: boolean;
|
||||||
|
enableIPv6: boolean;
|
||||||
|
bandwidth20: boolean;
|
||||||
|
nosleep: boolean;
|
||||||
|
tx_power: number;
|
||||||
local_ip?: string;
|
local_ip?: string;
|
||||||
gateway_ip?: string;
|
gateway_ip?: string;
|
||||||
subnet_mask?: string;
|
subnet_mask?: string;
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
|
|
||||||
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
import {
|
||||||
|
restController,
|
||||||
|
RestControllerProps,
|
||||||
|
RestFormLoader,
|
||||||
|
SectionContent
|
||||||
|
} from '../components';
|
||||||
import { NTP_SETTINGS_ENDPOINT } from '../api';
|
import { NTP_SETTINGS_ENDPOINT } from '../api';
|
||||||
|
|
||||||
import NTPSettingsForm from './NTPSettingsForm';
|
import NTPSettingsForm from './NTPSettingsForm';
|
||||||
@@ -9,7 +14,6 @@ import { NTPSettings } from './types';
|
|||||||
type NTPSettingsControllerProps = RestControllerProps<NTPSettings>;
|
type NTPSettingsControllerProps = RestControllerProps<NTPSettings>;
|
||||||
|
|
||||||
class NTPSettingsController extends Component<NTPSettingsControllerProps> {
|
class NTPSettingsController extends Component<NTPSettingsControllerProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.loadData();
|
this.props.loadData();
|
||||||
}
|
}
|
||||||
@@ -19,12 +23,11 @@ class NTPSettingsController extends Component<NTPSettingsControllerProps> {
|
|||||||
<SectionContent title="NTP Settings" titleGutter>
|
<SectionContent title="NTP Settings" titleGutter>
|
||||||
<RestFormLoader
|
<RestFormLoader
|
||||||
{...this.props}
|
{...this.props}
|
||||||
render={formProps => <NTPSettingsForm {...formProps} />}
|
render={(formProps) => <NTPSettingsForm {...formProps} />}
|
||||||
/>
|
/>
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default restController(NTP_SETTINGS_ENDPOINT, NTPSettingsController);
|
export default restController(NTP_SETTINGS_ENDPOINT, NTPSettingsController);
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { TextValidator, ValidatorForm, SelectValidator } from 'react-material-ui-form-validator';
|
import {
|
||||||
|
TextValidator,
|
||||||
|
ValidatorForm,
|
||||||
|
SelectValidator
|
||||||
|
} from 'react-material-ui-form-validator';
|
||||||
|
|
||||||
import { Checkbox, MenuItem } from '@material-ui/core';
|
import { Checkbox, MenuItem } from '@material-ui/core';
|
||||||
import SaveIcon from '@material-ui/icons/Save';
|
import SaveIcon from '@material-ui/icons/Save';
|
||||||
|
|
||||||
import { RestFormProps, FormActions, FormButton, BlockFormControlLabel } from '../components';
|
import {
|
||||||
|
RestFormProps,
|
||||||
|
FormActions,
|
||||||
|
FormButton,
|
||||||
|
BlockFormControlLabel
|
||||||
|
} from '../components';
|
||||||
import { isIP, isHostname, or } from '../validators';
|
import { isIP, isHostname, or } from '../validators';
|
||||||
|
|
||||||
import { TIME_ZONES, timeZoneSelectItems, selectedTimeZone } from './TZ';
|
import { TIME_ZONES, timeZoneSelectItems, selectedTimeZone } from './TZ';
|
||||||
@@ -13,7 +22,6 @@ import { NTPSettings } from './types';
|
|||||||
type NTPSettingsFormProps = RestFormProps<NTPSettings>;
|
type NTPSettingsFormProps = RestFormProps<NTPSettings>;
|
||||||
|
|
||||||
class NTPSettingsForm extends React.Component<NTPSettingsFormProps> {
|
class NTPSettingsForm extends React.Component<NTPSettingsFormProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
ValidatorForm.addValidationRule('isIPOrHostname', or(isIP, isHostname));
|
ValidatorForm.addValidationRule('isIPOrHostname', or(isIP, isHostname));
|
||||||
}
|
}
|
||||||
@@ -25,7 +33,7 @@ class NTPSettingsForm extends React.Component<NTPSettingsFormProps> {
|
|||||||
tz_label: event.target.value,
|
tz_label: event.target.value,
|
||||||
tz_format: TIME_ZONES[event.target.value]
|
tz_format: TIME_ZONES[event.target.value]
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { data, handleValueChange, saveData } = this.props;
|
const { data, handleValueChange, saveData } = this.props;
|
||||||
@@ -43,7 +51,10 @@ class NTPSettingsForm extends React.Component<NTPSettingsFormProps> {
|
|||||||
/>
|
/>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['required', 'isIPOrHostname']}
|
validators={['required', 'isIPOrHostname']}
|
||||||
errorMessages={['Server is required', "Not a valid IP address or hostname"]}
|
errorMessages={[
|
||||||
|
'Server is required',
|
||||||
|
'Not a valid IP address or hostname'
|
||||||
|
]}
|
||||||
name="server"
|
name="server"
|
||||||
label="Server"
|
label="Server"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -68,7 +79,12 @@ class NTPSettingsForm extends React.Component<NTPSettingsFormProps> {
|
|||||||
{timeZoneSelectItems()}
|
{timeZoneSelectItems()}
|
||||||
</SelectValidator>
|
</SelectValidator>
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
|
<FormButton
|
||||||
|
startIcon={<SaveIcon />}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
Save
|
Save
|
||||||
</FormButton>
|
</FormButton>
|
||||||
</FormActions>
|
</FormActions>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Theme } from "@material-ui/core";
|
import { Theme } from '@material-ui/core';
|
||||||
import { NTPStatus, NTPSyncStatus } from "./types";
|
import { NTPStatus, NTPSyncStatus } from './types';
|
||||||
|
|
||||||
export const isNtpActive = ({ status }: NTPStatus) => status === NTPSyncStatus.NTP_ACTIVE;
|
export const isNtpActive = ({ status }: NTPStatus) =>
|
||||||
|
status === NTPSyncStatus.NTP_ACTIVE;
|
||||||
|
|
||||||
export const ntpStatusHighlight = ({ status }: NTPStatus, theme: Theme) => {
|
export const ntpStatusHighlight = ({ status }: NTPStatus, theme: Theme) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
@@ -12,15 +13,15 @@ export const ntpStatusHighlight = ({ status }: NTPStatus, theme: Theme) => {
|
|||||||
default:
|
default:
|
||||||
return theme.palette.error.main;
|
return theme.palette.error.main;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export const ntpStatus = ({ status }: NTPStatus) => {
|
export const ntpStatus = ({ status }: NTPStatus) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case NTPSyncStatus.NTP_INACTIVE:
|
case NTPSyncStatus.NTP_INACTIVE:
|
||||||
return "Inactive";
|
return 'Inactive';
|
||||||
case NTPSyncStatus.NTP_ACTIVE:
|
case NTPSyncStatus.NTP_ACTIVE:
|
||||||
return "Active";
|
return 'Active';
|
||||||
default:
|
default:
|
||||||
return "Unknown";
|
return 'Unknown';
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
|
|
||||||
import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
import {
|
||||||
|
restController,
|
||||||
|
RestControllerProps,
|
||||||
|
RestFormLoader,
|
||||||
|
SectionContent
|
||||||
|
} from '../components';
|
||||||
import { NTP_STATUS_ENDPOINT } from '../api';
|
import { NTP_STATUS_ENDPOINT } from '../api';
|
||||||
|
|
||||||
import NTPStatusForm from './NTPStatusForm';
|
import NTPStatusForm from './NTPStatusForm';
|
||||||
@@ -9,7 +14,6 @@ import { NTPStatus } from './types';
|
|||||||
type NTPStatusControllerProps = RestControllerProps<NTPStatus>;
|
type NTPStatusControllerProps = RestControllerProps<NTPStatus>;
|
||||||
|
|
||||||
class NTPStatusController extends Component<NTPStatusControllerProps> {
|
class NTPStatusController extends Component<NTPStatusControllerProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.loadData();
|
this.props.loadData();
|
||||||
}
|
}
|
||||||
@@ -19,12 +23,11 @@ class NTPStatusController extends Component<NTPStatusControllerProps> {
|
|||||||
<SectionContent title="NTP Status">
|
<SectionContent title="NTP Status">
|
||||||
<RestFormLoader
|
<RestFormLoader
|
||||||
{...this.props}
|
{...this.props}
|
||||||
render={formProps => <NTPStatusForm {...formProps} />}
|
render={(formProps) => <NTPStatusForm {...formProps} />}
|
||||||
/>
|
/>
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default restController(NTP_STATUS_ENDPOINT, NTPStatusController);
|
export default restController(NTP_STATUS_ENDPOINT, NTPStatusController);
|
||||||
|
|||||||
@@ -1,8 +1,23 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
|
|
||||||
import { WithTheme, withTheme } from '@material-ui/core/styles';
|
import { WithTheme, withTheme } from '@material-ui/core/styles';
|
||||||
import { Avatar, Divider, List, ListItem, ListItemAvatar, ListItemText, Button } from '@material-ui/core';
|
import {
|
||||||
import { Dialog, DialogTitle, DialogContent, DialogActions, Box, TextField } from '@material-ui/core';
|
Avatar,
|
||||||
|
Divider,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemAvatar,
|
||||||
|
ListItemText,
|
||||||
|
Button
|
||||||
|
} from '@material-ui/core';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
|
Box,
|
||||||
|
TextField
|
||||||
|
} from '@material-ui/core';
|
||||||
|
|
||||||
import SwapVerticalCircleIcon from '@material-ui/icons/SwapVerticalCircle';
|
import SwapVerticalCircleIcon from '@material-ui/icons/SwapVerticalCircle';
|
||||||
import AccessTimeIcon from '@material-ui/icons/AccessTime';
|
import AccessTimeIcon from '@material-ui/icons/AccessTime';
|
||||||
@@ -13,12 +28,22 @@ import RefreshIcon from '@material-ui/icons/Refresh';
|
|||||||
|
|
||||||
import { RestFormProps, FormButton, HighlightAvatar } from '../components';
|
import { RestFormProps, FormButton, HighlightAvatar } from '../components';
|
||||||
import { isNtpActive, ntpStatusHighlight, ntpStatus } from './NTPStatus';
|
import { isNtpActive, ntpStatusHighlight, ntpStatus } from './NTPStatus';
|
||||||
import { formatDuration, formatDateTime, formatLocalDateTime } from './TimeFormat';
|
import {
|
||||||
|
formatDuration,
|
||||||
|
formatDateTime,
|
||||||
|
formatLocalDateTime
|
||||||
|
} from './TimeFormat';
|
||||||
import { NTPStatus, Time } from './types';
|
import { NTPStatus, Time } from './types';
|
||||||
import { redirectingAuthorizedFetch, withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
|
import {
|
||||||
|
redirectingAuthorizedFetch,
|
||||||
|
withAuthenticatedContext,
|
||||||
|
AuthenticatedContextProps
|
||||||
|
} from '../authentication';
|
||||||
import { TIME_ENDPOINT } from '../api';
|
import { TIME_ENDPOINT } from '../api';
|
||||||
|
|
||||||
type NTPStatusFormProps = RestFormProps<NTPStatus> & WithTheme & AuthenticatedContextProps;
|
type NTPStatusFormProps = RestFormProps<NTPStatus> &
|
||||||
|
WithTheme &
|
||||||
|
AuthenticatedContextProps;
|
||||||
|
|
||||||
interface NTPStatusFormState {
|
interface NTPStatusFormState {
|
||||||
settingTime: boolean;
|
settingTime: boolean;
|
||||||
@@ -27,7 +52,6 @@ interface NTPStatusFormState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
|
class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
|
||||||
|
|
||||||
constructor(props: NTPStatusFormProps) {
|
constructor(props: NTPStatusFormProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
@@ -41,20 +65,20 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
|
|||||||
this.setState({
|
this.setState({
|
||||||
localTime: event.target.value
|
localTime: event.target.value
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
openSetTime = () => {
|
openSetTime = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
localTime: formatLocalDateTime(new Date()),
|
localTime: formatLocalDateTime(new Date()),
|
||||||
settingTime: true
|
settingTime: true
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
closeSetTime = () => {
|
closeSetTime = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
settingTime: false
|
settingTime: false
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
createTime = (): Time => ({
|
createTime = (): Time => ({
|
||||||
local_time: formatLocalDateTime(new Date(this.state.localTime))
|
local_time: formatLocalDateTime(new Date(this.state.localTime))
|
||||||
@@ -62,37 +86,48 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
|
|||||||
|
|
||||||
configureTime = () => {
|
configureTime = () => {
|
||||||
this.setState({ processing: true });
|
this.setState({ processing: true });
|
||||||
redirectingAuthorizedFetch(TIME_ENDPOINT,
|
redirectingAuthorizedFetch(TIME_ENDPOINT, {
|
||||||
{
|
method: 'POST',
|
||||||
method: 'POST',
|
body: JSON.stringify(this.createTime()),
|
||||||
body: JSON.stringify(this.createTime()),
|
headers: {
|
||||||
headers: {
|
'Content-Type': 'application/json'
|
||||||
'Content-Type': 'application/json'
|
}
|
||||||
}
|
})
|
||||||
})
|
.then((response) => {
|
||||||
.then(response => {
|
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
this.props.enqueueSnackbar("Time set successfully", { variant: 'success' });
|
this.props.enqueueSnackbar('Time set successfully', {
|
||||||
this.setState({ processing: false, settingTime: false }, this.props.loadData);
|
variant: 'success'
|
||||||
|
});
|
||||||
|
this.setState(
|
||||||
|
{ processing: false, settingTime: false },
|
||||||
|
this.props.loadData
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
throw Error("Error setting time, status code: " + response.status);
|
throw Error('Error setting time, status code: ' + response.status);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
this.props.enqueueSnackbar(error.message || "Problem setting the time", { variant: 'error' });
|
this.props.enqueueSnackbar(
|
||||||
|
error.message || 'Problem setting the time',
|
||||||
|
{ variant: 'error' }
|
||||||
|
);
|
||||||
this.setState({ processing: false, settingTime: false });
|
this.setState({ processing: false, settingTime: false });
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
renderSetTimeDialog() {
|
renderSetTimeDialog() {
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
open={this.state.settingTime}
|
open={this.state.settingTime}
|
||||||
onClose={this.closeSetTime}
|
onClose={this.closeSetTime}
|
||||||
|
fullWidth
|
||||||
|
maxWidth="sm"
|
||||||
>
|
>
|
||||||
<DialogTitle>Set Time</DialogTitle>
|
<DialogTitle>Set Time</DialogTitle>
|
||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
<Box mb={2}>Enter local date and time below to set the device's time.</Box>
|
<Box mb={2}>
|
||||||
|
Enter local date and time below to set the device's time.
|
||||||
|
</Box>
|
||||||
<TextField
|
<TextField
|
||||||
label="Local Time"
|
label="Local Time"
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
@@ -102,24 +137,35 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{
|
InputLabelProps={{
|
||||||
shrink: true,
|
shrink: true
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button variant="contained" onClick={this.closeSetTime} color="secondary">
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={this.closeSetTime}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button startIcon={<AccessTimeIcon />} variant="contained" onClick={this.configureTime} disabled={this.state.processing} color="primary" autoFocus>
|
<Button
|
||||||
|
startIcon={<AccessTimeIcon />}
|
||||||
|
variant="contained"
|
||||||
|
onClick={this.configureTime}
|
||||||
|
disabled={this.state.processing}
|
||||||
|
color="primary"
|
||||||
|
autoFocus
|
||||||
|
>
|
||||||
Set Time
|
Set Time
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { data, theme } = this.props
|
const { data, theme } = this.props;
|
||||||
const me = this.props.authenticatedContext.me;
|
const me = this.props.authenticatedContext.me;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
@@ -152,7 +198,10 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
|
|||||||
<AccessTimeIcon />
|
<AccessTimeIcon />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary="Local Time" secondary={formatDateTime(data.local_time)} />
|
<ListItemText
|
||||||
|
primary="Local Time"
|
||||||
|
secondary={formatDateTime(data.local_time)}
|
||||||
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
<ListItem>
|
<ListItem>
|
||||||
@@ -161,7 +210,10 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
|
|||||||
<SwapVerticalCircleIcon />
|
<SwapVerticalCircleIcon />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary="UTC Time" secondary={formatDateTime(data.utc_time)} />
|
<ListItemText
|
||||||
|
primary="UTC Time"
|
||||||
|
secondary={formatDateTime(data.utc_time)}
|
||||||
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
<ListItem>
|
<ListItem>
|
||||||
@@ -170,19 +222,32 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
|
|||||||
<AvTimerIcon />
|
<AvTimerIcon />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary="Uptime" secondary={formatDuration(data.uptime)} />
|
<ListItemText
|
||||||
|
primary="Uptime"
|
||||||
|
secondary={formatDuration(data.uptime)}
|
||||||
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
</List>
|
</List>
|
||||||
<Box display="flex" flexWrap="wrap">
|
<Box display="flex" flexWrap="wrap">
|
||||||
<Box flexGrow={1} padding={1}>
|
<Box flexGrow={1} padding={1}>
|
||||||
<FormButton startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={this.props.loadData}>
|
<FormButton
|
||||||
|
startIcon={<RefreshIcon />}
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
onClick={this.props.loadData}
|
||||||
|
>
|
||||||
Refresh
|
Refresh
|
||||||
</FormButton>
|
</FormButton>
|
||||||
</Box>
|
</Box>
|
||||||
{me.admin && !isNtpActive(data) && (
|
{me.admin && !isNtpActive(data) && (
|
||||||
<Box flexWrap="none" padding={1} whiteSpace="nowrap">
|
<Box flexWrap="none" padding={1} whiteSpace="nowrap">
|
||||||
<Button onClick={this.openSetTime} variant="contained" color="primary" startIcon={<AccessTimeIcon />}>
|
<Button
|
||||||
|
onClick={this.openSetTime}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
startIcon={<AccessTimeIcon />}
|
||||||
|
>
|
||||||
Set Time
|
Set Time
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
|
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
|
||||||
|
|
||||||
import { Tabs, Tab } from '@material-ui/core';
|
import { Tabs, Tab } from '@material-ui/core';
|
||||||
|
|
||||||
import { withAuthenticatedContext, AuthenticatedContextProps, AuthenticatedRoute } from '../authentication';
|
import {
|
||||||
|
withAuthenticatedContext,
|
||||||
|
AuthenticatedContextProps,
|
||||||
|
AuthenticatedRoute
|
||||||
|
} from '../authentication';
|
||||||
import { MenuAppBar } from '../components';
|
import { MenuAppBar } from '../components';
|
||||||
|
|
||||||
import NTPStatusController from './NTPStatusController';
|
import NTPStatusController from './NTPStatusController';
|
||||||
@@ -12,8 +16,7 @@ import NTPSettingsController from './NTPSettingsController';
|
|||||||
type NetworkTimeProps = AuthenticatedContextProps & RouteComponentProps;
|
type NetworkTimeProps = AuthenticatedContextProps & RouteComponentProps;
|
||||||
|
|
||||||
class NetworkTime extends Component<NetworkTimeProps> {
|
class NetworkTime extends Component<NetworkTimeProps> {
|
||||||
|
handleTabChange = (path: string) => {
|
||||||
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
|
|
||||||
this.props.history.push(path);
|
this.props.history.push(path);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -21,19 +24,34 @@ class NetworkTime extends Component<NetworkTimeProps> {
|
|||||||
const { authenticatedContext } = this.props;
|
const { authenticatedContext } = this.props;
|
||||||
return (
|
return (
|
||||||
<MenuAppBar sectionTitle="Network Time">
|
<MenuAppBar sectionTitle="Network Time">
|
||||||
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
|
<Tabs
|
||||||
|
value={this.props.match.url}
|
||||||
|
onChange={(e, path) => this.handleTabChange(path)}
|
||||||
|
variant="fullWidth"
|
||||||
|
>
|
||||||
<Tab value="/ntp/status" label="NTP Status" />
|
<Tab value="/ntp/status" label="NTP Status" />
|
||||||
<Tab value="/ntp/settings" label="NTP Settings" disabled={!authenticatedContext.me.admin} />
|
<Tab
|
||||||
|
value="/ntp/settings"
|
||||||
|
label="NTP Settings"
|
||||||
|
disabled={!authenticatedContext.me.admin}
|
||||||
|
/>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<Switch>
|
<Switch>
|
||||||
<AuthenticatedRoute exact path="/ntp/status" component={NTPStatusController} />
|
<AuthenticatedRoute
|
||||||
<AuthenticatedRoute exact path="/ntp/settings" component={NTPSettingsController} />
|
exact
|
||||||
|
path="/ntp/status"
|
||||||
|
component={NTPStatusController}
|
||||||
|
/>
|
||||||
|
<AuthenticatedRoute
|
||||||
|
exact
|
||||||
|
path="/ntp/settings"
|
||||||
|
component={NTPSettingsController}
|
||||||
|
/>
|
||||||
<Redirect to="/ntp/status" />
|
<Redirect to="/ntp/status" />
|
||||||
</Switch>
|
</Switch>
|
||||||
</MenuAppBar>
|
</MenuAppBar>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withAuthenticatedContext(NetworkTime)
|
export default withAuthenticatedContext(NetworkTime);
|
||||||
|
|||||||
@@ -1,479 +1,480 @@
|
|||||||
import React from 'react';
|
|
||||||
import MenuItem from '@material-ui/core/MenuItem';
|
import MenuItem from '@material-ui/core/MenuItem';
|
||||||
|
|
||||||
type TimeZones = {
|
type TimeZones = {
|
||||||
[name: string]: string
|
[name: string]: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TIME_ZONES: TimeZones = {
|
export const TIME_ZONES: TimeZones = {
|
||||||
"Africa/Abidjan": "GMT0",
|
'Africa/Abidjan': 'GMT0',
|
||||||
"Africa/Accra": "GMT0",
|
'Africa/Accra': 'GMT0',
|
||||||
"Africa/Addis_Ababa": "EAT-3",
|
'Africa/Addis_Ababa': 'EAT-3',
|
||||||
"Africa/Algiers": "CET-1",
|
'Africa/Algiers': 'CET-1',
|
||||||
"Africa/Asmara": "EAT-3",
|
'Africa/Asmara': 'EAT-3',
|
||||||
"Africa/Bamako": "GMT0",
|
'Africa/Bamako': 'GMT0',
|
||||||
"Africa/Bangui": "WAT-1",
|
'Africa/Bangui': 'WAT-1',
|
||||||
"Africa/Banjul": "GMT0",
|
'Africa/Banjul': 'GMT0',
|
||||||
"Africa/Bissau": "GMT0",
|
'Africa/Bissau': 'GMT0',
|
||||||
"Africa/Blantyre": "CAT-2",
|
'Africa/Blantyre': 'CAT-2',
|
||||||
"Africa/Brazzaville": "WAT-1",
|
'Africa/Brazzaville': 'WAT-1',
|
||||||
"Africa/Bujumbura": "CAT-2",
|
'Africa/Bujumbura': 'CAT-2',
|
||||||
"Africa/Cairo": "EET-2",
|
'Africa/Cairo': 'EET-2',
|
||||||
"Africa/Casablanca": "UNK-1",
|
'Africa/Casablanca': 'UNK-1',
|
||||||
"Africa/Ceuta": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Africa/Ceuta': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Africa/Conakry": "GMT0",
|
'Africa/Conakry': 'GMT0',
|
||||||
"Africa/Dakar": "GMT0",
|
'Africa/Dakar': 'GMT0',
|
||||||
"Africa/Dar_es_Salaam": "EAT-3",
|
'Africa/Dar_es_Salaam': 'EAT-3',
|
||||||
"Africa/Djibouti": "EAT-3",
|
'Africa/Djibouti': 'EAT-3',
|
||||||
"Africa/Douala": "WAT-1",
|
'Africa/Douala': 'WAT-1',
|
||||||
"Africa/El_Aaiun": "UNK-1",
|
'Africa/El_Aaiun': 'UNK-1',
|
||||||
"Africa/Freetown": "GMT0",
|
'Africa/Freetown': 'GMT0',
|
||||||
"Africa/Gaborone": "CAT-2",
|
'Africa/Gaborone': 'CAT-2',
|
||||||
"Africa/Harare": "CAT-2",
|
'Africa/Harare': 'CAT-2',
|
||||||
"Africa/Johannesburg": "SAST-2",
|
'Africa/Johannesburg': 'SAST-2',
|
||||||
"Africa/Juba": "EAT-3",
|
'Africa/Juba': 'EAT-3',
|
||||||
"Africa/Kampala": "EAT-3",
|
'Africa/Kampala': 'EAT-3',
|
||||||
"Africa/Khartoum": "CAT-2",
|
'Africa/Khartoum': 'CAT-2',
|
||||||
"Africa/Kigali": "CAT-2",
|
'Africa/Kigali': 'CAT-2',
|
||||||
"Africa/Kinshasa": "WAT-1",
|
'Africa/Kinshasa': 'WAT-1',
|
||||||
"Africa/Lagos": "WAT-1",
|
'Africa/Lagos': 'WAT-1',
|
||||||
"Africa/Libreville": "WAT-1",
|
'Africa/Libreville': 'WAT-1',
|
||||||
"Africa/Lome": "GMT0",
|
'Africa/Lome': 'GMT0',
|
||||||
"Africa/Luanda": "WAT-1",
|
'Africa/Luanda': 'WAT-1',
|
||||||
"Africa/Lubumbashi": "CAT-2",
|
'Africa/Lubumbashi': 'CAT-2',
|
||||||
"Africa/Lusaka": "CAT-2",
|
'Africa/Lusaka': 'CAT-2',
|
||||||
"Africa/Malabo": "WAT-1",
|
'Africa/Malabo': 'WAT-1',
|
||||||
"Africa/Maputo": "CAT-2",
|
'Africa/Maputo': 'CAT-2',
|
||||||
"Africa/Maseru": "SAST-2",
|
'Africa/Maseru': 'SAST-2',
|
||||||
"Africa/Mbabane": "SAST-2",
|
'Africa/Mbabane': 'SAST-2',
|
||||||
"Africa/Mogadishu": "EAT-3",
|
'Africa/Mogadishu': 'EAT-3',
|
||||||
"Africa/Monrovia": "GMT0",
|
'Africa/Monrovia': 'GMT0',
|
||||||
"Africa/Nairobi": "EAT-3",
|
'Africa/Nairobi': 'EAT-3',
|
||||||
"Africa/Ndjamena": "WAT-1",
|
'Africa/Ndjamena': 'WAT-1',
|
||||||
"Africa/Niamey": "WAT-1",
|
'Africa/Niamey': 'WAT-1',
|
||||||
"Africa/Nouakchott": "GMT0",
|
'Africa/Nouakchott': 'GMT0',
|
||||||
"Africa/Ouagadougou": "GMT0",
|
'Africa/Ouagadougou': 'GMT0',
|
||||||
"Africa/Porto-Novo": "WAT-1",
|
'Africa/Porto-Novo': 'WAT-1',
|
||||||
"Africa/Sao_Tome": "GMT0",
|
'Africa/Sao_Tome': 'GMT0',
|
||||||
"Africa/Tripoli": "EET-2",
|
'Africa/Tripoli': 'EET-2',
|
||||||
"Africa/Tunis": "CET-1",
|
'Africa/Tunis': 'CET-1',
|
||||||
"Africa/Windhoek": "CAT-2",
|
'Africa/Windhoek': 'CAT-2',
|
||||||
"America/Adak": "HST10HDT,M3.2.0,M11.1.0",
|
'America/Adak': 'HST10HDT,M3.2.0,M11.1.0',
|
||||||
"America/Anchorage": "AKST9AKDT,M3.2.0,M11.1.0",
|
'America/Anchorage': 'AKST9AKDT,M3.2.0,M11.1.0',
|
||||||
"America/Anguilla": "AST4",
|
'America/Anguilla': 'AST4',
|
||||||
"America/Antigua": "AST4",
|
'America/Antigua': 'AST4',
|
||||||
"America/Araguaina": "UNK3",
|
'America/Araguaina': 'UNK3',
|
||||||
"America/Argentina/Buenos_Aires": "UNK3",
|
'America/Argentina/Buenos_Aires': 'UNK3',
|
||||||
"America/Argentina/Catamarca": "UNK3",
|
'America/Argentina/Catamarca': 'UNK3',
|
||||||
"America/Argentina/Cordoba": "UNK3",
|
'America/Argentina/Cordoba': 'UNK3',
|
||||||
"America/Argentina/Jujuy": "UNK3",
|
'America/Argentina/Jujuy': 'UNK3',
|
||||||
"America/Argentina/La_Rioja": "UNK3",
|
'America/Argentina/La_Rioja': 'UNK3',
|
||||||
"America/Argentina/Mendoza": "UNK3",
|
'America/Argentina/Mendoza': 'UNK3',
|
||||||
"America/Argentina/Rio_Gallegos": "UNK3",
|
'America/Argentina/Rio_Gallegos': 'UNK3',
|
||||||
"America/Argentina/Salta": "UNK3",
|
'America/Argentina/Salta': 'UNK3',
|
||||||
"America/Argentina/San_Juan": "UNK3",
|
'America/Argentina/San_Juan': 'UNK3',
|
||||||
"America/Argentina/San_Luis": "UNK3",
|
'America/Argentina/San_Luis': 'UNK3',
|
||||||
"America/Argentina/Tucuman": "UNK3",
|
'America/Argentina/Tucuman': 'UNK3',
|
||||||
"America/Argentina/Ushuaia": "UNK3",
|
'America/Argentina/Ushuaia': 'UNK3',
|
||||||
"America/Aruba": "AST4",
|
'America/Aruba': 'AST4',
|
||||||
"America/Asuncion": "UNK4UNK,M10.1.0/0,M3.4.0/0",
|
'America/Asuncion': 'UNK4UNK,M10.1.0/0,M3.4.0/0',
|
||||||
"America/Atikokan": "EST5",
|
'America/Atikokan': 'EST5',
|
||||||
"America/Bahia": "UNK3",
|
'America/Bahia': 'UNK3',
|
||||||
"America/Bahia_Banderas": "CST6CDT,M4.1.0,M10.5.0",
|
'America/Bahia_Banderas': 'CST6CDT,M4.1.0,M10.5.0',
|
||||||
"America/Barbados": "AST4",
|
'America/Barbados': 'AST4',
|
||||||
"America/Belem": "UNK3",
|
'America/Belem': 'UNK3',
|
||||||
"America/Belize": "CST6",
|
'America/Belize': 'CST6',
|
||||||
"America/Blanc-Sablon": "AST4",
|
'America/Blanc-Sablon': 'AST4',
|
||||||
"America/Boa_Vista": "UNK4",
|
'America/Boa_Vista': 'UNK4',
|
||||||
"America/Bogota": "UNK5",
|
'America/Bogota': 'UNK5',
|
||||||
"America/Boise": "MST7MDT,M3.2.0,M11.1.0",
|
'America/Boise': 'MST7MDT,M3.2.0,M11.1.0',
|
||||||
"America/Cambridge_Bay": "MST7MDT,M3.2.0,M11.1.0",
|
'America/Cambridge_Bay': 'MST7MDT,M3.2.0,M11.1.0',
|
||||||
"America/Campo_Grande": "UNK4",
|
'America/Campo_Grande': 'UNK4',
|
||||||
"America/Cancun": "EST5",
|
'America/Cancun': 'EST5',
|
||||||
"America/Caracas": "UNK4",
|
'America/Caracas': 'UNK4',
|
||||||
"America/Cayenne": "UNK3",
|
'America/Cayenne': 'UNK3',
|
||||||
"America/Cayman": "EST5",
|
'America/Cayman': 'EST5',
|
||||||
"America/Chicago": "CST6CDT,M3.2.0,M11.1.0",
|
'America/Chicago': 'CST6CDT,M3.2.0,M11.1.0',
|
||||||
"America/Chihuahua": "MST7MDT,M4.1.0,M10.5.0",
|
'America/Chihuahua': 'MST7MDT,M4.1.0,M10.5.0',
|
||||||
"America/Costa_Rica": "CST6",
|
'America/Costa_Rica': 'CST6',
|
||||||
"America/Creston": "MST7",
|
'America/Creston': 'MST7',
|
||||||
"America/Cuiaba": "UNK4",
|
'America/Cuiaba': 'UNK4',
|
||||||
"America/Curacao": "AST4",
|
'America/Curacao': 'AST4',
|
||||||
"America/Danmarkshavn": "GMT0",
|
'America/Danmarkshavn': 'GMT0',
|
||||||
"America/Dawson": "MST7",
|
'America/Dawson': 'MST7',
|
||||||
"America/Dawson_Creek": "MST7",
|
'America/Dawson_Creek': 'MST7',
|
||||||
"America/Denver": "MST7MDT,M3.2.0,M11.1.0",
|
'America/Denver': 'MST7MDT,M3.2.0,M11.1.0',
|
||||||
"America/Detroit": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Detroit': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Dominica": "AST4",
|
'America/Dominica': 'AST4',
|
||||||
"America/Edmonton": "MST7MDT,M3.2.0,M11.1.0",
|
'America/Edmonton': 'MST7MDT,M3.2.0,M11.1.0',
|
||||||
"America/Eirunepe": "UNK5",
|
'America/Eirunepe': 'UNK5',
|
||||||
"America/El_Salvador": "CST6",
|
'America/El_Salvador': 'CST6',
|
||||||
"America/Fort_Nelson": "MST7",
|
'America/Fort_Nelson': 'MST7',
|
||||||
"America/Fortaleza": "UNK3",
|
'America/Fortaleza': 'UNK3',
|
||||||
"America/Glace_Bay": "AST4ADT,M3.2.0,M11.1.0",
|
'America/Glace_Bay': 'AST4ADT,M3.2.0,M11.1.0',
|
||||||
"America/Godthab": "UNK3UNK,M3.5.0/-2,M10.5.0/-1",
|
'America/Godthab': 'UNK3UNK,M3.5.0/-2,M10.5.0/-1',
|
||||||
"America/Goose_Bay": "AST4ADT,M3.2.0,M11.1.0",
|
'America/Goose_Bay': 'AST4ADT,M3.2.0,M11.1.0',
|
||||||
"America/Grand_Turk": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Grand_Turk': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Grenada": "AST4",
|
'America/Grenada': 'AST4',
|
||||||
"America/Guadeloupe": "AST4",
|
'America/Guadeloupe': 'AST4',
|
||||||
"America/Guatemala": "CST6",
|
'America/Guatemala': 'CST6',
|
||||||
"America/Guayaquil": "UNK5",
|
'America/Guayaquil': 'UNK5',
|
||||||
"America/Guyana": "UNK4",
|
'America/Guyana': 'UNK4',
|
||||||
"America/Halifax": "AST4ADT,M3.2.0,M11.1.0",
|
'America/Halifax': 'AST4ADT,M3.2.0,M11.1.0',
|
||||||
"America/Havana": "CST5CDT,M3.2.0/0,M11.1.0/1",
|
'America/Havana': 'CST5CDT,M3.2.0/0,M11.1.0/1',
|
||||||
"America/Hermosillo": "MST7",
|
'America/Hermosillo': 'MST7',
|
||||||
"America/Indiana/Indianapolis": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Indiana/Indianapolis': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Indiana/Knox": "CST6CDT,M3.2.0,M11.1.0",
|
'America/Indiana/Knox': 'CST6CDT,M3.2.0,M11.1.0',
|
||||||
"America/Indiana/Marengo": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Indiana/Marengo': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Indiana/Petersburg": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Indiana/Petersburg': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Indiana/Tell_City": "CST6CDT,M3.2.0,M11.1.0",
|
'America/Indiana/Tell_City': 'CST6CDT,M3.2.0,M11.1.0',
|
||||||
"America/Indiana/Vevay": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Indiana/Vevay': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Indiana/Vincennes": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Indiana/Vincennes': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Indiana/Winamac": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Indiana/Winamac': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Inuvik": "MST7MDT,M3.2.0,M11.1.0",
|
'America/Inuvik': 'MST7MDT,M3.2.0,M11.1.0',
|
||||||
"America/Iqaluit": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Iqaluit': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Jamaica": "EST5",
|
'America/Jamaica': 'EST5',
|
||||||
"America/Juneau": "AKST9AKDT,M3.2.0,M11.1.0",
|
'America/Juneau': 'AKST9AKDT,M3.2.0,M11.1.0',
|
||||||
"America/Kentucky/Louisville": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Kentucky/Louisville': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Kentucky/Monticello": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Kentucky/Monticello': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Kralendijk": "AST4",
|
'America/Kralendijk': 'AST4',
|
||||||
"America/La_Paz": "UNK4",
|
'America/La_Paz': 'UNK4',
|
||||||
"America/Lima": "UNK5",
|
'America/Lima': 'UNK5',
|
||||||
"America/Los_Angeles": "PST8PDT,M3.2.0,M11.1.0",
|
'America/Los_Angeles': 'PST8PDT,M3.2.0,M11.1.0',
|
||||||
"America/Lower_Princes": "AST4",
|
'America/Lower_Princes': 'AST4',
|
||||||
"America/Maceio": "UNK3",
|
'America/Maceio': 'UNK3',
|
||||||
"America/Managua": "CST6",
|
'America/Managua': 'CST6',
|
||||||
"America/Manaus": "UNK4",
|
'America/Manaus': 'UNK4',
|
||||||
"America/Marigot": "AST4",
|
'America/Marigot': 'AST4',
|
||||||
"America/Martinique": "AST4",
|
'America/Martinique': 'AST4',
|
||||||
"America/Matamoros": "CST6CDT,M3.2.0,M11.1.0",
|
'America/Matamoros': 'CST6CDT,M3.2.0,M11.1.0',
|
||||||
"America/Mazatlan": "MST7MDT,M4.1.0,M10.5.0",
|
'America/Mazatlan': 'MST7MDT,M4.1.0,M10.5.0',
|
||||||
"America/Menominee": "CST6CDT,M3.2.0,M11.1.0",
|
'America/Menominee': 'CST6CDT,M3.2.0,M11.1.0',
|
||||||
"America/Merida": "CST6CDT,M4.1.0,M10.5.0",
|
'America/Merida': 'CST6CDT,M4.1.0,M10.5.0',
|
||||||
"America/Metlakatla": "AKST9AKDT,M3.2.0,M11.1.0",
|
'America/Metlakatla': 'AKST9AKDT,M3.2.0,M11.1.0',
|
||||||
"America/Mexico_City": "CST6CDT,M4.1.0,M10.5.0",
|
'America/Mexico_City': 'CST6CDT,M4.1.0,M10.5.0',
|
||||||
"America/Miquelon": "UNK3UNK,M3.2.0,M11.1.0",
|
'America/Miquelon': 'UNK3UNK,M3.2.0,M11.1.0',
|
||||||
"America/Moncton": "AST4ADT,M3.2.0,M11.1.0",
|
'America/Moncton': 'AST4ADT,M3.2.0,M11.1.0',
|
||||||
"America/Monterrey": "CST6CDT,M4.1.0,M10.5.0",
|
'America/Monterrey': 'CST6CDT,M4.1.0,M10.5.0',
|
||||||
"America/Montevideo": "UNK3",
|
'America/Montevideo': 'UNK3',
|
||||||
"America/Montreal": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Montreal': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Montserrat": "AST4",
|
'America/Montserrat': 'AST4',
|
||||||
"America/Nassau": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Nassau': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/New_York": "EST5EDT,M3.2.0,M11.1.0",
|
'America/New_York': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Nipigon": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Nipigon': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Nome": "AKST9AKDT,M3.2.0,M11.1.0",
|
'America/Nome': 'AKST9AKDT,M3.2.0,M11.1.0',
|
||||||
"America/Noronha": "UNK2",
|
'America/Noronha': 'UNK2',
|
||||||
"America/North_Dakota/Beulah": "CST6CDT,M3.2.0,M11.1.0",
|
'America/North_Dakota/Beulah': 'CST6CDT,M3.2.0,M11.1.0',
|
||||||
"America/North_Dakota/Center": "CST6CDT,M3.2.0,M11.1.0",
|
'America/North_Dakota/Center': 'CST6CDT,M3.2.0,M11.1.0',
|
||||||
"America/North_Dakota/New_Salem": "CST6CDT,M3.2.0,M11.1.0",
|
'America/North_Dakota/New_Salem': 'CST6CDT,M3.2.0,M11.1.0',
|
||||||
"America/Ojinaga": "MST7MDT,M3.2.0,M11.1.0",
|
'America/Ojinaga': 'MST7MDT,M3.2.0,M11.1.0',
|
||||||
"America/Panama": "EST5",
|
'America/Panama': 'EST5',
|
||||||
"America/Pangnirtung": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Pangnirtung': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Paramaribo": "UNK3",
|
'America/Paramaribo': 'UNK3',
|
||||||
"America/Phoenix": "MST7",
|
'America/Phoenix': 'MST7',
|
||||||
"America/Port-au-Prince": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Port-au-Prince': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Port_of_Spain": "AST4",
|
'America/Port_of_Spain': 'AST4',
|
||||||
"America/Porto_Velho": "UNK4",
|
'America/Porto_Velho': 'UNK4',
|
||||||
"America/Puerto_Rico": "AST4",
|
'America/Puerto_Rico': 'AST4',
|
||||||
"America/Punta_Arenas": "UNK3",
|
'America/Punta_Arenas': 'UNK3',
|
||||||
"America/Rainy_River": "CST6CDT,M3.2.0,M11.1.0",
|
'America/Rainy_River': 'CST6CDT,M3.2.0,M11.1.0',
|
||||||
"America/Rankin_Inlet": "CST6CDT,M3.2.0,M11.1.0",
|
'America/Rankin_Inlet': 'CST6CDT,M3.2.0,M11.1.0',
|
||||||
"America/Recife": "UNK3",
|
'America/Recife': 'UNK3',
|
||||||
"America/Regina": "CST6",
|
'America/Regina': 'CST6',
|
||||||
"America/Resolute": "CST6CDT,M3.2.0,M11.1.0",
|
'America/Resolute': 'CST6CDT,M3.2.0,M11.1.0',
|
||||||
"America/Rio_Branco": "UNK5",
|
'America/Rio_Branco': 'UNK5',
|
||||||
"America/Santarem": "UNK3",
|
'America/Santarem': 'UNK3',
|
||||||
"America/Santiago": "UNK4UNK,M9.1.6/24,M4.1.6/24",
|
'America/Santiago': 'UNK4UNK,M9.1.6/24,M4.1.6/24',
|
||||||
"America/Santo_Domingo": "AST4",
|
'America/Santo_Domingo': 'AST4',
|
||||||
"America/Sao_Paulo": "UNK3",
|
'America/Sao_Paulo': 'UNK3',
|
||||||
"America/Scoresbysund": "UNK1UNK,M3.5.0/0,M10.5.0/1",
|
'America/Scoresbysund': 'UNK1UNK,M3.5.0/0,M10.5.0/1',
|
||||||
"America/Sitka": "AKST9AKDT,M3.2.0,M11.1.0",
|
'America/Sitka': 'AKST9AKDT,M3.2.0,M11.1.0',
|
||||||
"America/St_Barthelemy": "AST4",
|
'America/St_Barthelemy': 'AST4',
|
||||||
"America/St_Johns": "NST3:30NDT,M3.2.0,M11.1.0",
|
'America/St_Johns': 'NST3:30NDT,M3.2.0,M11.1.0',
|
||||||
"America/St_Kitts": "AST4",
|
'America/St_Kitts': 'AST4',
|
||||||
"America/St_Lucia": "AST4",
|
'America/St_Lucia': 'AST4',
|
||||||
"America/St_Thomas": "AST4",
|
'America/St_Thomas': 'AST4',
|
||||||
"America/St_Vincent": "AST4",
|
'America/St_Vincent': 'AST4',
|
||||||
"America/Swift_Current": "CST6",
|
'America/Swift_Current': 'CST6',
|
||||||
"America/Tegucigalpa": "CST6",
|
'America/Tegucigalpa': 'CST6',
|
||||||
"America/Thule": "AST4ADT,M3.2.0,M11.1.0",
|
'America/Thule': 'AST4ADT,M3.2.0,M11.1.0',
|
||||||
"America/Thunder_Bay": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Thunder_Bay': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Tijuana": "PST8PDT,M3.2.0,M11.1.0",
|
'America/Tijuana': 'PST8PDT,M3.2.0,M11.1.0',
|
||||||
"America/Toronto": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Toronto': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Tortola": "AST4",
|
'America/Tortola': 'AST4',
|
||||||
"America/Vancouver": "PST8PDT,M3.2.0,M11.1.0",
|
'America/Vancouver': 'PST8PDT,M3.2.0,M11.1.0',
|
||||||
"America/Whitehorse": "MST7",
|
'America/Whitehorse': 'MST7',
|
||||||
"America/Winnipeg": "CST6CDT,M3.2.0,M11.1.0",
|
'America/Winnipeg': 'CST6CDT,M3.2.0,M11.1.0',
|
||||||
"America/Yakutat": "AKST9AKDT,M3.2.0,M11.1.0",
|
'America/Yakutat': 'AKST9AKDT,M3.2.0,M11.1.0',
|
||||||
"America/Yellowknife": "MST7MDT,M3.2.0,M11.1.0",
|
'America/Yellowknife': 'MST7MDT,M3.2.0,M11.1.0',
|
||||||
"Antarctica/Casey": "UNK-8",
|
'Antarctica/Casey': 'UNK-8',
|
||||||
"Antarctica/Davis": "UNK-7",
|
'Antarctica/Davis': 'UNK-7',
|
||||||
"Antarctica/DumontDUrville": "UNK-10",
|
'Antarctica/DumontDUrville': 'UNK-10',
|
||||||
"Antarctica/Macquarie": "UNK-11",
|
'Antarctica/Macquarie': 'UNK-11',
|
||||||
"Antarctica/Mawson": "UNK-5",
|
'Antarctica/Mawson': 'UNK-5',
|
||||||
"Antarctica/McMurdo": "NZST-12NZDT,M9.5.0,M4.1.0/3",
|
'Antarctica/McMurdo': 'NZST-12NZDT,M9.5.0,M4.1.0/3',
|
||||||
"Antarctica/Palmer": "UNK3",
|
'Antarctica/Palmer': 'UNK3',
|
||||||
"Antarctica/Rothera": "UNK3",
|
'Antarctica/Rothera': 'UNK3',
|
||||||
"Antarctica/Syowa": "UNK-3",
|
'Antarctica/Syowa': 'UNK-3',
|
||||||
"Antarctica/Troll": "UNK0UNK-2,M3.5.0/1,M10.5.0/3",
|
'Antarctica/Troll': 'UNK0UNK-2,M3.5.0/1,M10.5.0/3',
|
||||||
"Antarctica/Vostok": "UNK-6",
|
'Antarctica/Vostok': 'UNK-6',
|
||||||
"Arctic/Longyearbyen": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Arctic/Longyearbyen': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Asia/Aden": "UNK-3",
|
'Asia/Aden': 'UNK-3',
|
||||||
"Asia/Almaty": "UNK-6",
|
'Asia/Almaty': 'UNK-6',
|
||||||
"Asia/Amman": "EET-2EEST,M3.5.4/24,M10.5.5/1",
|
'Asia/Amman': 'EET-2EEST,M3.5.4/24,M10.5.5/1',
|
||||||
"Asia/Anadyr": "UNK-12",
|
'Asia/Anadyr': 'UNK-12',
|
||||||
"Asia/Aqtau": "UNK-5",
|
'Asia/Aqtau': 'UNK-5',
|
||||||
"Asia/Aqtobe": "UNK-5",
|
'Asia/Aqtobe': 'UNK-5',
|
||||||
"Asia/Ashgabat": "UNK-5",
|
'Asia/Ashgabat': 'UNK-5',
|
||||||
"Asia/Atyrau": "UNK-5",
|
'Asia/Atyrau': 'UNK-5',
|
||||||
"Asia/Baghdad": "UNK-3",
|
'Asia/Baghdad': 'UNK-3',
|
||||||
"Asia/Bahrain": "UNK-3",
|
'Asia/Bahrain': 'UNK-3',
|
||||||
"Asia/Baku": "UNK-4",
|
'Asia/Baku': 'UNK-4',
|
||||||
"Asia/Bangkok": "UNK-7",
|
'Asia/Bangkok': 'UNK-7',
|
||||||
"Asia/Barnaul": "UNK-7",
|
'Asia/Barnaul': 'UNK-7',
|
||||||
"Asia/Beirut": "EET-2EEST,M3.5.0/0,M10.5.0/0",
|
'Asia/Beirut': 'EET-2EEST,M3.5.0/0,M10.5.0/0',
|
||||||
"Asia/Bishkek": "UNK-6",
|
'Asia/Bishkek': 'UNK-6',
|
||||||
"Asia/Brunei": "UNK-8",
|
'Asia/Brunei': 'UNK-8',
|
||||||
"Asia/Chita": "UNK-9",
|
'Asia/Chita': 'UNK-9',
|
||||||
"Asia/Choibalsan": "UNK-8",
|
'Asia/Choibalsan': 'UNK-8',
|
||||||
"Asia/Colombo": "UNK-5:30",
|
'Asia/Colombo': 'UNK-5:30',
|
||||||
"Asia/Damascus": "EET-2EEST,M3.5.5/0,M10.5.5/0",
|
'Asia/Damascus': 'EET-2EEST,M3.5.5/0,M10.5.5/0',
|
||||||
"Asia/Dhaka": "UNK-6",
|
'Asia/Dhaka': 'UNK-6',
|
||||||
"Asia/Dili": "UNK-9",
|
'Asia/Dili': 'UNK-9',
|
||||||
"Asia/Dubai": "UNK-4",
|
'Asia/Dubai': 'UNK-4',
|
||||||
"Asia/Dushanbe": "UNK-5",
|
'Asia/Dushanbe': 'UNK-5',
|
||||||
"Asia/Famagusta": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
'Asia/Famagusta': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||||
"Asia/Gaza": "EET-2EEST,M3.5.5/0,M10.5.6/1",
|
'Asia/Gaza': 'EET-2EEST,M3.5.5/0,M10.5.6/1',
|
||||||
"Asia/Hebron": "EET-2EEST,M3.5.5/0,M10.5.6/1",
|
'Asia/Hebron': 'EET-2EEST,M3.5.5/0,M10.5.6/1',
|
||||||
"Asia/Ho_Chi_Minh": "UNK-7",
|
'Asia/Ho_Chi_Minh': 'UNK-7',
|
||||||
"Asia/Hong_Kong": "HKT-8",
|
'Asia/Hong_Kong': 'HKT-8',
|
||||||
"Asia/Hovd": "UNK-7",
|
'Asia/Hovd': 'UNK-7',
|
||||||
"Asia/Irkutsk": "UNK-8",
|
'Asia/Irkutsk': 'UNK-8',
|
||||||
"Asia/Jakarta": "WIB-7",
|
'Asia/Jakarta': 'WIB-7',
|
||||||
"Asia/Jayapura": "WIT-9",
|
'Asia/Jayapura': 'WIT-9',
|
||||||
"Asia/Jerusalem": "IST-2IDT,M3.4.4/26,M10.5.0",
|
'Asia/Jerusalem': 'IST-2IDT,M3.4.4/26,M10.5.0',
|
||||||
"Asia/Kabul": "UNK-4:30",
|
'Asia/Kabul': 'UNK-4:30',
|
||||||
"Asia/Kamchatka": "UNK-12",
|
'Asia/Kamchatka': 'UNK-12',
|
||||||
"Asia/Karachi": "PKT-5",
|
'Asia/Karachi': 'PKT-5',
|
||||||
"Asia/Kathmandu": "UNK-5:45",
|
'Asia/Kathmandu': 'UNK-5:45',
|
||||||
"Asia/Khandyga": "UNK-9",
|
'Asia/Khandyga': 'UNK-9',
|
||||||
"Asia/Kolkata": "IST-5:30",
|
'Asia/Kolkata': 'IST-5:30',
|
||||||
"Asia/Krasnoyarsk": "UNK-7",
|
'Asia/Krasnoyarsk': 'UNK-7',
|
||||||
"Asia/Kuala_Lumpur": "UNK-8",
|
'Asia/Kuala_Lumpur': 'UNK-8',
|
||||||
"Asia/Kuching": "UNK-8",
|
'Asia/Kuching': 'UNK-8',
|
||||||
"Asia/Kuwait": "UNK-3",
|
'Asia/Kuwait': 'UNK-3',
|
||||||
"Asia/Macau": "CST-8",
|
'Asia/Macau': 'CST-8',
|
||||||
"Asia/Magadan": "UNK-11",
|
'Asia/Magadan': 'UNK-11',
|
||||||
"Asia/Makassar": "WITA-8",
|
'Asia/Makassar': 'WITA-8',
|
||||||
"Asia/Manila": "PST-8",
|
'Asia/Manila': 'PST-8',
|
||||||
"Asia/Muscat": "UNK-4",
|
'Asia/Muscat': 'UNK-4',
|
||||||
"Asia/Nicosia": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
'Asia/Nicosia': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||||
"Asia/Novokuznetsk": "UNK-7",
|
'Asia/Novokuznetsk': 'UNK-7',
|
||||||
"Asia/Novosibirsk": "UNK-7",
|
'Asia/Novosibirsk': 'UNK-7',
|
||||||
"Asia/Omsk": "UNK-6",
|
'Asia/Omsk': 'UNK-6',
|
||||||
"Asia/Oral": "UNK-5",
|
'Asia/Oral': 'UNK-5',
|
||||||
"Asia/Phnom_Penh": "UNK-7",
|
'Asia/Phnom_Penh': 'UNK-7',
|
||||||
"Asia/Pontianak": "WIB-7",
|
'Asia/Pontianak': 'WIB-7',
|
||||||
"Asia/Pyongyang": "KST-9",
|
'Asia/Pyongyang': 'KST-9',
|
||||||
"Asia/Qatar": "UNK-3",
|
'Asia/Qatar': 'UNK-3',
|
||||||
"Asia/Qyzylorda": "UNK-5",
|
'Asia/Qyzylorda': 'UNK-5',
|
||||||
"Asia/Riyadh": "UNK-3",
|
'Asia/Riyadh': 'UNK-3',
|
||||||
"Asia/Sakhalin": "UNK-11",
|
'Asia/Sakhalin': 'UNK-11',
|
||||||
"Asia/Samarkand": "UNK-5",
|
'Asia/Samarkand': 'UNK-5',
|
||||||
"Asia/Seoul": "KST-9",
|
'Asia/Seoul': 'KST-9',
|
||||||
"Asia/Shanghai": "CST-8",
|
'Asia/Shanghai': 'CST-8',
|
||||||
"Asia/Singapore": "UNK-8",
|
'Asia/Singapore': 'UNK-8',
|
||||||
"Asia/Srednekolymsk": "UNK-11",
|
'Asia/Srednekolymsk': 'UNK-11',
|
||||||
"Asia/Taipei": "CST-8",
|
'Asia/Taipei': 'CST-8',
|
||||||
"Asia/Tashkent": "UNK-5",
|
'Asia/Tashkent': 'UNK-5',
|
||||||
"Asia/Tbilisi": "UNK-4",
|
'Asia/Tbilisi': 'UNK-4',
|
||||||
"Asia/Tehran": "UNK-3:30UNK,J79/24,J263/24",
|
'Asia/Tehran': 'UNK-3:30UNK,J79/24,J263/24',
|
||||||
"Asia/Thimphu": "UNK-6",
|
'Asia/Thimphu': 'UNK-6',
|
||||||
"Asia/Tokyo": "JST-9",
|
'Asia/Tokyo': 'JST-9',
|
||||||
"Asia/Tomsk": "UNK-7",
|
'Asia/Tomsk': 'UNK-7',
|
||||||
"Asia/Ulaanbaatar": "UNK-8",
|
'Asia/Ulaanbaatar': 'UNK-8',
|
||||||
"Asia/Urumqi": "UNK-6",
|
'Asia/Urumqi': 'UNK-6',
|
||||||
"Asia/Ust-Nera": "UNK-10",
|
'Asia/Ust-Nera': 'UNK-10',
|
||||||
"Asia/Vientiane": "UNK-7",
|
'Asia/Vientiane': 'UNK-7',
|
||||||
"Asia/Vladivostok": "UNK-10",
|
'Asia/Vladivostok': 'UNK-10',
|
||||||
"Asia/Yakutsk": "UNK-9",
|
'Asia/Yakutsk': 'UNK-9',
|
||||||
"Asia/Yangon": "UNK-6:30",
|
'Asia/Yangon': 'UNK-6:30',
|
||||||
"Asia/Yekaterinburg": "UNK-5",
|
'Asia/Yekaterinburg': 'UNK-5',
|
||||||
"Asia/Yerevan": "UNK-4",
|
'Asia/Yerevan': 'UNK-4',
|
||||||
"Atlantic/Azores": "UNK1UNK,M3.5.0/0,M10.5.0/1",
|
'Atlantic/Azores': 'UNK1UNK,M3.5.0/0,M10.5.0/1',
|
||||||
"Atlantic/Bermuda": "AST4ADT,M3.2.0,M11.1.0",
|
'Atlantic/Bermuda': 'AST4ADT,M3.2.0,M11.1.0',
|
||||||
"Atlantic/Canary": "WET0WEST,M3.5.0/1,M10.5.0",
|
'Atlantic/Canary': 'WET0WEST,M3.5.0/1,M10.5.0',
|
||||||
"Atlantic/Cape_Verde": "UNK1",
|
'Atlantic/Cape_Verde': 'UNK1',
|
||||||
"Atlantic/Faroe": "WET0WEST,M3.5.0/1,M10.5.0",
|
'Atlantic/Faroe': 'WET0WEST,M3.5.0/1,M10.5.0',
|
||||||
"Atlantic/Madeira": "WET0WEST,M3.5.0/1,M10.5.0",
|
'Atlantic/Madeira': 'WET0WEST,M3.5.0/1,M10.5.0',
|
||||||
"Atlantic/Reykjavik": "GMT0",
|
'Atlantic/Reykjavik': 'GMT0',
|
||||||
"Atlantic/South_Georgia": "UNK2",
|
'Atlantic/South_Georgia': 'UNK2',
|
||||||
"Atlantic/St_Helena": "GMT0",
|
'Atlantic/St_Helena': 'GMT0',
|
||||||
"Atlantic/Stanley": "UNK3",
|
'Atlantic/Stanley': 'UNK3',
|
||||||
"Australia/Adelaide": "ACST-9:30ACDT,M10.1.0,M4.1.0/3",
|
'Australia/Adelaide': 'ACST-9:30ACDT,M10.1.0,M4.1.0/3',
|
||||||
"Australia/Brisbane": "AEST-10",
|
'Australia/Brisbane': 'AEST-10',
|
||||||
"Australia/Broken_Hill": "ACST-9:30ACDT,M10.1.0,M4.1.0/3",
|
'Australia/Broken_Hill': 'ACST-9:30ACDT,M10.1.0,M4.1.0/3',
|
||||||
"Australia/Currie": "AEST-10AEDT,M10.1.0,M4.1.0/3",
|
'Australia/Currie': 'AEST-10AEDT,M10.1.0,M4.1.0/3',
|
||||||
"Australia/Darwin": "ACST-9:30",
|
'Australia/Darwin': 'ACST-9:30',
|
||||||
"Australia/Eucla": "UNK-8:45",
|
'Australia/Eucla': 'UNK-8:45',
|
||||||
"Australia/Hobart": "AEST-10AEDT,M10.1.0,M4.1.0/3",
|
'Australia/Hobart': 'AEST-10AEDT,M10.1.0,M4.1.0/3',
|
||||||
"Australia/Lindeman": "AEST-10",
|
'Australia/Lindeman': 'AEST-10',
|
||||||
"Australia/Lord_Howe": "UNK-10:30UNK-11,M10.1.0,M4.1.0",
|
'Australia/Lord_Howe': 'UNK-10:30UNK-11,M10.1.0,M4.1.0',
|
||||||
"Australia/Melbourne": "AEST-10AEDT,M10.1.0,M4.1.0/3",
|
'Australia/Melbourne': 'AEST-10AEDT,M10.1.0,M4.1.0/3',
|
||||||
"Australia/Perth": "AWST-8",
|
'Australia/Perth': 'AWST-8',
|
||||||
"Australia/Sydney": "AEST-10AEDT,M10.1.0,M4.1.0/3",
|
'Australia/Sydney': 'AEST-10AEDT,M10.1.0,M4.1.0/3',
|
||||||
"Etc/GMT": "GMT0",
|
'Etc/GMT': 'GMT0',
|
||||||
"Etc/GMT+0": "GMT0",
|
'Etc/GMT+0': 'GMT0',
|
||||||
"Etc/GMT+1": "UNK1",
|
'Etc/GMT+1': 'UNK1',
|
||||||
"Etc/GMT+10": "UNK10",
|
'Etc/GMT+10': 'UNK10',
|
||||||
"Etc/GMT+11": "UNK11",
|
'Etc/GMT+11': 'UNK11',
|
||||||
"Etc/GMT+12": "UNK12",
|
'Etc/GMT+12': 'UNK12',
|
||||||
"Etc/GMT+2": "UNK2",
|
'Etc/GMT+2': 'UNK2',
|
||||||
"Etc/GMT+3": "UNK3",
|
'Etc/GMT+3': 'UNK3',
|
||||||
"Etc/GMT+4": "UNK4",
|
'Etc/GMT+4': 'UNK4',
|
||||||
"Etc/GMT+5": "UNK5",
|
'Etc/GMT+5': 'UNK5',
|
||||||
"Etc/GMT+6": "UNK6",
|
'Etc/GMT+6': 'UNK6',
|
||||||
"Etc/GMT+7": "UNK7",
|
'Etc/GMT+7': 'UNK7',
|
||||||
"Etc/GMT+8": "UNK8",
|
'Etc/GMT+8': 'UNK8',
|
||||||
"Etc/GMT+9": "UNK9",
|
'Etc/GMT+9': 'UNK9',
|
||||||
"Etc/GMT-0": "GMT0",
|
'Etc/GMT-0': 'GMT0',
|
||||||
"Etc/GMT-1": "UNK-1",
|
'Etc/GMT-1': 'UNK-1',
|
||||||
"Etc/GMT-10": "UNK-10",
|
'Etc/GMT-10': 'UNK-10',
|
||||||
"Etc/GMT-11": "UNK-11",
|
'Etc/GMT-11': 'UNK-11',
|
||||||
"Etc/GMT-12": "UNK-12",
|
'Etc/GMT-12': 'UNK-12',
|
||||||
"Etc/GMT-13": "UNK-13",
|
'Etc/GMT-13': 'UNK-13',
|
||||||
"Etc/GMT-14": "UNK-14",
|
'Etc/GMT-14': 'UNK-14',
|
||||||
"Etc/GMT-2": "UNK-2",
|
'Etc/GMT-2': 'UNK-2',
|
||||||
"Etc/GMT-3": "UNK-3",
|
'Etc/GMT-3': 'UNK-3',
|
||||||
"Etc/GMT-4": "UNK-4",
|
'Etc/GMT-4': 'UNK-4',
|
||||||
"Etc/GMT-5": "UNK-5",
|
'Etc/GMT-5': 'UNK-5',
|
||||||
"Etc/GMT-6": "UNK-6",
|
'Etc/GMT-6': 'UNK-6',
|
||||||
"Etc/GMT-7": "UNK-7",
|
'Etc/GMT-7': 'UNK-7',
|
||||||
"Etc/GMT-8": "UNK-8",
|
'Etc/GMT-8': 'UNK-8',
|
||||||
"Etc/GMT-9": "UNK-9",
|
'Etc/GMT-9': 'UNK-9',
|
||||||
"Etc/GMT0": "GMT0",
|
'Etc/GMT0': 'GMT0',
|
||||||
"Etc/Greenwich": "GMT0",
|
'Etc/Greenwich': 'GMT0',
|
||||||
"Etc/UCT": "UTC0",
|
'Etc/UCT': 'UTC0',
|
||||||
"Etc/UTC": "UTC0",
|
'Etc/UTC': 'UTC0',
|
||||||
"Etc/Universal": "UTC0",
|
'Etc/Universal': 'UTC0',
|
||||||
"Etc/Zulu": "UTC0",
|
'Etc/Zulu': 'UTC0',
|
||||||
"Europe/Amsterdam": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Amsterdam': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Andorra": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Andorra': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Astrakhan": "UNK-4",
|
'Europe/Astrakhan': 'UNK-4',
|
||||||
"Europe/Athens": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
'Europe/Athens': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||||
"Europe/Belgrade": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Belgrade': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Berlin": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Berlin': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Bratislava": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Bratislava': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Brussels": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Brussels': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Bucharest": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
'Europe/Bucharest': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||||
"Europe/Budapest": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Budapest': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Busingen": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Busingen': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Chisinau": "EET-2EEST,M3.5.0,M10.5.0/3",
|
'Europe/Chisinau': 'EET-2EEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Copenhagen": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Copenhagen': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Dublin": "IST-1GMT0,M10.5.0,M3.5.0/1",
|
'Europe/Dublin': 'IST-1GMT0,M10.5.0,M3.5.0/1',
|
||||||
"Europe/Gibraltar": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Gibraltar': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Guernsey": "GMT0BST,M3.5.0/1,M10.5.0",
|
'Europe/Guernsey': 'GMT0BST,M3.5.0/1,M10.5.0',
|
||||||
"Europe/Helsinki": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
'Europe/Helsinki': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||||
"Europe/Isle_of_Man": "GMT0BST,M3.5.0/1,M10.5.0",
|
'Europe/Isle_of_Man': 'GMT0BST,M3.5.0/1,M10.5.0',
|
||||||
"Europe/Istanbul": "UNK-3",
|
'Europe/Istanbul': 'UNK-3',
|
||||||
"Europe/Jersey": "GMT0BST,M3.5.0/1,M10.5.0",
|
'Europe/Jersey': 'GMT0BST,M3.5.0/1,M10.5.0',
|
||||||
"Europe/Kaliningrad": "EET-2",
|
'Europe/Kaliningrad': 'EET-2',
|
||||||
"Europe/Kiev": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
'Europe/Kiev': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||||
"Europe/Kirov": "UNK-3",
|
'Europe/Kirov': 'UNK-3',
|
||||||
"Europe/Lisbon": "WET0WEST,M3.5.0/1,M10.5.0",
|
'Europe/Lisbon': 'WET0WEST,M3.5.0/1,M10.5.0',
|
||||||
"Europe/Ljubljana": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Ljubljana': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/London": "GMT0BST,M3.5.0/1,M10.5.0",
|
'Europe/London': 'GMT0BST,M3.5.0/1,M10.5.0',
|
||||||
"Europe/Luxembourg": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Luxembourg': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Madrid": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Madrid': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Malta": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Malta': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Mariehamn": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
'Europe/Mariehamn': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||||
"Europe/Minsk": "UNK-3",
|
'Europe/Minsk': 'UNK-3',
|
||||||
"Europe/Monaco": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Monaco': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Moscow": "MSK-3",
|
'Europe/Moscow': 'MSK-3',
|
||||||
"Europe/Oslo": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Oslo': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Paris": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Paris': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Podgorica": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Podgorica': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Prague": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Prague': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Riga": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
'Europe/Riga': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||||
"Europe/Rome": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Rome': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Samara": "UNK-4",
|
'Europe/Samara': 'UNK-4',
|
||||||
"Europe/San_Marino": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/San_Marino': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Sarajevo": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Sarajevo': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Saratov": "UNK-4",
|
'Europe/Saratov': 'UNK-4',
|
||||||
"Europe/Simferopol": "MSK-3",
|
'Europe/Simferopol': 'MSK-3',
|
||||||
"Europe/Skopje": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Skopje': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Sofia": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
'Europe/Sofia': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||||
"Europe/Stockholm": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Stockholm': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Tallinn": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
'Europe/Tallinn': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||||
"Europe/Tirane": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Tirane': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Ulyanovsk": "UNK-4",
|
'Europe/Ulyanovsk': 'UNK-4',
|
||||||
"Europe/Uzhgorod": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
'Europe/Uzhgorod': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||||
"Europe/Vaduz": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Vaduz': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Vatican": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Vatican': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Vienna": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Vienna': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Vilnius": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
'Europe/Vilnius': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||||
"Europe/Volgograd": "UNK-4",
|
'Europe/Volgograd': 'UNK-4',
|
||||||
"Europe/Warsaw": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Warsaw': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Zagreb": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Zagreb': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Zaporozhye": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
'Europe/Zaporozhye': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||||
"Europe/Zurich": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Zurich': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Indian/Antananarivo": "EAT-3",
|
'Indian/Antananarivo': 'EAT-3',
|
||||||
"Indian/Chagos": "UNK-6",
|
'Indian/Chagos': 'UNK-6',
|
||||||
"Indian/Christmas": "UNK-7",
|
'Indian/Christmas': 'UNK-7',
|
||||||
"Indian/Cocos": "UNK-6:30",
|
'Indian/Cocos': 'UNK-6:30',
|
||||||
"Indian/Comoro": "EAT-3",
|
'Indian/Comoro': 'EAT-3',
|
||||||
"Indian/Kerguelen": "UNK-5",
|
'Indian/Kerguelen': 'UNK-5',
|
||||||
"Indian/Mahe": "UNK-4",
|
'Indian/Mahe': 'UNK-4',
|
||||||
"Indian/Maldives": "UNK-5",
|
'Indian/Maldives': 'UNK-5',
|
||||||
"Indian/Mauritius": "UNK-4",
|
'Indian/Mauritius': 'UNK-4',
|
||||||
"Indian/Mayotte": "EAT-3",
|
'Indian/Mayotte': 'EAT-3',
|
||||||
"Indian/Reunion": "UNK-4",
|
'Indian/Reunion': 'UNK-4',
|
||||||
"Pacific/Apia": "UNK-13UNK,M9.5.0/3,M4.1.0/4",
|
'Pacific/Apia': 'UNK-13UNK,M9.5.0/3,M4.1.0/4',
|
||||||
"Pacific/Auckland": "NZST-12NZDT,M9.5.0,M4.1.0/3",
|
'Pacific/Auckland': 'NZST-12NZDT,M9.5.0,M4.1.0/3',
|
||||||
"Pacific/Bougainville": "UNK-11",
|
'Pacific/Bougainville': 'UNK-11',
|
||||||
"Pacific/Chatham": "UNK-12:45UNK,M9.5.0/2:45,M4.1.0/3:45",
|
'Pacific/Chatham': 'UNK-12:45UNK,M9.5.0/2:45,M4.1.0/3:45',
|
||||||
"Pacific/Chuuk": "UNK-10",
|
'Pacific/Chuuk': 'UNK-10',
|
||||||
"Pacific/Easter": "UNK6UNK,M9.1.6/22,M4.1.6/22",
|
'Pacific/Easter': 'UNK6UNK,M9.1.6/22,M4.1.6/22',
|
||||||
"Pacific/Efate": "UNK-11",
|
'Pacific/Efate': 'UNK-11',
|
||||||
"Pacific/Enderbury": "UNK-13",
|
'Pacific/Enderbury': 'UNK-13',
|
||||||
"Pacific/Fakaofo": "UNK-13",
|
'Pacific/Fakaofo': 'UNK-13',
|
||||||
"Pacific/Fiji": "UNK-12UNK,M11.2.0,M1.2.3/99",
|
'Pacific/Fiji': 'UNK-12UNK,M11.2.0,M1.2.3/99',
|
||||||
"Pacific/Funafuti": "UNK-12",
|
'Pacific/Funafuti': 'UNK-12',
|
||||||
"Pacific/Galapagos": "UNK6",
|
'Pacific/Galapagos': 'UNK6',
|
||||||
"Pacific/Gambier": "UNK9",
|
'Pacific/Gambier': 'UNK9',
|
||||||
"Pacific/Guadalcanal": "UNK-11",
|
'Pacific/Guadalcanal': 'UNK-11',
|
||||||
"Pacific/Guam": "ChST-10",
|
'Pacific/Guam': 'ChST-10',
|
||||||
"Pacific/Honolulu": "HST10",
|
'Pacific/Honolulu': 'HST10',
|
||||||
"Pacific/Kiritimati": "UNK-14",
|
'Pacific/Kiritimati': 'UNK-14',
|
||||||
"Pacific/Kosrae": "UNK-11",
|
'Pacific/Kosrae': 'UNK-11',
|
||||||
"Pacific/Kwajalein": "UNK-12",
|
'Pacific/Kwajalein': 'UNK-12',
|
||||||
"Pacific/Majuro": "UNK-12",
|
'Pacific/Majuro': 'UNK-12',
|
||||||
"Pacific/Marquesas": "UNK9:30",
|
'Pacific/Marquesas': 'UNK9:30',
|
||||||
"Pacific/Midway": "SST11",
|
'Pacific/Midway': 'SST11',
|
||||||
"Pacific/Nauru": "UNK-12",
|
'Pacific/Nauru': 'UNK-12',
|
||||||
"Pacific/Niue": "UNK11",
|
'Pacific/Niue': 'UNK11',
|
||||||
"Pacific/Norfolk": "UNK-11UNK,M10.1.0,M4.1.0/3",
|
'Pacific/Norfolk': 'UNK-11UNK,M10.1.0,M4.1.0/3',
|
||||||
"Pacific/Noumea": "UNK-11",
|
'Pacific/Noumea': 'UNK-11',
|
||||||
"Pacific/Pago_Pago": "SST11",
|
'Pacific/Pago_Pago': 'SST11',
|
||||||
"Pacific/Palau": "UNK-9",
|
'Pacific/Palau': 'UNK-9',
|
||||||
"Pacific/Pitcairn": "UNK8",
|
'Pacific/Pitcairn': 'UNK8',
|
||||||
"Pacific/Pohnpei": "UNK-11",
|
'Pacific/Pohnpei': 'UNK-11',
|
||||||
"Pacific/Port_Moresby": "UNK-10",
|
'Pacific/Port_Moresby': 'UNK-10',
|
||||||
"Pacific/Rarotonga": "UNK10",
|
'Pacific/Rarotonga': 'UNK10',
|
||||||
"Pacific/Saipan": "ChST-10",
|
'Pacific/Saipan': 'ChST-10',
|
||||||
"Pacific/Tahiti": "UNK10",
|
'Pacific/Tahiti': 'UNK10',
|
||||||
"Pacific/Tarawa": "UNK-12",
|
'Pacific/Tarawa': 'UNK-12',
|
||||||
"Pacific/Tongatapu": "UNK-13",
|
'Pacific/Tongatapu': 'UNK-13',
|
||||||
"Pacific/Wake": "UNK-12",
|
'Pacific/Wake': 'UNK-12',
|
||||||
"Pacific/Wallis": "UNK-12"
|
'Pacific/Wallis': 'UNK-12'
|
||||||
}
|
};
|
||||||
|
|
||||||
export function selectedTimeZone(label: string, format: string) {
|
export function selectedTimeZone(label: string, format: string) {
|
||||||
return TIME_ZONES[label] === format ? label : undefined;
|
return TIME_ZONES[label] === format ? label : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function timeZoneSelectItems() {
|
export function timeZoneSelectItems() {
|
||||||
return Object.keys(TIME_ZONES).map(label => (
|
return Object.keys(TIME_ZONES).map((label) => (
|
||||||
<MenuItem key={label} value={label}>{label}</MenuItem>
|
<MenuItem key={label} value={label}>
|
||||||
|
{label}
|
||||||
|
</MenuItem>
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -1,32 +1,29 @@
|
|||||||
import parseMilliseconds from 'parse-ms';
|
import parseMilliseconds from 'parse-ms';
|
||||||
|
|
||||||
const LOCALE_FORMAT = new Intl.DateTimeFormat(
|
const LOCALE_FORMAT = new Intl.DateTimeFormat([...window.navigator.languages], {
|
||||||
[...window.navigator.languages],
|
day: 'numeric',
|
||||||
{
|
month: 'short',
|
||||||
day: 'numeric',
|
year: 'numeric',
|
||||||
month: 'short',
|
hour: 'numeric',
|
||||||
year: 'numeric',
|
minute: 'numeric',
|
||||||
hour: 'numeric',
|
second: 'numeric',
|
||||||
minute: 'numeric',
|
hour12: false
|
||||||
second: 'numeric',
|
});
|
||||||
hour12: false
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const formatDateTime = (dateTime: string) => {
|
export const formatDateTime = (dateTime: string) => {
|
||||||
return LOCALE_FORMAT.format(new Date(dateTime.substr(0, 19)));
|
return LOCALE_FORMAT.format(new Date(dateTime.substr(0, 19)));
|
||||||
}
|
};
|
||||||
|
|
||||||
export const formatLocalDateTime = (date: Date) => {
|
export const formatLocalDateTime = (date: Date) => {
|
||||||
return new Date(date.getTime() - date.getTimezoneOffset() * 60000)
|
return new Date(date.getTime() - date.getTimezoneOffset() * 60000)
|
||||||
.toISOString()
|
.toISOString()
|
||||||
.slice(0, -1)
|
.slice(0, -1)
|
||||||
.substr(0, 19);
|
.substr(0, 19);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const formatDuration = (duration: number) => {
|
export const formatDuration = (duration: number) => {
|
||||||
const { days, hours, minutes, seconds } = parseMilliseconds(duration * 1000);
|
const { days, hours, minutes, seconds } = parseMilliseconds(duration * 1000);
|
||||||
var formatted = '';
|
let formatted = '';
|
||||||
if (days) {
|
if (days) {
|
||||||
formatted += pluralize(days, 'day');
|
formatted += pluralize(days, 'day');
|
||||||
}
|
}
|
||||||
@@ -40,6 +37,7 @@ export const formatDuration = (duration: number) => {
|
|||||||
formatted += pluralize(seconds, 'second');
|
formatted += pluralize(seconds, 'second');
|
||||||
}
|
}
|
||||||
return formatted;
|
return formatted;
|
||||||
}
|
};
|
||||||
|
|
||||||
const pluralize = (count: number, noun: string, suffix: string = 's') => ` ${count} ${noun}${count !== 1 ? suffix : ''} `;
|
const pluralize = (count: number, noun: string, suffix = 's') =>
|
||||||
|
` ${count} ${noun}${count !== 1 ? suffix : ''} `;
|
||||||
|
|||||||
@@ -1,23 +1,22 @@
|
|||||||
import React from 'react';
|
|
||||||
import MenuItem from '@material-ui/core/MenuItem';
|
import MenuItem from '@material-ui/core/MenuItem';
|
||||||
|
|
||||||
type BoardProfiles = {
|
type BoardProfiles = {
|
||||||
[name: string]: string
|
[name: string]: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BOARD_PROFILES: BoardProfiles = {
|
export const BOARD_PROFILES: BoardProfiles = {
|
||||||
"S32": "BBQKees Gateway S32",
|
S32: 'BBQKees Gateway S32',
|
||||||
"E32": "BBQKees Gateway E32",
|
E32: 'BBQKees Gateway E32',
|
||||||
"NODEMCU": "NodeMCU 32S",
|
NODEMCU: 'NodeMCU 32S',
|
||||||
"MT-ET": "MT-ET Live D1 Mini",
|
'MH-ET': 'MH-ET Live D1 Mini',
|
||||||
"LOLIN": "Lolin D32",
|
LOLIN: 'Lolin D32',
|
||||||
"OLIMEX": "Olimex ESP32-EVB",
|
OLIMEX: 'Olimex ESP32-EVB'
|
||||||
"TLK110": "Generic Ethernet (TLK110)",
|
};
|
||||||
"LAN8720": "Generic Ethernet (LAN8720)"
|
|
||||||
}
|
|
||||||
|
|
||||||
export function boardProfileSelectItems() {
|
export function boardProfileSelectItems() {
|
||||||
return Object.keys(BOARD_PROFILES).map(code => (
|
return Object.keys(BOARD_PROFILES).map((code) => (
|
||||||
<MenuItem key={code} value={code}>{BOARD_PROFILES[code]}</MenuItem>
|
<MenuItem key={code} value={code}>
|
||||||
));
|
{BOARD_PROFILES[code]}
|
||||||
|
</MenuItem>
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
|
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
|
||||||
|
|
||||||
import { Tabs, Tab } from '@material-ui/core';
|
import { Tabs, Tab } from '@material-ui/core';
|
||||||
|
|
||||||
@@ -8,34 +8,47 @@ import { MenuAppBar } from '../components';
|
|||||||
import { AuthenticatedRoute } from '../authentication';
|
import { AuthenticatedRoute } from '../authentication';
|
||||||
|
|
||||||
import EMSESPStatusController from './EMSESPStatusController';
|
import EMSESPStatusController from './EMSESPStatusController';
|
||||||
import EMSESPDevicesController from './EMSESPDevicesController';
|
import EMSESPDataController from './EMSESPDataController';
|
||||||
import EMSESPHelp from './EMSESPHelp';
|
import EMSESPHelp from './EMSESPHelp';
|
||||||
|
|
||||||
class EMSESP extends Component<RouteComponentProps> {
|
class EMSESP extends Component<RouteComponentProps> {
|
||||||
|
handleTabChange = (path: string) => {
|
||||||
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
|
|
||||||
this.props.history.push(path);
|
this.props.history.push(path);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<MenuAppBar sectionTitle="Dashboard">
|
<MenuAppBar sectionTitle="Dashboard">
|
||||||
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
|
<Tabs
|
||||||
<Tab value={`/${PROJECT_PATH}/devices`} label="Devices & Sensors" />
|
value={this.props.match.url}
|
||||||
|
onChange={(e, path) => this.handleTabChange(path)}
|
||||||
|
variant="fullWidth"
|
||||||
|
>
|
||||||
|
<Tab value={`/${PROJECT_PATH}/data`} label="Devices & Sensors" />
|
||||||
<Tab value={`/${PROJECT_PATH}/status`} label="EMS Status" />
|
<Tab value={`/${PROJECT_PATH}/status`} label="EMS Status" />
|
||||||
<Tab value={`/${PROJECT_PATH}/help`} label="EMS-ESP Help" />
|
<Tab value={`/${PROJECT_PATH}/help`} label="EMS-ESP Help" />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<Switch>
|
<Switch>
|
||||||
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/devices`} component={EMSESPDevicesController} />
|
<AuthenticatedRoute
|
||||||
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/status`} component={EMSESPStatusController} />
|
exact
|
||||||
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/help`} component={EMSESPHelp} />
|
path={`/${PROJECT_PATH}/data`}
|
||||||
<Redirect to={`/${PROJECT_PATH}/devices`} />
|
component={EMSESPDataController}
|
||||||
|
/>
|
||||||
|
<AuthenticatedRoute
|
||||||
|
exact
|
||||||
|
path={`/${PROJECT_PATH}/status`}
|
||||||
|
component={EMSESPStatusController}
|
||||||
|
/>
|
||||||
|
<AuthenticatedRoute
|
||||||
|
exact
|
||||||
|
path={`/${PROJECT_PATH}/help`}
|
||||||
|
component={EMSESPHelp}
|
||||||
|
/>
|
||||||
|
<Redirect to={`/${PROJECT_PATH}/data`} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</MenuAppBar>
|
</MenuAppBar>
|
||||||
|
);
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EMSESP;
|
export default EMSESP;
|
||||||
|
|||||||
35
interface/src/project/EMSESPDataController.tsx
Normal file
35
interface/src/project/EMSESPDataController.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
restController,
|
||||||
|
RestControllerProps,
|
||||||
|
RestFormLoader,
|
||||||
|
SectionContent
|
||||||
|
} from '../components';
|
||||||
|
|
||||||
|
import { ENDPOINT_ROOT } from '../api';
|
||||||
|
import EMSESPDataForm from './EMSESPDataForm';
|
||||||
|
import { EMSESPData } from './EMSESPtypes';
|
||||||
|
|
||||||
|
export const EMSESP_DATA_ENDPOINT = ENDPOINT_ROOT + 'data';
|
||||||
|
|
||||||
|
type EMSESPDataControllerProps = RestControllerProps<EMSESPData>;
|
||||||
|
|
||||||
|
class EMSESPDataController extends Component<EMSESPDataControllerProps> {
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<SectionContent title="Devices & Sensors">
|
||||||
|
<RestFormLoader
|
||||||
|
{...this.props}
|
||||||
|
render={(formProps) => <EMSESPDataForm {...formProps} />}
|
||||||
|
/>
|
||||||
|
</SectionContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default restController(EMSESP_DATA_ENDPOINT, EMSESPDataController);
|
||||||
692
interface/src/project/EMSESPDataForm.tsx
Normal file
692
interface/src/project/EMSESPDataForm.tsx
Normal file
@@ -0,0 +1,692 @@
|
|||||||
|
import React, { Component, Fragment } from 'react';
|
||||||
|
import { withStyles, Theme, createStyles } from '@material-ui/core/styles';
|
||||||
|
|
||||||
|
import parseMilliseconds from 'parse-ms';
|
||||||
|
|
||||||
|
import { Decoder } from '@msgpack/msgpack';
|
||||||
|
const decoder = new Decoder();
|
||||||
|
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
TableContainer,
|
||||||
|
withWidth,
|
||||||
|
WithWidthProps,
|
||||||
|
isWidthDown,
|
||||||
|
Button,
|
||||||
|
Tooltip,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
|
Box,
|
||||||
|
Dialog,
|
||||||
|
Typography
|
||||||
|
} from '@material-ui/core';
|
||||||
|
|
||||||
|
import RefreshIcon from '@material-ui/icons/Refresh';
|
||||||
|
import ListIcon from '@material-ui/icons/List';
|
||||||
|
import IconButton from '@material-ui/core/IconButton';
|
||||||
|
import EditIcon from '@material-ui/icons/Edit';
|
||||||
|
|
||||||
|
import {
|
||||||
|
redirectingAuthorizedFetch,
|
||||||
|
withAuthenticatedContext,
|
||||||
|
AuthenticatedContextProps
|
||||||
|
} from '../authentication';
|
||||||
|
|
||||||
|
import { RestFormProps, FormButton, extractEventValue } from '../components';
|
||||||
|
|
||||||
|
import {
|
||||||
|
EMSESPData,
|
||||||
|
EMSESPDeviceData,
|
||||||
|
Device,
|
||||||
|
DeviceValue,
|
||||||
|
DeviceValueUOM,
|
||||||
|
DeviceValueUOM_s,
|
||||||
|
Sensor
|
||||||
|
} from './EMSESPtypes';
|
||||||
|
|
||||||
|
import ValueForm from './ValueForm';
|
||||||
|
import SensorForm from './SensorForm';
|
||||||
|
|
||||||
|
import { ENDPOINT_ROOT } from '../api';
|
||||||
|
|
||||||
|
export const SCANDEVICES_ENDPOINT = ENDPOINT_ROOT + 'scanDevices';
|
||||||
|
export const DEVICE_DATA_ENDPOINT = ENDPOINT_ROOT + 'deviceData';
|
||||||
|
export const WRITE_VALUE_ENDPOINT = ENDPOINT_ROOT + 'writeValue';
|
||||||
|
export const WRITE_SENSOR_ENDPOINT = ENDPOINT_ROOT + 'writeSensor';
|
||||||
|
|
||||||
|
const StyledTableRow = withStyles((theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
|
root: {
|
||||||
|
'&:nth-child(even)': {
|
||||||
|
backgroundColor: '#4e4e4e'
|
||||||
|
},
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: theme.palette.info.light
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
selected: {
|
||||||
|
backgroundColor: theme.palette.common.white
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)(TableRow);
|
||||||
|
|
||||||
|
const StyledTableRowHeader = withStyles((theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
|
head: {
|
||||||
|
backgroundColor: theme.palette.common.black
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)(TableRow);
|
||||||
|
|
||||||
|
const StyledTooltip = withStyles((theme: Theme) => ({
|
||||||
|
tooltip: {
|
||||||
|
backgroundColor: theme.palette.secondary.main,
|
||||||
|
color: 'white',
|
||||||
|
boxShadow: theme.shadows[1],
|
||||||
|
fontSize: 11,
|
||||||
|
border: '1px solid #dadde9'
|
||||||
|
}
|
||||||
|
}))(Tooltip);
|
||||||
|
|
||||||
|
function compareDevices(a: Device, b: Device) {
|
||||||
|
if (a.t < b.t) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (a.t > b.t) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EMSESPDataFormState {
|
||||||
|
confirmScanDevices: boolean;
|
||||||
|
processing: boolean;
|
||||||
|
deviceData?: EMSESPDeviceData;
|
||||||
|
selectedDevice?: number;
|
||||||
|
edit_devicevalue?: DeviceValue;
|
||||||
|
edit_Sensor?: Sensor;
|
||||||
|
}
|
||||||
|
|
||||||
|
type EMSESPDataFormProps = RestFormProps<EMSESPData> &
|
||||||
|
AuthenticatedContextProps &
|
||||||
|
WithWidthProps;
|
||||||
|
|
||||||
|
const pluralize = (count: number, noun: string, suffix = 's') =>
|
||||||
|
` ${Intl.NumberFormat().format(count)} ${noun}${count !== 1 ? suffix : ''} `;
|
||||||
|
|
||||||
|
export const formatDuration = (duration_min: number) => {
|
||||||
|
const { days, hours, minutes } = parseMilliseconds(duration_min * 60000);
|
||||||
|
let formatted = '';
|
||||||
|
if (days) {
|
||||||
|
formatted += pluralize(days, 'day');
|
||||||
|
}
|
||||||
|
if (hours) {
|
||||||
|
formatted += pluralize(hours, 'hour');
|
||||||
|
}
|
||||||
|
if (minutes) {
|
||||||
|
formatted += pluralize(minutes, 'minute');
|
||||||
|
}
|
||||||
|
return formatted;
|
||||||
|
};
|
||||||
|
|
||||||
|
function formatValue(value: any, uom: number) {
|
||||||
|
switch (uom) {
|
||||||
|
case DeviceValueUOM.HOURS:
|
||||||
|
return value ? formatDuration(value * 60) : '0 hours';
|
||||||
|
case DeviceValueUOM.MINUTES:
|
||||||
|
return value ? formatDuration(value) : '0 minutes';
|
||||||
|
case DeviceValueUOM.NONE:
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
return new Intl.NumberFormat().format(value);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
case DeviceValueUOM.DEGREES:
|
||||||
|
// always show with one decimal place
|
||||||
|
return (
|
||||||
|
new Intl.NumberFormat(undefined, {
|
||||||
|
minimumFractionDigits: 1
|
||||||
|
}).format(value) +
|
||||||
|
' ' +
|
||||||
|
DeviceValueUOM_s[uom]
|
||||||
|
);
|
||||||
|
case DeviceValueUOM.TIMES:
|
||||||
|
case DeviceValueUOM.SECONDS:
|
||||||
|
return pluralize(value, DeviceValueUOM_s[uom]);
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
new Intl.NumberFormat().format(value) + ' ' + DeviceValueUOM_s[uom]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EMSESPDataForm extends Component<
|
||||||
|
EMSESPDataFormProps,
|
||||||
|
EMSESPDataFormState
|
||||||
|
> {
|
||||||
|
state: EMSESPDataFormState = {
|
||||||
|
confirmScanDevices: false,
|
||||||
|
processing: false
|
||||||
|
};
|
||||||
|
|
||||||
|
handleDeviceValueChange = (name: keyof DeviceValue) => (
|
||||||
|
event: React.ChangeEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
|
this.setState({
|
||||||
|
edit_devicevalue: {
|
||||||
|
...this.state.edit_devicevalue!,
|
||||||
|
[name]: extractEventValue(event)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
cancelEditingDeviceValue = () => {
|
||||||
|
this.setState({ edit_devicevalue: undefined });
|
||||||
|
};
|
||||||
|
|
||||||
|
doneEditingDeviceValue = () => {
|
||||||
|
const { edit_devicevalue, selectedDevice } = this.state;
|
||||||
|
|
||||||
|
redirectingAuthorizedFetch(WRITE_VALUE_ENDPOINT, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: selectedDevice,
|
||||||
|
devicevalue: edit_devicevalue
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
this.props.enqueueSnackbar('Write command sent to device', {
|
||||||
|
variant: 'success'
|
||||||
|
});
|
||||||
|
this.handleRowClick(selectedDevice);
|
||||||
|
} else if (response.status === 204) {
|
||||||
|
this.props.enqueueSnackbar('Write command failed', {
|
||||||
|
variant: 'error'
|
||||||
|
});
|
||||||
|
} else if (response.status === 403) {
|
||||||
|
this.props.enqueueSnackbar('Write access denied', {
|
||||||
|
variant: 'error'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw Error('Unexpected response code: ' + response.status);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.props.enqueueSnackbar(error.message || 'Problem writing value', {
|
||||||
|
variant: 'error'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (edit_devicevalue) {
|
||||||
|
this.setState({ edit_devicevalue: undefined });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
sendCommand = (dv: DeviceValue) => {
|
||||||
|
if (dv.c && this.props.authenticatedContext.me.admin) {
|
||||||
|
this.setState({ edit_devicevalue: dv });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSensorChange = (name: keyof Sensor) => (
|
||||||
|
event: React.ChangeEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
|
this.setState({
|
||||||
|
edit_Sensor: {
|
||||||
|
...this.state.edit_Sensor!,
|
||||||
|
[name]: extractEventValue(event)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
cancelEditingSensor = () => {
|
||||||
|
this.setState({ edit_Sensor: undefined });
|
||||||
|
};
|
||||||
|
|
||||||
|
doneEditingSensor = () => {
|
||||||
|
const { edit_Sensor } = this.state;
|
||||||
|
|
||||||
|
redirectingAuthorizedFetch(WRITE_SENSOR_ENDPOINT, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
// because input field with type=number doens't like negative values, force it here
|
||||||
|
sensor: {
|
||||||
|
no: edit_Sensor?.n, // no
|
||||||
|
id: edit_Sensor?.i, // id
|
||||||
|
temp: edit_Sensor?.t, // temp
|
||||||
|
offset: Number(edit_Sensor?.o) // offset
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
this.props.enqueueSnackbar('Sensor updated', {
|
||||||
|
variant: 'success'
|
||||||
|
});
|
||||||
|
this.props.loadData();
|
||||||
|
} else if (response.status === 204) {
|
||||||
|
this.props.enqueueSnackbar('Sensor change failed', {
|
||||||
|
variant: 'error'
|
||||||
|
});
|
||||||
|
} else if (response.status === 403) {
|
||||||
|
this.props.enqueueSnackbar('Write access denied', {
|
||||||
|
variant: 'error'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw Error('Unexpected response code: ' + response.status);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.props.enqueueSnackbar(error.message || 'Problem writing value', {
|
||||||
|
variant: 'error'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (edit_Sensor) {
|
||||||
|
this.setState({ edit_Sensor: undefined });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
sendSensor = (sn: Sensor) => {
|
||||||
|
if (this.props.authenticatedContext.me.admin) {
|
||||||
|
this.setState({ edit_Sensor: sn });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
noDevices = () => {
|
||||||
|
return this.props.data.devices.length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
noSensors = () => {
|
||||||
|
return this.props.data.sensors.length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
noDeviceData = () => {
|
||||||
|
return (this.state.deviceData?.data || []).length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
renderDevices() {
|
||||||
|
const { width, data } = this.props;
|
||||||
|
return (
|
||||||
|
<TableContainer>
|
||||||
|
<Typography variant="h6" color="primary">
|
||||||
|
EMS Devices
|
||||||
|
</Typography>
|
||||||
|
<p></p>
|
||||||
|
{!this.noDevices() && (
|
||||||
|
<Table
|
||||||
|
size="small"
|
||||||
|
padding={isWidthDown('xs', width!) ? 'none' : 'normal'}
|
||||||
|
>
|
||||||
|
<TableBody>
|
||||||
|
{data.devices.sort(compareDevices).map((device) => (
|
||||||
|
<TableRow
|
||||||
|
hover
|
||||||
|
key={device.i}
|
||||||
|
onClick={() => this.handleRowClick(device.i)}
|
||||||
|
>
|
||||||
|
<TableCell>
|
||||||
|
<StyledTooltip
|
||||||
|
title={
|
||||||
|
'DeviceID:0x' +
|
||||||
|
('00' + device.d.toString(16).toUpperCase()).slice(-2) +
|
||||||
|
' ProductID:' +
|
||||||
|
device.p +
|
||||||
|
' Version:' +
|
||||||
|
device.v
|
||||||
|
}
|
||||||
|
placement="right-end"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
startIcon={<ListIcon />}
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
|
{device.t}
|
||||||
|
</Button>
|
||||||
|
</StyledTooltip>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="right">
|
||||||
|
{device.b + ' ' + device.n}{' '}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
{this.noDevices() && (
|
||||||
|
<Box
|
||||||
|
bgcolor="error.main"
|
||||||
|
color="error.contrastText"
|
||||||
|
p={2}
|
||||||
|
mt={2}
|
||||||
|
mb={2}
|
||||||
|
>
|
||||||
|
<Typography variant="body1">
|
||||||
|
No EMS devices found. Check the connections and for possible Tx
|
||||||
|
errors.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</TableContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSensorData() {
|
||||||
|
const { data } = this.props;
|
||||||
|
const me = this.props.authenticatedContext.me;
|
||||||
|
return (
|
||||||
|
<TableContainer>
|
||||||
|
<p></p>
|
||||||
|
<Typography variant="h6" color="primary" paragraph>
|
||||||
|
Sensors
|
||||||
|
</Typography>
|
||||||
|
{!this.noSensors() && (
|
||||||
|
<Table size="small" padding="normal">
|
||||||
|
<TableHead>
|
||||||
|
<StyledTableRowHeader>
|
||||||
|
<TableCell padding="checkbox" style={{ width: 18 }}></TableCell>
|
||||||
|
<TableCell>Dallas Sensor #</TableCell>
|
||||||
|
<TableCell align="left">ID / Name</TableCell>
|
||||||
|
<TableCell align="right">Temperature</TableCell>
|
||||||
|
</StyledTableRowHeader>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{data.sensors.map((sensorData) => (
|
||||||
|
<StyledTableRow
|
||||||
|
key={sensorData.n}
|
||||||
|
onClick={() => this.sendSensor(sensorData)}
|
||||||
|
>
|
||||||
|
<TableCell padding="checkbox">
|
||||||
|
{me.admin && (
|
||||||
|
<StyledTooltip title="edit" placement="left-end">
|
||||||
|
<IconButton edge="start" size="small" aria-label="Edit">
|
||||||
|
<EditIcon color="primary" fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</StyledTooltip>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell component="th" scope="row">
|
||||||
|
{sensorData.n}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="left">{sensorData.i}</TableCell>
|
||||||
|
<TableCell align="right">
|
||||||
|
{formatValue(sensorData.t, DeviceValueUOM.DEGREES)}
|
||||||
|
</TableCell>
|
||||||
|
</StyledTableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
{this.noSensors() && (
|
||||||
|
<Box color="warning.main" p={0} mt={0} mb={0}>
|
||||||
|
<Typography variant="body1">
|
||||||
|
<i>no Dallas temperature sensors were detected</i>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</TableContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderAnalogData() {
|
||||||
|
const { data } = this.props;
|
||||||
|
return (
|
||||||
|
<TableContainer>
|
||||||
|
<p></p>
|
||||||
|
{data.analog > 0 && (
|
||||||
|
<Table size="small" padding="normal">
|
||||||
|
<TableHead>
|
||||||
|
<StyledTableRowHeader>
|
||||||
|
<TableCell padding="normal" style={{ width: 18 }}></TableCell>
|
||||||
|
<TableCell>Sensor Type</TableCell>
|
||||||
|
<TableCell align="right">Value</TableCell>
|
||||||
|
</StyledTableRowHeader>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell padding="normal"> </TableCell>
|
||||||
|
<TableCell component="th" scope="row">
|
||||||
|
Analog Input
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="right">
|
||||||
|
{formatValue(data.analog, DeviceValueUOM.MV)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
</TableContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderScanDevicesDialog() {
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={this.state.confirmScanDevices}
|
||||||
|
onClose={this.onScanDevicesRejected}
|
||||||
|
fullWidth
|
||||||
|
maxWidth="sm"
|
||||||
|
>
|
||||||
|
<DialogTitle>Confirm Scan Devices</DialogTitle>
|
||||||
|
<DialogContent dividers>
|
||||||
|
Are you sure you want to start a scan on the EMS bus for all new
|
||||||
|
devices?
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={this.onScanDevicesRejected}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
startIcon={<RefreshIcon />}
|
||||||
|
variant="contained"
|
||||||
|
onClick={this.onScanDevicesConfirmed}
|
||||||
|
disabled={this.state.processing}
|
||||||
|
color="primary"
|
||||||
|
autoFocus
|
||||||
|
>
|
||||||
|
Start Scan
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onScanDevices = () => {
|
||||||
|
this.setState({ confirmScanDevices: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onScanDevicesRejected = () => {
|
||||||
|
this.setState({ confirmScanDevices: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
onScanDevicesConfirmed = () => {
|
||||||
|
this.setState({ processing: true });
|
||||||
|
redirectingAuthorizedFetch(SCANDEVICES_ENDPOINT)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
this.props.enqueueSnackbar('Device scan is starting...', {
|
||||||
|
variant: 'info'
|
||||||
|
});
|
||||||
|
this.setState({ processing: false, confirmScanDevices: false });
|
||||||
|
} else {
|
||||||
|
throw Error('Invalid status code: ' + response.status);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.props.enqueueSnackbar(error.message || 'Problem with scan', {
|
||||||
|
variant: 'error'
|
||||||
|
});
|
||||||
|
this.setState({ processing: false, confirmScanDevices: false });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
handleRowClick = (device: any) => {
|
||||||
|
this.setState({ selectedDevice: device, deviceData: undefined });
|
||||||
|
redirectingAuthorizedFetch(DEVICE_DATA_ENDPOINT, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ id: device }),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
return response.arrayBuffer();
|
||||||
|
}
|
||||||
|
throw Error('Unexpected response code: ' + response.status);
|
||||||
|
})
|
||||||
|
.then((arrayBuffer) => {
|
||||||
|
const json: any = decoder.decode(arrayBuffer);
|
||||||
|
this.setState({ deviceData: json });
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.props.enqueueSnackbar(
|
||||||
|
error.message || 'Problem getting device data',
|
||||||
|
{ variant: 'error' }
|
||||||
|
);
|
||||||
|
this.setState({ deviceData: undefined });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
renderDeviceData() {
|
||||||
|
const { deviceData } = this.state;
|
||||||
|
const { width } = this.props;
|
||||||
|
|
||||||
|
if (this.noDevices()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!deviceData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<p></p>
|
||||||
|
<Box bgcolor="info.main" p={1} mt={1} mb={1}>
|
||||||
|
<Typography variant="body1" color="initial">
|
||||||
|
{deviceData.type} Data
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
{!this.noDeviceData() && (
|
||||||
|
<TableContainer>
|
||||||
|
<Table
|
||||||
|
size="small"
|
||||||
|
padding={isWidthDown('xs', width!) ? 'none' : 'normal'}
|
||||||
|
>
|
||||||
|
<TableBody>
|
||||||
|
{deviceData.data.map((item, i) => (
|
||||||
|
<StyledTableRow
|
||||||
|
key={i}
|
||||||
|
onClick={() => this.sendCommand(item)}
|
||||||
|
>
|
||||||
|
<TableCell padding="checkbox" style={{ width: 18 }}>
|
||||||
|
{item.c && this.props.authenticatedContext.me.admin && (
|
||||||
|
<StyledTooltip
|
||||||
|
title="change value"
|
||||||
|
placement="left-end"
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
edge="start"
|
||||||
|
size="small"
|
||||||
|
aria-label="Edit"
|
||||||
|
>
|
||||||
|
<EditIcon color="primary" fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</StyledTooltip>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell padding="none" component="th" scope="row">
|
||||||
|
{item.n}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell padding="none" align="right">
|
||||||
|
{formatValue(item.v, item.u)}
|
||||||
|
</TableCell>
|
||||||
|
</StyledTableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
)}
|
||||||
|
{this.noDeviceData() && (
|
||||||
|
<Box color="warning.main" p={0} mt={0} mb={0}>
|
||||||
|
<Typography variant="body1">
|
||||||
|
<i>no data available for this device</i>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { edit_devicevalue, edit_Sensor } = this.state;
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<br></br>
|
||||||
|
{this.renderDevices()}
|
||||||
|
{this.renderDeviceData()}
|
||||||
|
{this.renderSensorData()}
|
||||||
|
{this.renderAnalogData()}
|
||||||
|
<br></br>
|
||||||
|
<Box display="flex" flexWrap="wrap">
|
||||||
|
<Box flexGrow={1} padding={1}>
|
||||||
|
<FormButton
|
||||||
|
startIcon={<RefreshIcon />}
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
onClick={this.props.loadData}
|
||||||
|
>
|
||||||
|
Refresh
|
||||||
|
</FormButton>
|
||||||
|
</Box>
|
||||||
|
<Box flexWrap="none" padding={1} whiteSpace="nowrap">
|
||||||
|
<FormButton
|
||||||
|
startIcon={<RefreshIcon />}
|
||||||
|
variant="contained"
|
||||||
|
onClick={this.onScanDevices}
|
||||||
|
>
|
||||||
|
Scan Devices
|
||||||
|
</FormButton>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
{this.renderScanDevicesDialog()}
|
||||||
|
{edit_devicevalue && (
|
||||||
|
<ValueForm
|
||||||
|
devicevalue={edit_devicevalue}
|
||||||
|
onDoneEditing={this.doneEditingDeviceValue}
|
||||||
|
onCancelEditing={this.cancelEditingDeviceValue}
|
||||||
|
handleValueChange={this.handleDeviceValueChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{edit_Sensor && (
|
||||||
|
<SensorForm
|
||||||
|
sensor={edit_Sensor}
|
||||||
|
onDoneEditing={this.doneEditingSensor}
|
||||||
|
onCancelEditing={this.cancelEditingSensor}
|
||||||
|
handleSensorChange={this.handleSensorChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withAuthenticatedContext(withWidth()(EMSESPDataForm));
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
|
|
||||||
import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
|
||||||
import { ENDPOINT_ROOT } from '../api';
|
|
||||||
import EMSESPDevicesForm from './EMSESPDevicesForm';
|
|
||||||
import { EMSESPDevices } from './EMSESPtypes';
|
|
||||||
|
|
||||||
export const EMSESP_DEVICES_ENDPOINT = ENDPOINT_ROOT + "allDevices";
|
|
||||||
|
|
||||||
type EMSESPDevicesControllerProps = RestControllerProps<EMSESPDevices>;
|
|
||||||
|
|
||||||
class EMSESPDevicesController extends Component<EMSESPDevicesControllerProps> {
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.props.loadData();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<SectionContent title="Devices & Sensors">
|
|
||||||
<RestFormLoader
|
|
||||||
{...this.props}
|
|
||||||
render={formProps => <EMSESPDevicesForm {...formProps} />}
|
|
||||||
/>
|
|
||||||
</SectionContent>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default restController(EMSESP_DEVICES_ENDPOINT, EMSESPDevicesController);
|
|
||||||
@@ -1,389 +0,0 @@
|
|||||||
import React, { Component, Fragment } from "react";
|
|
||||||
import { withStyles, Theme, createStyles } from "@material-ui/core/styles";
|
|
||||||
|
|
||||||
import {
|
|
||||||
Table, TableBody, TableCell, TableHead, TableRow, TableContainer, withWidth, WithWidthProps, isWidthDown,
|
|
||||||
Button, Tooltip, DialogTitle, DialogContent, DialogActions, Box, Dialog, Typography
|
|
||||||
} from "@material-ui/core";
|
|
||||||
|
|
||||||
import RefreshIcon from "@material-ui/icons/Refresh";
|
|
||||||
import ListIcon from "@material-ui/icons/List";
|
|
||||||
|
|
||||||
import { redirectingAuthorizedFetch, withAuthenticatedContext, AuthenticatedContextProps } from "../authentication";
|
|
||||||
import { RestFormProps, FormButton } from "../components";
|
|
||||||
|
|
||||||
import { EMSESPDevices, EMSESPDeviceData, Device } from "./EMSESPtypes";
|
|
||||||
|
|
||||||
import { ENDPOINT_ROOT } from "../api";
|
|
||||||
export const SCANDEVICES_ENDPOINT = ENDPOINT_ROOT + "scanDevices";
|
|
||||||
export const DEVICE_DATA_ENDPOINT = ENDPOINT_ROOT + "deviceData";
|
|
||||||
|
|
||||||
const StyledTableCell = withStyles((theme: Theme) =>
|
|
||||||
createStyles({
|
|
||||||
head: {
|
|
||||||
backgroundColor: theme.palette.common.black,
|
|
||||||
color: theme.palette.common.white,
|
|
||||||
},
|
|
||||||
body: {
|
|
||||||
fontSize: 14,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
)(TableCell);
|
|
||||||
|
|
||||||
function compareDevices(a: Device, b: Device) {
|
|
||||||
if (a.type < b.type) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (a.type > b.type) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EMSESPDevicesFormState {
|
|
||||||
confirmScanDevices: boolean;
|
|
||||||
processing: boolean;
|
|
||||||
deviceData?: EMSESPDeviceData;
|
|
||||||
}
|
|
||||||
|
|
||||||
type EMSESPDevicesFormProps = RestFormProps<EMSESPDevices> &
|
|
||||||
AuthenticatedContextProps &
|
|
||||||
WithWidthProps;
|
|
||||||
|
|
||||||
function formatTemp(t: string) {
|
|
||||||
if (t == null) {
|
|
||||||
return "n/a";
|
|
||||||
}
|
|
||||||
return t + " °C";
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatUnit(u: string) {
|
|
||||||
if (u == null) {
|
|
||||||
return u;
|
|
||||||
}
|
|
||||||
return " " + u;
|
|
||||||
}
|
|
||||||
|
|
||||||
class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesFormState> {
|
|
||||||
state: EMSESPDevicesFormState = {
|
|
||||||
confirmScanDevices: false,
|
|
||||||
processing: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
noDevices = () => {
|
|
||||||
return this.props.data.devices.length === 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
noSensors = () => {
|
|
||||||
return this.props.data.sensors.length === 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
noDeviceData = () => {
|
|
||||||
return (this.state.deviceData?.data || []).length === 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
renderDeviceItems() {
|
|
||||||
const { width, data } = this.props;
|
|
||||||
return (
|
|
||||||
<TableContainer>
|
|
||||||
<Typography variant="h6" color="primary">
|
|
||||||
EMS Devices
|
|
||||||
</Typography>
|
|
||||||
<p></p>
|
|
||||||
{!this.noDevices() && (
|
|
||||||
<Table
|
|
||||||
size="small"
|
|
||||||
padding={isWidthDown("xs", width!) ? "none" : "default"}
|
|
||||||
>
|
|
||||||
<TableHead>
|
|
||||||
<TableRow>
|
|
||||||
<StyledTableCell>Type</StyledTableCell>
|
|
||||||
<StyledTableCell align="center">Brand</StyledTableCell>
|
|
||||||
<StyledTableCell align="center">Model</StyledTableCell>
|
|
||||||
<StyledTableCell align="center">Device ID</StyledTableCell>
|
|
||||||
<StyledTableCell align="center">Product ID</StyledTableCell>
|
|
||||||
<StyledTableCell align="center">Version</StyledTableCell>
|
|
||||||
<StyledTableCell></StyledTableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{data.devices.sort(compareDevices).map((device) => (
|
|
||||||
<TableRow
|
|
||||||
hover
|
|
||||||
key={device.id}
|
|
||||||
onClick={() => this.handleRowClick(device.id)}
|
|
||||||
>
|
|
||||||
<TableCell component="th" scope="row">
|
|
||||||
<Tooltip
|
|
||||||
title="click for details..."
|
|
||||||
arrow
|
|
||||||
placement="right-end"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
startIcon={<ListIcon />}
|
|
||||||
size="small"
|
|
||||||
variant="outlined"
|
|
||||||
>
|
|
||||||
{device.type}
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell align="center">{device.brand}</TableCell>
|
|
||||||
<TableCell align="center">{device.name}</TableCell>
|
|
||||||
<TableCell align="center">
|
|
||||||
0x
|
|
||||||
{("00" + device.deviceid.toString(16).toUpperCase()).slice(
|
|
||||||
-2
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell align="center">{device.productid}</TableCell>
|
|
||||||
<TableCell align="center">{device.version}</TableCell>
|
|
||||||
<TableCell></TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
)}
|
|
||||||
{this.noDevices() && (
|
|
||||||
<Box
|
|
||||||
bgcolor="error.main"
|
|
||||||
color="error.contrastText"
|
|
||||||
p={2}
|
|
||||||
mt={2}
|
|
||||||
mb={2}
|
|
||||||
>
|
|
||||||
<Typography variant="body1">
|
|
||||||
No EMS devices found. Check the connections and for possible Tx
|
|
||||||
errors.
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</TableContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderSensorItems() {
|
|
||||||
const { data } = this.props;
|
|
||||||
return (
|
|
||||||
<TableContainer>
|
|
||||||
<p></p>
|
|
||||||
<Typography variant="h6" color="primary" paragraph>
|
|
||||||
Dallas Sensors
|
|
||||||
</Typography>
|
|
||||||
{!this.noSensors() && (
|
|
||||||
<Table size="small" padding="default">
|
|
||||||
<TableHead>
|
|
||||||
<TableRow>
|
|
||||||
<StyledTableCell>Sensor #</StyledTableCell>
|
|
||||||
<StyledTableCell align="center">ID</StyledTableCell>
|
|
||||||
<StyledTableCell align="right">Temperature</StyledTableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{data.sensors.map((sensorData) => (
|
|
||||||
<TableRow key={sensorData.no}>
|
|
||||||
<TableCell component="th" scope="row">
|
|
||||||
{sensorData.no}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell align="center">{sensorData.id}</TableCell>
|
|
||||||
<TableCell align="right">
|
|
||||||
{formatTemp(sensorData.temp)}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
)}
|
|
||||||
{this.noSensors() && (
|
|
||||||
<Box color="warning.main" p={0} mt={0} mb={0}>
|
|
||||||
<Typography variant="body1">
|
|
||||||
<i>no external temperature sensors were detected</i>
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</TableContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderScanDevicesDialog() {
|
|
||||||
return (
|
|
||||||
<Dialog
|
|
||||||
open={this.state.confirmScanDevices}
|
|
||||||
onClose={this.onScanDevicesRejected}
|
|
||||||
>
|
|
||||||
<DialogTitle>Confirm Scan Devices</DialogTitle>
|
|
||||||
<DialogContent dividers>
|
|
||||||
Are you sure you want to initiate a scan on the EMS bus for all new
|
|
||||||
devices?
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
onClick={this.onScanDevicesRejected}
|
|
||||||
color="secondary"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
startIcon={<RefreshIcon />}
|
|
||||||
variant="contained"
|
|
||||||
onClick={this.onScanDevicesConfirmed}
|
|
||||||
disabled={this.state.processing}
|
|
||||||
color="primary"
|
|
||||||
autoFocus
|
|
||||||
>
|
|
||||||
Start Scan
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
onScanDevices = () => {
|
|
||||||
this.setState({ confirmScanDevices: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onScanDevicesRejected = () => {
|
|
||||||
this.setState({ confirmScanDevices: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
onScanDevicesConfirmed = () => {
|
|
||||||
this.setState({ processing: true });
|
|
||||||
redirectingAuthorizedFetch(SCANDEVICES_ENDPOINT)
|
|
||||||
.then((response) => {
|
|
||||||
if (response.status === 200) {
|
|
||||||
this.props.enqueueSnackbar("Device scan is starting...", {
|
|
||||||
variant: "info",
|
|
||||||
});
|
|
||||||
this.setState({ processing: false, confirmScanDevices: false });
|
|
||||||
} else {
|
|
||||||
throw Error("Invalid status code: " + response.status);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
this.props.enqueueSnackbar(error.message || "Problem with scan", {
|
|
||||||
variant: "error",
|
|
||||||
});
|
|
||||||
this.setState({ processing: false, confirmScanDevices: false });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
handleRowClick = (id: any) => {
|
|
||||||
this.setState({ deviceData: undefined });
|
|
||||||
redirectingAuthorizedFetch(DEVICE_DATA_ENDPOINT, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({ id: id }),
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((response) => {
|
|
||||||
if (response.status === 200) {
|
|
||||||
return response.json();
|
|
||||||
}
|
|
||||||
throw Error("Unexpected response code: " + response.status);
|
|
||||||
})
|
|
||||||
.then((json) => {
|
|
||||||
this.setState({ deviceData: json });
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
this.props.enqueueSnackbar(
|
|
||||||
error.message || "Problem getting device data",
|
|
||||||
{ variant: "error" }
|
|
||||||
);
|
|
||||||
this.setState({ deviceData: undefined });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
renderDeviceData() {
|
|
||||||
const { deviceData } = this.state;
|
|
||||||
const { width } = this.props;
|
|
||||||
|
|
||||||
if (this.noDevices()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!deviceData) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<p></p>
|
|
||||||
<Box bgcolor="info.main" p={1} mt={1} mb={1}>
|
|
||||||
<Typography variant="body1" color="initial">
|
|
||||||
{deviceData.name}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
{!this.noDeviceData() && (
|
|
||||||
<TableContainer>
|
|
||||||
<Table
|
|
||||||
size="small"
|
|
||||||
padding={isWidthDown("xs", width!) ? "none" : "default"}
|
|
||||||
>
|
|
||||||
<TableHead></TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{deviceData.data.map((item, i) => {
|
|
||||||
if (i % 3) {
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<TableRow key={i}>
|
|
||||||
<TableCell component="th" scope="row">{deviceData.data[i + 2]}</TableCell>
|
|
||||||
<TableCell align="right">{deviceData.data[i]}{formatUnit(deviceData.data[i + 1])}</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
)}
|
|
||||||
{this.noDeviceData() && (
|
|
||||||
<Box color="warning.main" p={0} mt={0} mb={0}>
|
|
||||||
<Typography variant="body1">
|
|
||||||
<i>No data available for this device</i>
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Fragment >
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<br></br>
|
|
||||||
{this.renderDeviceItems()}
|
|
||||||
{this.renderDeviceData()}
|
|
||||||
{this.renderSensorItems()}
|
|
||||||
<br></br>
|
|
||||||
<Box display="flex" flexWrap="wrap">
|
|
||||||
<Box flexGrow={1} padding={1}>
|
|
||||||
<FormButton
|
|
||||||
startIcon={<RefreshIcon />}
|
|
||||||
variant="contained"
|
|
||||||
color="secondary"
|
|
||||||
onClick={this.props.loadData}
|
|
||||||
>
|
|
||||||
Refresh
|
|
||||||
</FormButton>
|
|
||||||
</Box>
|
|
||||||
<Box flexWrap="none" padding={1} whiteSpace="nowrap">
|
|
||||||
<FormButton
|
|
||||||
startIcon={<RefreshIcon />}
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
onClick={this.onScanDevices}
|
|
||||||
>
|
|
||||||
Scan Devices
|
|
||||||
</FormButton>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
{this.renderScanDevicesDialog()}
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withAuthenticatedContext(withWidth()(EMSESPDevicesForm));
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user