Compare commits
719 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3858546de | ||
|
|
de40cb8920 | ||
|
|
8ddf1315eb | ||
|
|
00d5b16de7 | ||
|
|
0b02bb417a | ||
|
|
d68260411d | ||
|
|
7227937660 | ||
|
|
e66e2c40a6 | ||
|
|
39bc0d5fb2 | ||
|
|
50e73e2a04 | ||
|
|
efa9875744 | ||
|
|
8182b0e95a | ||
|
|
6091621858 | ||
|
|
ba568581ff | ||
|
|
c78190c3a0 | ||
|
|
84c968e053 | ||
|
|
6b7da4068c | ||
|
|
e3e14e7a66 | ||
|
|
997ced3938 | ||
|
|
b18da9064b | ||
|
|
b3ee5f4d9a | ||
|
|
5398abb074 | ||
|
|
65397d3e1e | ||
|
|
887d53528b | ||
|
|
1603eafbb2 | ||
|
|
33bcb54aaf | ||
|
|
d36f87707a | ||
|
|
226c1fd6c5 | ||
|
|
74b961ab29 | ||
|
|
e4358ba489 | ||
|
|
26522cb061 | ||
|
|
be128da9e0 | ||
|
|
381afcbb6d | ||
|
|
1dc008855e | ||
|
|
bbf4431b5f | ||
|
|
c38ad8e382 | ||
|
|
1416bad9cc | ||
|
|
0e0e9eccf1 | ||
|
|
032a631b61 | ||
|
|
5d0f6b665b | ||
|
|
4bbf096350 | ||
|
|
e1a950ec21 | ||
|
|
ede8e7dfce | ||
|
|
9d155d6a0e | ||
|
|
1fa92eec57 | ||
|
|
7fc28fd1bb | ||
|
|
dd338f5f4b | ||
|
|
486bc5ac28 | ||
|
|
68215f37f5 | ||
|
|
4449d3fce8 | ||
|
|
cbb20439ed | ||
|
|
ec357b71f1 | ||
|
|
5acdb4dc31 | ||
|
|
dd959a7316 | ||
|
|
0b6dbe8bfa | ||
|
|
78e6263a2e | ||
|
|
3b2a4d1eb4 | ||
|
|
485195e035 | ||
|
|
d735f397d8 | ||
|
|
5e41481e27 | ||
|
|
d52c54d6fb | ||
|
|
36c85eed5e | ||
|
|
b09deb1494 | ||
|
|
cb4cce119b | ||
|
|
7e174a1b7d | ||
|
|
1f08940e47 | ||
|
|
8db5724b77 | ||
|
|
a21352ae4f | ||
|
|
a38f4978fa | ||
|
|
4f05ddab93 | ||
|
|
bbbeb155f0 | ||
|
|
f6238cd6ab | ||
|
|
d658b67e93 | ||
|
|
e2c675d9a5 | ||
|
|
3e3a600a60 | ||
|
|
1f0d2b147b | ||
|
|
285b0dc01d | ||
|
|
172153b840 | ||
|
|
ce71b8b5f6 | ||
|
|
d72d2b33bd | ||
|
|
d15fbe7801 | ||
|
|
caab1cae39 | ||
|
|
dced355a29 | ||
|
|
f133fdfb1c | ||
|
|
80ea1e95ee | ||
|
|
4098cef279 | ||
|
|
d0aac18b88 | ||
|
|
7bb35812ff | ||
|
|
ad49267b29 | ||
|
|
88404bcb56 | ||
|
|
33af6ec57e | ||
|
|
4b3a0bf4e2 | ||
|
|
508dd399be | ||
|
|
8b2466dcde | ||
|
|
c754d19015 | ||
|
|
ad680de897 | ||
|
|
78ae4d7a7d | ||
|
|
74acf2fe0f | ||
|
|
bab9fd8ec4 | ||
|
|
cfce36c212 | ||
|
|
5c6d6da4f9 | ||
|
|
5005d06507 | ||
|
|
9a09062a84 | ||
|
|
6dc993a276 | ||
|
|
c45ceec33b | ||
|
|
c28de99907 | ||
|
|
b587c08768 | ||
|
|
71043963e7 | ||
|
|
04b17a15bb | ||
|
|
4aa9d11574 | ||
|
|
56b5739cfc | ||
|
|
bb1704ed7a | ||
|
|
9163fc74d4 | ||
|
|
49113edeff | ||
|
|
718ede8439 | ||
|
|
5ee49d8dad | ||
|
|
c19885d730 | ||
|
|
d2e64468d5 | ||
|
|
e3b6a3308f | ||
|
|
c3f88ae0c8 | ||
|
|
31a5868864 | ||
|
|
4cb50a1dff | ||
|
|
2062703565 | ||
|
|
5e00f07fd8 | ||
|
|
3bf2110df1 | ||
|
|
c3ff60dced | ||
|
|
7cf3b8f1a9 | ||
|
|
4536d60e3e | ||
|
|
2f1ea4da67 | ||
|
|
34f6b412f6 | ||
|
|
b16a16d100 | ||
|
|
55750844ea | ||
|
|
6c09134552 | ||
|
|
8fa74c63c9 | ||
|
|
275ac351fe | ||
|
|
b728827324 | ||
|
|
2db99a3e32 | ||
|
|
263c011a7b | ||
|
|
8eeebb0cef | ||
|
|
f055e53987 | ||
|
|
6dce5f5931 | ||
|
|
14cfbf78bd | ||
|
|
6de577839b | ||
|
|
bfcdf3ef98 | ||
|
|
ffa7ddebb8 | ||
|
|
29838a433a | ||
|
|
1f1422bedd | ||
|
|
d41e634611 | ||
|
|
22cc890cab | ||
|
|
15e940bd2d | ||
|
|
c788f53bb9 | ||
|
|
10f1c5781e | ||
|
|
75b0869a77 | ||
|
|
c4c341922b | ||
|
|
43b4adc618 | ||
|
|
bad982dbdd | ||
|
|
5d45064c2d | ||
|
|
6390f4aa48 | ||
|
|
bb94d56bd0 | ||
|
|
14f3d9ab12 | ||
|
|
d9ecf0efb8 | ||
|
|
9b66b02e46 | ||
|
|
feca878fdd | ||
|
|
f53fd74873 | ||
|
|
bbaf892523 | ||
|
|
451b3abddf | ||
|
|
ba03add3d3 | ||
|
|
a41de7ed1c | ||
|
|
28de5bb097 | ||
|
|
d08a1224e2 | ||
|
|
327cf7ec75 | ||
|
|
83438129a2 | ||
|
|
9b3b7fc8ff | ||
|
|
8000497302 | ||
|
|
84589a4b40 | ||
|
|
067129f5a9 | ||
|
|
ffcf98b06b | ||
|
|
b2f001e416 | ||
|
|
44c31c9c61 | ||
|
|
8aa659eee2 | ||
|
|
51bf163149 | ||
|
|
d5c02f6b94 | ||
|
|
55fa968afb | ||
|
|
a9aac33a46 | ||
|
|
1f45506b37 | ||
|
|
9d9d88b171 | ||
|
|
291da3f7fe | ||
|
|
70cfbc3715 | ||
|
|
9d80c2cea7 | ||
|
|
a7f7959f91 | ||
|
|
082268d9fc | ||
|
|
945ef2f1b0 | ||
|
|
1f1feed3ae | ||
|
|
290890f6e7 | ||
|
|
b7bfd803eb | ||
|
|
2151905d46 | ||
|
|
fdde118af1 | ||
|
|
5f9ba2e04b | ||
|
|
ca871f654b | ||
|
|
3c91ac27dc | ||
|
|
94b4cb0baf | ||
|
|
1b956c6ad7 | ||
|
|
b9b79bbd9a | ||
|
|
9802581301 | ||
|
|
c1f39fbf57 | ||
|
|
f66e7712c3 | ||
|
|
2bb6d985cc | ||
|
|
d300ed38ea | ||
|
|
9cbb810fe4 | ||
|
|
cfa486d8cc | ||
|
|
ab1924d266 | ||
|
|
d6de0f6fa8 | ||
|
|
d1afea104e | ||
|
|
fae8cf83cd | ||
|
|
5a69ac074f | ||
|
|
15f0560005 | ||
|
|
6f7fa6abd9 | ||
|
|
cbd55b0366 | ||
|
|
f1dbd3018d | ||
|
|
502613b433 | ||
|
|
2bdc0d59cf | ||
|
|
ce0ee49ebf | ||
|
|
bc3bdb1221 | ||
|
|
3bea7576b5 | ||
|
|
d70b5d7dc0 | ||
|
|
eb30c1e5c7 | ||
|
|
17cbd2623f | ||
|
|
dc27a44c0c | ||
|
|
f72b02ab0b | ||
|
|
c0946f1e0c | ||
|
|
575fdcb8cd | ||
|
|
783ea7901c | ||
|
|
44e6bb79a2 | ||
|
|
ab3b9f13b5 | ||
|
|
dc6e7f7b1b | ||
|
|
94b75dda24 | ||
|
|
a529879889 | ||
|
|
4b7bbb3d50 | ||
|
|
88c98efd94 | ||
|
|
147be12583 | ||
|
|
137e047205 | ||
|
|
4bd6db31c0 | ||
|
|
1350638fb3 | ||
|
|
b6de431a56 | ||
|
|
1aef27da33 | ||
|
|
1e78979ed0 | ||
|
|
ba90ebda4c | ||
|
|
7c1bade54d | ||
|
|
a46b394714 | ||
|
|
6dff1136c5 | ||
|
|
ccbb56d403 | ||
|
|
4f3a7e5451 | ||
|
|
e4cda4087e | ||
|
|
6eeb8de02c | ||
|
|
29c4fec90a | ||
|
|
5c9ba8de43 | ||
|
|
2358f6a9c9 | ||
|
|
7097279dca | ||
|
|
c3eb553425 | ||
|
|
24f64fac6b | ||
|
|
4cdd5e9f20 | ||
|
|
de9e261807 | ||
|
|
c26793c68d | ||
|
|
11fd833cdb | ||
|
|
2b21a0c31f | ||
|
|
7e888f6408 | ||
|
|
bba70ce852 | ||
|
|
e96b5af0c8 | ||
|
|
5f5e786c0e | ||
|
|
8d66e43117 | ||
|
|
ccbc809ecf | ||
|
|
e67fde24f1 | ||
|
|
923494fdce | ||
|
|
73bff2cabe | ||
|
|
c20fc5a345 | ||
|
|
f71c62f167 | ||
|
|
bdaf9e6dc6 | ||
|
|
a2730fb17c | ||
|
|
5061ddf38e | ||
|
|
1735c036cc | ||
|
|
fa703db41e | ||
|
|
9665f4136a | ||
|
|
abeef2be4a | ||
|
|
995420354e | ||
|
|
1af1a1863a | ||
|
|
fd04f8be5a | ||
|
|
ccc9e6dcfb | ||
|
|
9e23710c6d | ||
|
|
3878a3ee0b | ||
|
|
0c9d0a4d15 | ||
|
|
739c007c95 | ||
|
|
6f27253441 | ||
|
|
b900932574 | ||
|
|
0903b31fcf | ||
|
|
d080f5db0f | ||
|
|
5029a1625e | ||
|
|
2950f167aa | ||
|
|
afdf9a3dfb | ||
|
|
132ca9d106 | ||
|
|
990d75d42a | ||
|
|
1c48aa8444 | ||
|
|
67e07813cb | ||
|
|
7fbeed88a9 | ||
|
|
9053e7ac88 | ||
|
|
e3c94cc1f7 | ||
|
|
e8d6c4d451 | ||
|
|
ba1813c767 | ||
|
|
bcd8992abc | ||
|
|
557b532f74 | ||
|
|
d450464a1a | ||
|
|
5d10b28433 | ||
|
|
eafe358deb | ||
|
|
048bd877da | ||
|
|
7f18dd942f | ||
|
|
424234ec48 | ||
|
|
4168a08276 | ||
|
|
996063d76d | ||
|
|
085f5ff22f | ||
|
|
5b5dc6a8cc | ||
|
|
4103bad8de | ||
|
|
d6a8563cc7 | ||
|
|
c9ef0bcd7b | ||
|
|
6b978759ca | ||
|
|
bc31d54028 | ||
|
|
7bad0e04b1 | ||
|
|
7144c746c6 | ||
|
|
d5592e5662 | ||
|
|
12b027110e | ||
|
|
644896faff | ||
|
|
f41f7c0769 | ||
|
|
68f1a891ca | ||
|
|
b186311968 | ||
|
|
c21b594cf0 | ||
|
|
21ec46843a | ||
|
|
4da827501a | ||
|
|
5d0c7bfd32 | ||
|
|
d583409af4 | ||
|
|
559e607601 | ||
|
|
276b27d8a6 | ||
|
|
2134f42cfd | ||
|
|
88135d2750 | ||
|
|
1b1a4bcad4 | ||
|
|
af8e82a860 | ||
|
|
c8d5e37b44 | ||
|
|
781fe03b5d | ||
|
|
702103aa66 | ||
|
|
d2503431c6 | ||
|
|
dccdf18226 | ||
|
|
a92f287256 | ||
|
|
a193d67f11 | ||
|
|
6873e113bb | ||
|
|
77860d9d05 | ||
|
|
de97010cfb | ||
|
|
89245c7af7 | ||
|
|
d51745774f | ||
|
|
deda1daa04 | ||
|
|
8594c4a7eb | ||
|
|
d0d73aa5e2 | ||
|
|
1f43bb201c | ||
|
|
6e92d31f5d | ||
|
|
ae4070b7f2 | ||
|
|
ee2ac18b69 | ||
|
|
85ce697dad | ||
|
|
82eb79ce40 | ||
|
|
c76b4b9ede | ||
|
|
9c2e814e16 | ||
|
|
2e9499ea90 | ||
|
|
52296bfed1 | ||
|
|
a53d54a1d6 | ||
|
|
81cf08b723 | ||
|
|
f192d7dffc | ||
|
|
5425896988 | ||
|
|
9e2be00b5c | ||
|
|
06004ce478 | ||
|
|
5bda018d25 | ||
|
|
b12729a874 | ||
|
|
d3a84e02e4 | ||
|
|
9865c84df5 | ||
|
|
33d2cb9a49 | ||
|
|
37c121e8de | ||
|
|
9dd0bf01e2 | ||
|
|
92ac601072 | ||
|
|
dfd7647838 | ||
|
|
058246e2ce | ||
|
|
8ed789892b | ||
|
|
57775af24b | ||
|
|
845681b6dc | ||
|
|
ea2c9cbc9a | ||
|
|
96bb3013a3 | ||
|
|
aeee37fdae | ||
|
|
551497bfeb | ||
|
|
57057cac0e | ||
|
|
dccd9f09e7 | ||
|
|
b91e474343 | ||
|
|
f25262c191 | ||
|
|
723662a6bc | ||
|
|
757fcd3ea4 | ||
|
|
ec3e28436d | ||
|
|
6a291ef1e2 | ||
|
|
1e529a9e19 | ||
|
|
05d393e7fe | ||
|
|
c757ace2b4 | ||
|
|
5ab066de5f | ||
|
|
b0ae22d493 | ||
|
|
03a6abcfc6 | ||
|
|
e58491ed97 | ||
|
|
1e92ae05c0 | ||
|
|
513b6181a4 | ||
|
|
05b54bc6f5 | ||
|
|
1d4634a76c | ||
|
|
342cf12ae7 | ||
|
|
6883dbbce1 | ||
|
|
a3d3706b89 | ||
|
|
da911e374a | ||
|
|
26904f4e0e | ||
|
|
ec5601f3ca | ||
|
|
a2ee2a5e6b | ||
|
|
78d5f8b76d | ||
|
|
dd0cc00004 | ||
|
|
825836c447 | ||
|
|
4996eb9e3c | ||
|
|
0bc3a3c34e | ||
|
|
ca5bc313ee | ||
|
|
069df92dbf | ||
|
|
bdce4ee9f9 | ||
|
|
756a136124 | ||
|
|
4809ef3537 | ||
|
|
2c056d6807 | ||
|
|
90af466a2f | ||
|
|
20cbbcd9f4 | ||
|
|
036953044e | ||
|
|
499456fa77 | ||
|
|
22d9705412 | ||
|
|
663c853aff | ||
|
|
103ffa4761 | ||
|
|
3baedf01d1 | ||
|
|
bfad6d34b5 | ||
|
|
2aa2564078 | ||
|
|
6561bb5a6c | ||
|
|
ac75176292 | ||
|
|
1005079f71 | ||
|
|
e0e07a9deb | ||
|
|
c65005e5a6 | ||
|
|
64f2f82e0c | ||
|
|
98495c8114 | ||
|
|
d0ac0b7804 | ||
|
|
e45c31345e | ||
|
|
23cd677133 | ||
|
|
805c1298fb | ||
|
|
5199edff1e | ||
|
|
f3adc13c6d | ||
|
|
40f5f7026d | ||
|
|
b8b96763cf | ||
|
|
80b94fcd00 | ||
|
|
8d09d3c654 | ||
|
|
03564b3a82 | ||
|
|
a7569256d0 | ||
|
|
b0d4a094c1 | ||
|
|
1b232adc72 | ||
|
|
ed81e095ee | ||
|
|
76734b77f1 | ||
|
|
d8284ec09f | ||
|
|
6e982acde8 | ||
|
|
db6f8eaba1 | ||
|
|
eb7ad7163f | ||
|
|
6a478eec5e | ||
|
|
1dc08d6399 | ||
|
|
c3cfed5ac3 | ||
|
|
aba1dc93d9 | ||
|
|
d9bbf35afc | ||
|
|
dec3abfab9 | ||
|
|
bb98f13b19 | ||
|
|
8b10970e03 | ||
|
|
b859ab9d78 | ||
|
|
26208103fc | ||
|
|
d150b017e3 | ||
|
|
49414adfd2 | ||
|
|
5c675c7ce7 | ||
|
|
f16aaf7874 | ||
|
|
d80831e708 | ||
|
|
3e3e7156ec | ||
|
|
dbb2a365cb | ||
|
|
52a8c7288d | ||
|
|
42f6bf6182 | ||
|
|
9d4d3738ff | ||
|
|
1647a6d0a7 | ||
|
|
e5e058672d | ||
|
|
f5cd8e2523 | ||
|
|
fd2cc8aff1 | ||
|
|
f244fb837b | ||
|
|
b4848ab7f9 | ||
|
|
0fa60f0502 | ||
|
|
8d290317d7 | ||
|
|
cb5c8f3a85 | ||
|
|
5a0f4c1462 | ||
|
|
e4445413fd | ||
|
|
9be13eb0b9 | ||
|
|
2c7eeeca7b | ||
|
|
b59a552288 | ||
|
|
def67899ed | ||
|
|
ea0870c180 | ||
|
|
379d57ca8e | ||
|
|
761df2b4cc | ||
|
|
56e1f02f69 | ||
|
|
f921ef4708 | ||
|
|
f11dc9bc25 | ||
|
|
0ebfcd7238 | ||
|
|
5fb9cd142f | ||
|
|
7d5e112efb | ||
|
|
e82dd816de | ||
|
|
887a245d82 | ||
|
|
2f706b33fa | ||
|
|
dcbdb04009 | ||
|
|
008903cbbd | ||
|
|
b67113fc1f | ||
|
|
3afbe832cc | ||
|
|
9adfa0ecfc | ||
|
|
f081d7fd3c | ||
|
|
394a3253aa | ||
|
|
13890d2835 | ||
|
|
6fd3e567cd | ||
|
|
b6d8e55b00 | ||
|
|
e6d3d347ab | ||
|
|
c159ce7eb9 | ||
|
|
65c9bf22dc | ||
|
|
87dfffeddb | ||
|
|
8d6c676fed | ||
|
|
324d27896b | ||
|
|
8c94ce99b2 | ||
|
|
fd5fcf356f | ||
|
|
0dde5a9d2b | ||
|
|
73f7603c1d | ||
|
|
5faffc3886 | ||
|
|
d09d5a7dbe | ||
|
|
b906fecdff | ||
|
|
a6e4122e44 | ||
|
|
e663ecb458 | ||
|
|
95876e28bf | ||
|
|
348932d929 | ||
|
|
bd28516324 | ||
|
|
d1fc050bed | ||
|
|
bace01e4f7 | ||
|
|
0a56ee7dbb | ||
|
|
c90be99216 | ||
|
|
a0a431e0e2 | ||
|
|
4489e7149c | ||
|
|
ec81420894 | ||
|
|
619dd0c99d | ||
|
|
9f5a5108fb | ||
|
|
974510b2c7 | ||
|
|
f2f10f0c79 | ||
|
|
39cbcd4d6f | ||
|
|
1accdfcafb | ||
|
|
e37bbe420c | ||
|
|
58c4455076 | ||
|
|
a413ffb9d2 | ||
|
|
1f96622e74 | ||
|
|
9527cf6abf | ||
|
|
13f0bc3296 | ||
|
|
58a0ec9cca | ||
|
|
fe385de342 | ||
|
|
ae5fb26387 | ||
|
|
cd67ab03ff | ||
|
|
94f134f3fe | ||
|
|
ca95e44a81 | ||
|
|
ecc045b2a8 | ||
|
|
9c205a07c5 | ||
|
|
c414354708 | ||
|
|
ddacd2d9d7 | ||
|
|
987fcb4a5a | ||
|
|
4c70da28e6 | ||
|
|
246684f28c | ||
|
|
e450a0e096 | ||
|
|
90d2144588 | ||
|
|
4302d314cf | ||
|
|
22322a55ed | ||
|
|
d09c0436e0 | ||
|
|
9b619216cb | ||
|
|
fe0a855618 | ||
|
|
3bca7c9a13 | ||
|
|
87887494a5 | ||
|
|
614a8cb14b | ||
|
|
da6e64e89f | ||
|
|
87d0db0b5c | ||
|
|
c756df90fc | ||
|
|
d3053d8ce2 | ||
|
|
4c728cf777 | ||
|
|
17b90af972 | ||
|
|
a0339f5ae2 | ||
|
|
732aaffbb6 | ||
|
|
d50606ce13 | ||
|
|
777c9db0f6 | ||
|
|
815397dba6 | ||
|
|
01f361e7cd | ||
|
|
1278776297 | ||
|
|
a3a8e515bf | ||
|
|
96af9afc83 | ||
|
|
c841c8c284 | ||
|
|
d6ee8ccb2d | ||
|
|
a7930d8403 | ||
|
|
eeeb889ba7 | ||
|
|
f1f4147628 | ||
|
|
42118e0169 | ||
|
|
b67d69a5c8 | ||
|
|
d8144c901d | ||
|
|
e9d9cf7e48 | ||
|
|
0867d7fe0e | ||
|
|
b646331adc | ||
|
|
30109a54ce | ||
|
|
e85ca7dcd3 | ||
|
|
faf05ceb72 | ||
|
|
049e37ba82 | ||
|
|
d187ee23ea | ||
|
|
20b0c9653d | ||
|
|
022b667858 | ||
|
|
1b4af09185 | ||
|
|
58ca4efd42 | ||
|
|
f1bb183017 | ||
|
|
081c11c503 | ||
|
|
d54843635a | ||
|
|
12638275fa | ||
|
|
25c50435ca | ||
|
|
e1c61cfadb | ||
|
|
06fd860964 | ||
|
|
ab4e1f63c5 | ||
|
|
fb998a7e6a | ||
|
|
dbbbfc1170 | ||
|
|
eb432d9acb | ||
|
|
23f65b9eb2 | ||
|
|
c11ea4fe0d | ||
|
|
6b1cfffb6f | ||
|
|
918c0315eb | ||
|
|
106517fc8b | ||
|
|
96070d6b42 | ||
|
|
b22473d0d0 | ||
|
|
ef842b6c52 | ||
|
|
e9415c1708 | ||
|
|
0114efc66f | ||
|
|
84e7847c16 | ||
|
|
3f935942ea | ||
|
|
8f45322b7b | ||
|
|
6e095c1085 | ||
|
|
acaceefc89 | ||
|
|
21c3a4bee8 | ||
|
|
a959af80b5 | ||
|
|
e0d7e7698e | ||
|
|
dbf9912c5a | ||
|
|
f9038b9180 | ||
|
|
506aacbd83 | ||
|
|
63e31d672b | ||
|
|
0d5d353e99 | ||
|
|
766ec6a6e4 | ||
|
|
872effb297 | ||
|
|
fb13b79a76 | ||
|
|
706daeb678 | ||
|
|
9d6af82f9c | ||
|
|
2bc37027dd | ||
|
|
e70b6b210e | ||
|
|
df8a36c695 | ||
|
|
702af4b1c8 | ||
|
|
cd15e11ce3 | ||
|
|
ca8b236ccb | ||
|
|
06b057f4a2 | ||
|
|
0158c9b434 | ||
|
|
368728d12b | ||
|
|
c1cab103aa | ||
|
|
d9da85638c | ||
|
|
a25989d213 | ||
|
|
6bfe4fe258 | ||
|
|
e7cbb8cc77 | ||
|
|
06354efa76 | ||
|
|
345f05b23f | ||
|
|
f0489cb918 | ||
|
|
d0b265985b | ||
|
|
82f0902fa4 | ||
|
|
1dcb44f81a | ||
|
|
9bbc421407 | ||
|
|
54f335946e | ||
|
|
d95d8278de | ||
|
|
c813339945 | ||
|
|
cc44bc9d7f | ||
|
|
4cd655fb36 | ||
|
|
5845c37672 | ||
|
|
9ae81779ff | ||
|
|
df4aa64883 | ||
|
|
70f94322ee | ||
|
|
c2ccb4edda | ||
|
|
f42fafb25f | ||
|
|
0bccf9d358 | ||
|
|
d3f105d9b9 | ||
|
|
5c240316ee | ||
|
|
2fde0c6d8c | ||
|
|
76d78f34ee | ||
|
|
3efb3a99e9 | ||
|
|
65dfc3b878 | ||
|
|
b820aad3fc | ||
|
|
09d4cae6f4 | ||
|
|
85308e52a7 | ||
|
|
4f42ae7efc | ||
|
|
7cc2eac79d | ||
|
|
e3487b4845 | ||
|
|
354b2facf7 | ||
|
|
0d381fd5e9 | ||
|
|
65b7d64e92 | ||
|
|
e79115d719 | ||
|
|
be4f49e96d | ||
|
|
6b24a71ad7 | ||
|
|
1ccacdc600 | ||
|
|
3ea71e1dfb | ||
|
|
d8e324a005 | ||
|
|
7122e878a5 | ||
|
|
c8848a9e76 | ||
|
|
84499dab35 | ||
|
|
e9c695f76a | ||
|
|
029ade8dd6 | ||
|
|
0ebb509205 | ||
|
|
9fa59310d2 | ||
|
|
1f8e6cc82b | ||
|
|
1a4ce643fc |
14
.github/workflows/pre_release.yml
vendored
@@ -14,9 +14,9 @@ jobs:
|
||||
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
|
||||
@@ -24,19 +24,19 @@ jobs:
|
||||
id: build_info
|
||||
run: |
|
||||
version=`grep -E '^#define EMSESP_APP_VERSION' ./src/version.h | awk -F'"' '{print $2}'`
|
||||
echo "::set-output name=version::$version"
|
||||
echo "VERSION=$version" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Install PlatformIO
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -U platformio
|
||||
platformio upgrade
|
||||
platformio update
|
||||
|
||||
- name: Build WebUI
|
||||
run: |
|
||||
cd interface
|
||||
npm ci
|
||||
npx typesafe-i18n --no-watch
|
||||
sed -i "s/= 'pl'/= 'en'/" ./src/i18n/i18n-util.ts
|
||||
npm run build
|
||||
|
||||
- name: Build firmware
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
uses: "marvinpinto/action-automatic-releases@latest"
|
||||
with:
|
||||
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
title: Development Build v${{steps.build_info.outputs.version}}
|
||||
title: Development Build v${{steps.build_info.outputs.VERSION}}
|
||||
automatic_release_tag: "latest"
|
||||
prerelease: true
|
||||
files: |
|
||||
|
||||
4
.github/workflows/tagged_release.yml
vendored
@@ -24,12 +24,14 @@ jobs:
|
||||
python -m pip install --upgrade pip
|
||||
pip install -U platformio
|
||||
platformio upgrade
|
||||
platformio update
|
||||
pio pkg update
|
||||
|
||||
- name: Build WebUI
|
||||
run: |
|
||||
cd interface
|
||||
npm ci
|
||||
npx typesafe-i18n --no-watch
|
||||
sed -i "s/= 'pl'/= 'en'/" ./src/i18n/i18n-util.ts
|
||||
npm run build
|
||||
|
||||
- name: Build firmware
|
||||
|
||||
12
.gitignore
vendored
@@ -13,7 +13,6 @@ debug.log
|
||||
# platformio
|
||||
.pio
|
||||
pio_local.ini
|
||||
/.VSCodeCounter
|
||||
|
||||
# OS specific
|
||||
.DS_Store
|
||||
@@ -31,7 +30,18 @@ test.sh
|
||||
scripts/__pycache__
|
||||
.temp
|
||||
|
||||
# i18n generated files
|
||||
interface/src/i18n/i18n-react.tsx
|
||||
interface/src/i18n/i18n-types.ts
|
||||
interface/src/i18n/i18n-util.ts
|
||||
interface/src/i18n/i18n-util.sync.ts
|
||||
interface/src/i18n/i18n-util.async.ts
|
||||
|
||||
# sonar
|
||||
.scannerwork/
|
||||
sonar/
|
||||
build_wrapper_output_directory/
|
||||
|
||||
# other build files
|
||||
dump_entities.csv
|
||||
dump_entities.xls*
|
||||
|
||||
75
CHANGELOG.md
@@ -5,6 +5,80 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
# [3.5.0]
|
||||
|
||||
## **IMPORTANT! BREAKING CHANGES**
|
||||
|
||||
- When upgrading to v3.5 for the first time from v3.4 on a BBQKees Gateway board you will need to use the [EMS-EPS Flasher](https://github.com/emsesp/EMS-ESP-Flasher/releases) to correctly re-partition the flash. Make sure you backup the settings and customizations from the WebUI (System->Upload/Download) and restore after the upgrade.
|
||||
- Support for multiple EMS-ESPs [#759] has been added as an optional setting for MQTT. When enabled, which is now the default, all MQTT Discovery Entity IDs will include the MQTT base name and the shortname of the EMS-ESP device entity. For example what was previously `sensor.boiler_actual_boiler_temperature` will now become `sensor.ems_esp_boiler_boiltemp`. If you still want to use the old format and retain the history and script compatibility in Home Assistant then set this back to the old format.
|
||||
|
||||
## Added
|
||||
|
||||
- Translations in Web UI and all device entity names (DE, NL, SV, PL, NO, FR) [#22](https://github.com/emsesp/EMS-ESP32/issues/22)
|
||||
- Add support for Lolin C3 mini [#620](https://github.com/emsesp/EMS-ESP32/pull/620)
|
||||
- Add support for ESP32-S2 [#667](https://github.com/emsesp/EMS-ESP32/pull/667)
|
||||
- Add devices: Greenstar 30Ri boiler, Junkers FW500 thermostat, Buderus BC30 controller
|
||||
- Add program memory info
|
||||
- Add mqtt queue and connection infos
|
||||
- Adapt min/max if ems-value is not in this range
|
||||
- Add heat pump settings for inputs and limits [#600](https://github.com/emsesp/EMS-ESP32/issues/600)
|
||||
- Add hybrid heatpump [#500](https://github.com/emsesp/EMS-ESP32/issues/500)
|
||||
- Add translated tags
|
||||
- Add min/max to customization table [#686](https://github.com/emsesp/EMS-ESP32/issues/686)
|
||||
- Add MD5 check [#637](https://github.com/emsesp/EMS-ESP32/issues/637)
|
||||
- Add more bus-ids [#673](https://github.com/emsesp/EMS-ESP32/issues/673)
|
||||
- Use HA connectivity device class for Status, added boot time [#751](https://github.com/emsesp/EMS-ESP32/issues/751)
|
||||
- Add commands for analog sensors outputs
|
||||
- Support for multiple EMS-ESPs with MQTT and HA [[#759](https://github.com/emsesp/EMS-ESP32/issues/759)]
|
||||
- Settings for heatpump silent mode and additional heater [[#802](https://github.com/emsesp/EMS-ESP32/issues/802)] [[#803](https://github.com/emsesp/EMS-ESP32/issues/803)]
|
||||
- Zone module MZ100 [#826](https://github.com/emsesp/EMS-ESP32/issues/826)
|
||||
- Default MQTT hostname is blank [#829](https://github.com/emsesp/EMS-ESP32/issues/829)
|
||||
- wwCurFlow for ems+ devices [#829](https://github.com/emsesp/EMS-ESP32/issues/829)
|
||||
- Add Rego 3000, TR120RF thermostats [#917](https://github.com/emsesp/EMS-ESP32/issues/917)
|
||||
- Add config for ESP32-S3
|
||||
- Add heatpump silent mode and other entities [#896](https://github.com/emsesp/EMS-ESP32/issues/896)
|
||||
- Allow reboot to other partition (factory or asymetric OTA)
|
||||
- Blacklist entities to remove from memory [#891](https://github.com/emsesp/EMS-ESP32/issues/891)
|
||||
- Add boiler pump operating mode [#944](https://github.com/emsesp/EMS-ESP32/issues/944)
|
||||
|
||||
## Fixed
|
||||
|
||||
- Factory Reset not working [#628](https://github.com/emsesp/EMS-ESP32/issues/628)
|
||||
- Valid 4 byte values [#820](https://github.com/emsesp/EMS-ESP32/issues/820)
|
||||
- Commands for multiple thermostats [#826](https://github.com/emsesp/EMS-ESP32/issues/826)
|
||||
- API queries for multiple devices [#865](https://github.com/emsesp/EMS-ESP32/issues/865)
|
||||
- Console crash when using call with command `hcx` only. [#841](https://github.com/emsesp/EMS-ESP32/issues/841)
|
||||
- `heatingPump2Mod` was wrong, changed to absBurnPow [[#908](https://github.com/emsesp/EMS-ESP32/issues/908)
|
||||
- Rounding of web input values
|
||||
- Analog sensor with single gpio number [#915](https://github.com/emsesp/EMS-ESP32/issues/915)
|
||||
- HA dallas and analog configs: remove/rebuild on change [#888](https://github.com/emsesp/EMS-ESP32/issues/888)
|
||||
- Modes and set seltemp for RC30 and RC20 [#932](https://github.com/emsesp/EMS-ESP32/issues/932)
|
||||
|
||||
## Changed
|
||||
|
||||
- Discovery in HomeAssistant don't work with custom base topic. [#596](https://github.com/emsesp/EMS-ESP32/issues/596) Base topic containing `/` are changed to `_`
|
||||
- RF room temperature sensor are shown as thermostat
|
||||
- Render mqtt float json values with trailing zero
|
||||
- Removed flash strings, to increase available heap memory
|
||||
- Reload page after restart button is pressed
|
||||
- Analog/dallas values command as list like ems-devices
|
||||
- Analog/dallas HA-entities based on id
|
||||
- MQTT Base is a mandatory field. Removed MQTT topic length from settings
|
||||
- HA duration class for time entities [[#822](https://github.com/emsesp/EMS-ESP32/issues/822)
|
||||
- AM200 alternative heatsource as class heatsource [[#857](https://github.com/emsesp/EMS-ESP32/issues/857)
|
||||
|
||||
# [3.4.4]
|
||||
|
||||
## Fixed
|
||||
|
||||
- Fix for new installations with filesystem not initializing
|
||||
|
||||
# [3.4.3]
|
||||
|
||||
## Fixed
|
||||
|
||||
- Fix for new installations with filesystem not initializing
|
||||
|
||||
# [3.4.2]
|
||||
|
||||
## Added
|
||||
@@ -29,7 +103,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
## Changed
|
||||
|
||||
- Shorten "friendly names" in Home Assistant [#555](https://github.com/emsesp/EMS-ESP32/issues/555)
|
||||
|
||||
- platformio 2.3.0 (IDF 4, Arduino 2)
|
||||
- remove master-thermostat, support multiple thermostats
|
||||
- merge up- and download in webui [#577](https://github.com/emsesp/EMS-ESP32/issues/577)
|
||||
|
||||
19
Makefile
@@ -1,7 +1,7 @@
|
||||
#
|
||||
# GNUMakefile for EMS-ESP
|
||||
# (c) 2020 Paul Derbyshire
|
||||
#
|
||||
|
||||
NUMJOBS=${NUMJOBS:-" -j4 "}
|
||||
MAKEFLAGS+="j "
|
||||
#----------------------------------------------------------------------
|
||||
@@ -17,23 +17,30 @@ MAKEFLAGS+="j "
|
||||
#TARGET := $(notdir $(CURDIR))
|
||||
TARGET := emsesp
|
||||
BUILD := build
|
||||
SOURCES := src src/* lib_standalone lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src src/devices lib/ArduinoJson/src lib/PButton
|
||||
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
|
||||
SOURCES := src src/* lib_standalone lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src src/devices lib/ArduinoJson/src lib/PButton lib/semver
|
||||
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/semver lib/* src/devices
|
||||
LIBRARIES :=
|
||||
|
||||
CPPCHECK = cppcheck
|
||||
# CHECKFLAGS = -q --force --std=c++17
|
||||
CHECKFLAGS = -q --force --std=c++11
|
||||
|
||||
#----------------------------------------------------------------------
|
||||
# Languages Standard
|
||||
#----------------------------------------------------------------------
|
||||
# C_STANDARD := -std=c17
|
||||
# CXX_STANDARD := -std=c++17
|
||||
C_STANDARD := -std=c11
|
||||
CXX_STANDARD := -std=c++11
|
||||
|
||||
#----------------------------------------------------------------------
|
||||
# Defined Symbols
|
||||
#----------------------------------------------------------------------
|
||||
DEFINES += -DFACTORY_WIFI_HOSTNAME=\"ems-esp\" -DARDUINOJSON_ENABLE_STD_STRING=1 -DARDUINOJSON_ENABLE_PROGMEM=1 -DARDUINOJSON_ENABLE_ARDUINO_STRING -DARDUINOJSON_USE_DOUBLE=0 -DEMSESP_DEBUG -DEMSESP_STANDALONE -DEMSESP_USE_SERIAL -DEMSESP_DEFAULT_BOARD_PROFILE=\"LOLIN\"
|
||||
DEFINES += -DARDUINOJSON_ENABLE_STD_STRING=1 -DARDUINOJSON_ENABLE_PROGMEM=1 -DARDUINOJSON_ENABLE_ARDUINO_STRING -DARDUINOJSON_USE_DOUBLE=0
|
||||
DEFINES += -DEMSESP_DEBUG -DEMSESP_STANDALONE -DEMSESP_USE_SERIAL
|
||||
DEFINES += $(ARGS)
|
||||
|
||||
DEFAULTS = -DEMSESP_DEFAULT_LOCALE=\"en\" -DEMSESP_DEFAULT_TX_MODE=8 -DEMSESP_DEFAULT_VERSION=\"3.5.0b11\" -DEMSESP_DEFAULT_BOARD_PROFILE=\"S32\"
|
||||
|
||||
#----------------------------------------------------------------------
|
||||
# Sources & Files
|
||||
@@ -66,7 +73,7 @@ CXX := /usr/bin/g++
|
||||
# CXXFLAGS C++ Compiler Flags
|
||||
# LDFLAGS Linker Flags
|
||||
#----------------------------------------------------------------------
|
||||
CPPFLAGS += $(DEFINES) $(INCLUDE)
|
||||
CPPFLAGS += $(DEFINES) $(DEFAULTS) $(INCLUDE)
|
||||
CPPFLAGS += -ggdb
|
||||
CPPFLAGS += -g3
|
||||
CPPFLAGS += -Os
|
||||
@@ -114,6 +121,8 @@ COMPILE.cpp = $(CXX) $(CXX_STANDARD) $(CXXFLAGS) $(DEPFLAGS) -c $< -o $@
|
||||
# Targets
|
||||
#----------------------------------------------------------------------
|
||||
.PHONY: all
|
||||
.SILENT: $(OUTPUT)
|
||||
|
||||
all: $(OUTPUT)
|
||||
|
||||
$(OUTPUT): $(OBJS)
|
||||
|
||||
131
README.md
@@ -1,17 +1,5 @@
|
||||
# 
|
||||
|
||||
**EMS-ESP** is an open-source firmware for the Espressif ESP8266 and ESP32 microcontroller that communicates with **EMS** (Energy Management System) based equipment from manufacturers like Bosch, Buderus, Nefit, Junkers, Worcester and Sieger.
|
||||
|
||||
This project is the specifically for the ESP32. Compared with the previous ESP8266 (version 2) release it has the following enhancements:
|
||||
|
||||
- Ethernet Support
|
||||
- Pre-configured circuit board layouts
|
||||
- Supports writing EMS values directly from within Web UI
|
||||
- Mock API server for faster offline development and testing
|
||||
- Improved API and MQTT commands
|
||||
- Improvements to Dallas temperature sensors
|
||||
- Embedded log tracing in the Web UI
|
||||
|
||||
[](https://github.com/emsesp/EMS-ESP32/blob/main/CHANGELOG.md)
|
||||
[](https://github.com/emsesp/EMS-ESP32/commits/main)
|
||||
[](LICENSE)
|
||||
@@ -20,34 +8,54 @@ This project is the specifically for the ESP32. Compared with the previous ESP82
|
||||
[](https://github.com/emsesp/EMS-ESP32/releases)
|
||||
[](https://discord.gg/3J3GgnzpyT)
|
||||
|
||||
If you like **EMS-ESP**, please give it a star, or fork it and contribute!
|
||||
|
||||
[](https://github.com/emsesp/EMS-ESP32/stargazers)
|
||||
[](https://github.com/emsesp/EMS-ES32P/network)
|
||||
[](https://www.paypal.com/paypalme/prderbyshire/2)
|
||||
|
||||
Note, EMS-ESP requires a small hardware circuit that can convert the EMS bus data to be read by the microcontroller. These can be ordered at <https://bbqkees-electronics.nl> or contact the contributors that can provide the schematic and designs.
|
||||
**EMS-ESP** is an open-source firmware for the Espressif ESP32 microcontroller that communicates with **EMS** (Energy Management System) based equipment from manufacturers like Bosch, Buderus, Nefit, Junkers, Worcester and Sieger. It requires a small gateway circuit to interface with the EMS bus which can be purchased from <https://bbqkees-electronics.nl> or custom built.
|
||||
|
||||
<img src="media/gateway-integration.jpg" width=40%>
|
||||
## **Features**
|
||||
|
||||
---
|
||||
|
||||
# **Features**
|
||||
|
||||
- A multi-user secure web interface to change settings and monitor incoming data
|
||||
- A multi-user, multi-language secure web interface to change settings and monitor incoming data
|
||||
- A console, accessible via Serial and Telnet for more advanced monitoring
|
||||
- Native support for Home Assistant and Domoticz via [MQTT Discovery](https://www.home-assistant.io/docs/mqtt/discovery/)
|
||||
- Native support for Home Assistant, Domoticz and openHAB via [MQTT Discovery](https://www.home-assistant.io/docs/mqtt/discovery/)
|
||||
- Can run standalone as an independent WiFi Access Point or join an existing WiFi network
|
||||
- Easy first-time configuration via a web Captive Portal
|
||||
- Support for more than [100 EMS devices](https://emsesp.github.io/docs/#/Supported-EMS-Devices) (boilers, thermostats, solar modules, mixer modules, heat pumps, gateways)
|
||||
- Support for more than [110 EMS devices](https://emsesp.github.io/docs/#/Supported-EMS-Devices) (boilers, thermostats, solar modules, mixer modules, heat pumps, gateways, switches, heat sources)
|
||||
|
||||
## **Documentation**
|
||||
|
||||
For the complete documentation on how to install, configure and get support visit the [EMS-ESP Wiki](https://emsesp.github.io/docs).
|
||||
|
||||
## **Support**
|
||||
|
||||
To chat with the community reach out on our [Discord Server](https://discord.gg/3J3GgnzpyT).
|
||||
|
||||
If you like **EMS-ESP**, please give it a star, or fork it and contribute or offer a small donation!
|
||||
|
||||
## **Demo**
|
||||
|
||||
See a demo [here](https://ems-esp.derbyshire.nl). Log in with any username/password.
|
||||
For a live demo of the Web UI click [here](https://ems-esp.derbyshire.nl) and log in with any username/password.
|
||||
|
||||
# **Screenshots**
|
||||
## **Contributors ✨**
|
||||
|
||||
## Web Interface
|
||||
EMS-ESP is a project owned and maintained by [proddy](https://github.com/proddy) and [MichaelDvP](https://github.com/MichaelDvP).
|
||||
|
||||
## **Libraries used**
|
||||
|
||||
- [esp8266-react](https://github.com/rjwats/esp8266-react) by @rjwats for the framework that provides the core of the Web UI
|
||||
- [uuid-\*](https://github.com/nomis/mcu-uuid-console) from @nomis. The console, syslog, telnet and logging are based off these open source libraries
|
||||
- [ArduinoJson](https://github.com/bblanchon/ArduinoJson) for all the JSON
|
||||
- [AsyncMqttClient](https://github.com/marvinroger/async-mqtt-client) for the MQTT client, with custom modifications from @bertmelis and @proddy
|
||||
- ESPAsyncWebServer and AsyncTCP for the Web server and TCP backends, with custom modifications for performance
|
||||
|
||||
## **License**
|
||||
|
||||
This program is licensed under GPL-3.0
|
||||
|
||||
## **Screenshots**
|
||||
|
||||
### Web Interface
|
||||
|
||||
| | |
|
||||
| ---------------------------------- | -------------------------------- |
|
||||
@@ -55,75 +63,10 @@ See a demo [here](https://ems-esp.derbyshire.nl). Log in with any username/passw
|
||||
| <img src="media/web_devices.png"> | <img src="media/web_mqtt.png"> |
|
||||
| <img src="media/web_edit.png"> | <img src="media/web_log.png"> |
|
||||
|
||||
## Telnet Console
|
||||
### Telnet Console
|
||||
|
||||
<img src="media/console.png" width=80% height=80%>
|
||||
<img src="media/console0.png" width=80% height=80%>
|
||||
|
||||
## In Home Assistant
|
||||
### 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.
|
||||
|
||||
You can choose to use an pre-built firmware image or compile the code yourself:
|
||||
|
||||
- [Uploading a pre-built firmware build](https://emsesp.github.io/docs/#/Uploading-firmware)
|
||||
- [Building the firmware from source code and flashing manually](https://emsesp.github.io/docs/#/Building-firmware)
|
||||
|
||||
# **Support Information**
|
||||
|
||||
If you're looking for support on **EMS-ESP** there are some options available:
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Official EMS-ESP Documentation](https://emsesp.github.io/docs): For information on how to build and upload the firmware
|
||||
- [FAQ and Troubleshooting](https://emsesp.github.io/docs/#/Troubleshooting): For information on common problems and solutions. See also [BBQKees's wiki](https://bbqkees-electronics.nl/wiki/gateway/troubleshooting.html)
|
||||
|
||||
## Support Community
|
||||
|
||||
- [Discord Server](https://discord.gg/3J3GgnzpyT): For support, troubleshooting and general questions. You have better chances to get fast answers from members of the community
|
||||
- [Search in Issues](https://github.com/emsesp/EMS-ESP32/issues): You might find an answer to your question by searching current or closed issues
|
||||
|
||||
## Developer's Community
|
||||
|
||||
- [Bug Report](https://github.com/emsesp/EMS-ESP32/issues/new?template=bug_report.md): For reporting Bugs
|
||||
- [Feature Request](https://github.com/emsesp/EMS-ESP32/issues/new?template=feature_request.md): For requesting features/functions
|
||||
- [Troubleshooting](https://github.com/emsesp/EMS-ESP32/issues/new?template=questions---troubleshooting.md): As a last resort, you can open new _Troubleshooting & Question_ issue on GitHub if the solution could not be found using the other channels. Just remember: the more info you provide the more chances you'll have to get an accurate answer
|
||||
|
||||
# **Contributors ✨**
|
||||
|
||||
EMS-ESP is a project originally created and owned by [proddy](https://github.com/proddy). Key contributors are:
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
<!-- markdownlint-disable -->
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/MichaelDvP"><img src="https://avatars.githubusercontent.com/u/59284019?v=3?s=100" width="100px;" alt=""/><br /><sub><b>MichaelDvP</b></sub></a><br /></a> <a href="https://github.com/emsesp/EMS-ESP/commits?author=MichaelDvP" title="v2 Commits">v2</a>
|
||||
<a href="https://github.com/emsesp/EMS-ESP32/commits?author=MichaelDvP" title="v3 Commits">v3</a>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
<!-- markdownlint-restore -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
You can also contribute to EMS-ESP by
|
||||
|
||||
- providing Pull Requests (Features, Fixes, suggestions)
|
||||
- testing new released features and report issues on your EMS equipment
|
||||
- contributing to missing [Documentation](https://emsesp.github.io/docs)
|
||||
|
||||
# **Libraries used**
|
||||
|
||||
- [esp8266-react](https://github.com/rjwats/esp8266-react) by @rjwats for the framework that provides the core of the Web UI
|
||||
- [uuid-\*](https://github.com/nomis/mcu-uuid-console) from @nomis. The console, syslog, telnet and logging are based off these open source libraries
|
||||
- [ArduinoJson](https://github.com/bblanchon/ArduinoJson) for JSON
|
||||
- [AsyncMqttClient](https://github.com/marvinroger/async-mqtt-client) for the MQTT client, with custom modifications from @bertmelis and @proddy
|
||||
- ESPAsyncWebServer and AsyncTCP for the Web server and TCP backends, with custom modifications for performance
|
||||
|
||||
# **License**
|
||||
|
||||
This program is licensed under GPL-3.0
|
||||
|
||||
6
esp32_partition_16M.csv
Normal file
@@ -0,0 +1,6 @@
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
nvs, data, nvs, 0x9000, 0x5000,
|
||||
otadata, data, ota, , 0x2000,
|
||||
app0, app, ota_0, , 0x7F0000,
|
||||
app1, app, ota_1, , 0x7F0000,
|
||||
spiffs, data, spiffs, , 64K,
|
||||
|
6
esp32_partition_4M.csv
Normal file
@@ -0,0 +1,6 @@
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
nvs, data, nvs, 0x9000, 0x5000,
|
||||
otadata, data, ota, , 0x2000,
|
||||
app0, app, ota_0, , 0x1F0000,
|
||||
app1, app, ota_1, , 0x1F0000,
|
||||
spiffs, data, spiffs, , 64K,
|
||||
|
@@ -1,6 +0,0 @@
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
nvs, data, nvs, 0x9000, 0x5000,
|
||||
otadata, data, ota, 0xE000, 0x2000,
|
||||
app0, app, ota_0, 0x10000, 0x1F0000,
|
||||
app1, app, ota_1, 0x200000, 0x1F0000,
|
||||
spiffs, data, spiffs, 0x3F0000, 0x10000,
|
||||
|
@@ -7,8 +7,8 @@ build_flags =
|
||||
|
||||
; Access point settings
|
||||
-D FACTORY_AP_PROVISION_MODE=AP_MODE_DISCONNECTED
|
||||
-D FACTORY_AP_SSID=\"ems-esp\" ; 1-64 characters
|
||||
-D FACTORY_AP_PASSWORD=\"ems-esp-neo\" ; 8-64 characters
|
||||
-D FACTORY_AP_SSID=\"ems-esp\"
|
||||
-D FACTORY_AP_PASSWORD=\"ems-esp-neo\"
|
||||
-D FACTORY_AP_LOCAL_IP=\"192.168.4.1\"
|
||||
-D FACTORY_AP_GATEWAY_IP=\"192.168.4.1\"
|
||||
-D FACTORY_AP_SUBNET_MASK=\"255.255.255.0\"
|
||||
@@ -28,11 +28,11 @@ build_flags =
|
||||
; OTA settings
|
||||
-D FACTORY_OTA_PORT=8266
|
||||
-D FACTORY_OTA_PASSWORD=\"ems-esp-neo\"
|
||||
-D FACTORY_OTA_ENABLED=true
|
||||
-D FACTORY_OTA_ENABLED=false
|
||||
|
||||
; MQTT settings
|
||||
-D FACTORY_MQTT_ENABLED=false
|
||||
-D FACTORY_MQTT_HOST=\"test.mosquitto.org\"
|
||||
-D FACTORY_MQTT_HOST=\"\"
|
||||
-D FACTORY_MQTT_PORT=1883
|
||||
-D FACTORY_MQTT_USERNAME=\"\"
|
||||
-D FACTORY_MQTT_PASSWORD=\"\"
|
||||
|
||||
5
interface/.typesafe-i18n.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"adapter": "react",
|
||||
"baseLocale": "pl",
|
||||
"$schema": "https://unpkg.com/typesafe-i18n@5.24.0/schema/typesafe-i18n.json"
|
||||
}
|
||||
15257
interface/package-lock.json
generated
@@ -1,36 +1,35 @@
|
||||
{
|
||||
"name": "EMS-ESP",
|
||||
"version": "3.4.0",
|
||||
"version": "3.5.0",
|
||||
"private": true,
|
||||
"proxy": "http://localhost:3080",
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.10.4",
|
||||
"@emotion/styled": "^11.10.4",
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"@msgpack/msgpack": "^2.8.0",
|
||||
"@mui/icons-material": "^5.10.3",
|
||||
"@mui/material": "^5.10.5",
|
||||
"@table-library/react-table-library": "4.0.18",
|
||||
"@types/lodash": "^4.14.185",
|
||||
"@types/node": "^18.7.18",
|
||||
"@types/react": "^18.0.20",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@mui/icons-material": "^5.11.0",
|
||||
"@mui/material": "^5.11.7",
|
||||
"@table-library/react-table-library": "4.0.24",
|
||||
"@types/lodash": "^4.14.191",
|
||||
"@types/node": "^18.11.19",
|
||||
"@types/react": "^18.0.27",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"async-validator": "^4.2.5",
|
||||
"axios": "^0.27.2",
|
||||
"http-proxy-middleware": "^2.0.6",
|
||||
"axios": "^1.3.2",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"lodash": "^4.17.21",
|
||||
"notistack": "^2.0.5",
|
||||
"parse-ms": "^3.0.0",
|
||||
"notistack": "^2.0.8",
|
||||
"react": "^18.2.0",
|
||||
"react-app-rewired": "^2.2.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropzone": "^14.2.2",
|
||||
"react-icons": "^4.4.0",
|
||||
"react-router-dom": "^6.4.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-icons": "^4.7.1",
|
||||
"react-router-dom": "^6.8.1",
|
||||
"react-scripts": "5.0.1",
|
||||
"sockette": "^2.0.6",
|
||||
"typescript": "^4.8.3"
|
||||
"typesafe-i18n": "^5.24.0",
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-app-rewired start",
|
||||
@@ -41,8 +40,9 @@
|
||||
"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"
|
||||
"standalone": "npm-run-all -p start typesafe-i18n mock-api",
|
||||
"lint": "eslint . --ext .ts,.tsx",
|
||||
"typesafe-i18n": "typesafe-i18n"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
@@ -78,7 +78,7 @@
|
||||
"max-len": [
|
||||
1,
|
||||
{
|
||||
"code": 200
|
||||
"code": 220
|
||||
}
|
||||
],
|
||||
"arrow-parens": 1
|
||||
@@ -98,6 +98,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^2.0.20",
|
||||
"npm-run-all": "^4.1.5"
|
||||
"npm-run-all": "^4.1.5",
|
||||
"http-proxy-middleware": "^2.0.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,9 @@
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Roboto'), local('Roboto-Regular'), 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+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
unicode-range: U+0000-00FF, U+0104-0107, U+0118-0119, U+0131, U+0141-0144, U+0152-0153, U+015A-015B, U+0179-017C,
|
||||
U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF,
|
||||
U+FFFD;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
@@ -19,6 +20,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src: local('Roboto Medium'), local('Roboto-Medium'), url(../fonts/md.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC,
|
||||
U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
unicode-range: U+0000-00FF, U+0104-0107, U+0118-0119, U+0131, U+0141-0144, U+0152-0153, U+015A-015B, U+0179-017C,
|
||||
U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF,
|
||||
U+FFFD;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FC, createRef, createContext, useContext, RefObject } from 'react';
|
||||
import { FC, createRef, createContext, useContext, useEffect, useState, RefObject } from 'react';
|
||||
import { SnackbarProvider } from 'notistack';
|
||||
|
||||
import { IconButton } from '@mui/material';
|
||||
@@ -9,6 +9,13 @@ import { FeaturesLoader } from './contexts/features';
|
||||
import CustomTheme from './CustomTheme';
|
||||
import AppRouting from './AppRouting';
|
||||
|
||||
import { localStorageDetector } from 'typesafe-i18n/detectors';
|
||||
import TypesafeI18n from './i18n/i18n-react';
|
||||
import { detectLocale } from './i18n/i18n-util';
|
||||
import { loadLocaleAsync } from './i18n/i18n-util.async';
|
||||
|
||||
const detectedLocale = detectLocale(localStorageDetector);
|
||||
|
||||
const App: FC = () => {
|
||||
const notistackRef: RefObject<any> = createRef();
|
||||
|
||||
@@ -20,24 +27,34 @@ const App: FC = () => {
|
||||
|
||||
const colorMode = useContext(ColorModeContext);
|
||||
|
||||
const [wasLoaded, setWasLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadLocaleAsync(detectedLocale).then(() => setWasLoaded(true));
|
||||
}, []);
|
||||
|
||||
if (!wasLoaded) return null;
|
||||
|
||||
return (
|
||||
<ColorModeContext.Provider value={colorMode}>
|
||||
<CustomTheme>
|
||||
<SnackbarProvider
|
||||
maxSnack={3}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
||||
ref={notistackRef}
|
||||
action={(key) => (
|
||||
<IconButton onClick={onClickDismiss(key)} size="small">
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
>
|
||||
<FeaturesLoader>
|
||||
<AppRouting />
|
||||
</FeaturesLoader>
|
||||
</SnackbarProvider>
|
||||
</CustomTheme>
|
||||
<TypesafeI18n locale={detectedLocale}>
|
||||
<CustomTheme>
|
||||
<SnackbarProvider
|
||||
maxSnack={3}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
||||
ref={notistackRef}
|
||||
action={(key) => (
|
||||
<IconButton onClick={onClickDismiss(key)} size="small">
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
>
|
||||
<FeaturesLoader>
|
||||
<AppRouting />
|
||||
</FeaturesLoader>
|
||||
</SnackbarProvider>
|
||||
</CustomTheme>
|
||||
</TypesafeI18n>
|
||||
</ColorModeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,6 +2,8 @@ import { FC, useContext, useEffect } from 'react';
|
||||
import { Navigate, Routes, Route, useLocation } from 'react-router-dom';
|
||||
import { useSnackbar, VariantType } from 'notistack';
|
||||
|
||||
import { useI18nContext } from './i18n/i18n-react';
|
||||
|
||||
import { Authentication, AuthenticationContext } from './contexts/authentication';
|
||||
import { FeaturesContext } from './contexts/features';
|
||||
import { RequireAuthenticated, RequireUnauthenticated } from './components';
|
||||
@@ -41,13 +43,14 @@ export const RemoveTrailingSlashes = () => {
|
||||
|
||||
const AppRouting: FC = () => {
|
||||
const { features } = useContext(FeaturesContext);
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
return (
|
||||
<Authentication>
|
||||
<RemoveTrailingSlashes />
|
||||
<Routes>
|
||||
<Route path="/unauthorized" element={<RootRedirect message="Please sign in to continue" signOut />} />
|
||||
<Route path="/fileUpdated" element={<RootRedirect message="Upload successful" variant="success" />} />
|
||||
<Route path="/unauthorized" element={<RootRedirect message={LL.PLEASE_SIGNIN()} signOut />} />
|
||||
<Route path="/fileUpdated" element={<RootRedirect message={LL.UPLOAD_SUCCESSFUL()} variant="success" />} />
|
||||
{features.security && (
|
||||
<Route
|
||||
path="/"
|
||||
|
||||
@@ -2,20 +2,30 @@ import { FC, useContext, useState } from 'react';
|
||||
import { ValidateFieldsError } from 'async-validator';
|
||||
import { useSnackbar } from 'notistack';
|
||||
|
||||
import { Box, Fab, Paper, Typography } from '@mui/material';
|
||||
import { Box, Fab, Paper, Typography, Button } from '@mui/material';
|
||||
import ForwardIcon from '@mui/icons-material/Forward';
|
||||
|
||||
import * as AuthenticationApi from './api/authentication';
|
||||
import { PROJECT_NAME } from './api/env';
|
||||
import { AuthenticationContext } from './contexts/authentication';
|
||||
|
||||
import { AxiosError } from 'axios';
|
||||
|
||||
import { extractErrorMessage, onEnterCallback, updateValue } from './utils';
|
||||
import { SignInRequest } from './types';
|
||||
import { ValidatedTextField } from './components';
|
||||
import { SIGN_IN_REQUEST_VALIDATOR, validate } from './validators';
|
||||
|
||||
import { I18nContext } from './i18n/i18n-react';
|
||||
import type { Locales } from './i18n/i18n-types';
|
||||
import { loadLocaleAsync } from './i18n/i18n-util.async';
|
||||
|
||||
import { ReactComponent as NLflag } from './i18n/NL.svg';
|
||||
import { ReactComponent as DEflag } from './i18n/DE.svg';
|
||||
import { ReactComponent as GBflag } from './i18n/GB.svg';
|
||||
import { ReactComponent as SVflag } from './i18n/SV.svg';
|
||||
import { ReactComponent as PLflag } from './i18n/PL.svg';
|
||||
import { ReactComponent as NOflag } from './i18n/NO.svg';
|
||||
import { ReactComponent as FRflag } from './i18n/FR.svg';
|
||||
|
||||
const SignIn: FC = () => {
|
||||
const authenticationContext = useContext(AuthenticationContext);
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
@@ -31,6 +41,9 @@ const SignIn: FC = () => {
|
||||
|
||||
const validateAndSignIn = async () => {
|
||||
setProcessing(true);
|
||||
SIGN_IN_REQUEST_VALIDATOR.messages({
|
||||
required: LL.IS_REQUIRED('%s')
|
||||
});
|
||||
try {
|
||||
await validate(SIGN_IN_REQUEST_VALIDATOR, signInRequest);
|
||||
signIn();
|
||||
@@ -44,13 +57,13 @@ const SignIn: FC = () => {
|
||||
try {
|
||||
const { data: loginResponse } = await AuthenticationApi.signIn(signInRequest);
|
||||
authenticationContext.signIn(loginResponse.access_token);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
} catch (error) {
|
||||
if (error.response) {
|
||||
if (error.response?.status === 401) {
|
||||
enqueueSnackbar('Invalid login details', { variant: 'warning' });
|
||||
enqueueSnackbar(LL.INVALID_LOGIN(), { variant: 'warning' });
|
||||
}
|
||||
} else {
|
||||
enqueueSnackbar(extractErrorMessage(error, 'Unexpected error, please try again'), { variant: 'error' });
|
||||
enqueueSnackbar(extractErrorMessage(error, LL.ERROR()), { variant: 'error' });
|
||||
}
|
||||
setProcessing(false);
|
||||
}
|
||||
@@ -58,6 +71,14 @@ const SignIn: FC = () => {
|
||||
|
||||
const submitOnEnter = onEnterCallback(signIn);
|
||||
|
||||
const { LL, setLocale, locale } = useContext(I18nContext);
|
||||
|
||||
const selectLocale = async (loc: Locales) => {
|
||||
localStorage.setItem('lang', loc);
|
||||
await loadLocaleAsync(loc);
|
||||
setLocale(loc);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
display="flex"
|
||||
@@ -81,11 +102,49 @@ const SignIn: FC = () => {
|
||||
})}
|
||||
>
|
||||
<Typography variant="h4">{PROJECT_NAME}</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
'& button, & a, & .MuiCard-root': {
|
||||
mt: 0.5,
|
||||
mx: 0.5
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button size="small" variant={locale === 'en' ? 'contained' : 'outlined'} onClick={() => selectLocale('en')}>
|
||||
<GBflag style={{ width: 24 }} />
|
||||
EN
|
||||
</Button>
|
||||
<Button size="small" variant={locale === 'de' ? 'contained' : 'outlined'} onClick={() => selectLocale('de')}>
|
||||
<DEflag style={{ width: 24 }} />
|
||||
DE
|
||||
</Button>
|
||||
<Button size="small" variant={locale === 'fr' ? 'contained' : 'outlined'} onClick={() => selectLocale('fr')}>
|
||||
<FRflag style={{ width: 24 }} />
|
||||
FR
|
||||
</Button>
|
||||
<Button size="small" variant={locale === 'nl' ? 'contained' : 'outlined'} onClick={() => selectLocale('nl')}>
|
||||
<NLflag style={{ width: 24 }} />
|
||||
NL
|
||||
</Button>
|
||||
<Button size="small" variant={locale === 'no' ? 'contained' : 'outlined'} onClick={() => selectLocale('no')}>
|
||||
<NOflag style={{ width: 24 }} />
|
||||
NO
|
||||
</Button>
|
||||
<Button size="small" variant={locale === 'pl' ? 'contained' : 'outlined'} onClick={() => selectLocale('pl')}>
|
||||
<PLflag style={{ width: 24 }} />
|
||||
PL
|
||||
</Button>
|
||||
<Button size="small" variant={locale === 'sv' ? 'contained' : 'outlined'} onClick={() => selectLocale('sv')}>
|
||||
<SVflag style={{ width: 24 }} />
|
||||
SV
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
disabled={processing}
|
||||
name="username"
|
||||
label="Username"
|
||||
label={LL.USERNAME(0)}
|
||||
value={signInRequest.username}
|
||||
onChange={updateLoginRequestValue}
|
||||
margin="normal"
|
||||
@@ -97,7 +156,7 @@ const SignIn: FC = () => {
|
||||
disabled={processing}
|
||||
type="password"
|
||||
name="password"
|
||||
label="Password"
|
||||
label={LL.PASSWORD()}
|
||||
value={signInRequest.password}
|
||||
onChange={updateLoginRequestValue}
|
||||
onKeyDown={submitOnEnter}
|
||||
@@ -107,7 +166,7 @@ const SignIn: FC = () => {
|
||||
/>
|
||||
<Fab variant="extended" color="primary" sx={{ mt: 2 }} onClick={validateAndSignIn} disabled={processing}>
|
||||
<ForwardIcon sx={{ mr: 1 }} />
|
||||
Sign In
|
||||
{LL.SIGN_IN()}
|
||||
</Fab>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import axios, { AxiosPromise, CancelToken } from 'axios';
|
||||
import axios, { AxiosPromise, CancelToken, AxiosProgressEvent } from 'axios';
|
||||
|
||||
import { decode } from '@msgpack/msgpack';
|
||||
|
||||
@@ -89,7 +89,7 @@ function calculateEventSourceRoot(endpointPath: string) {
|
||||
|
||||
export interface FileUploadConfig {
|
||||
cancelToken?: CancelToken;
|
||||
onUploadProgress?: (progressEvent: ProgressEvent) => void;
|
||||
onUploadProgress?: (progressEvent: AxiosProgressEvent) => void;
|
||||
}
|
||||
|
||||
export const startUploadFile = (url: string, file: File, config?: FileUploadConfig): AxiosPromise<void> => {
|
||||
|
||||
@@ -11,6 +11,6 @@ export function readMqttSettings(): AxiosPromise<MqttSettings> {
|
||||
return AXIOS.get('/mqttSettings');
|
||||
}
|
||||
|
||||
export function updateMqttSettings(ntpSettings: MqttSettings): AxiosPromise<MqttSettings> {
|
||||
return AXIOS.post('/mqttSettings', ntpSettings);
|
||||
export function updateMqttSettings(mqttSettings: MqttSettings): AxiosPromise<MqttSettings> {
|
||||
return AXIOS.post('/mqttSettings', mqttSettings);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,10 @@ export function restart(): AxiosPromise<void> {
|
||||
return AXIOS.post('/restart');
|
||||
}
|
||||
|
||||
export function partition(): AxiosPromise<void> {
|
||||
return AXIOS.post('/partition');
|
||||
}
|
||||
|
||||
export function factoryReset(): AxiosPromise<void> {
|
||||
return AXIOS.post('/factoryReset');
|
||||
}
|
||||
@@ -38,4 +42,3 @@ export function updateLogSettings(logSettings: LogSettings): AxiosPromise<LogSet
|
||||
export function readLogEntries(): AxiosPromise<LogEntries> {
|
||||
return AXIOS_BIN.get('/fetchLog');
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,36 @@
|
||||
import { FC, useState, useContext } from 'react';
|
||||
import { FC, useState, useContext, ChangeEventHandler } from 'react';
|
||||
|
||||
import { Box, Button, Divider, IconButton, Popover, Typography, Avatar, styled, TypographyProps } from '@mui/material';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Divider,
|
||||
IconButton,
|
||||
Popover,
|
||||
Typography,
|
||||
Avatar,
|
||||
styled,
|
||||
TypographyProps,
|
||||
MenuItem,
|
||||
TextField
|
||||
} from '@mui/material';
|
||||
|
||||
import PersonIcon from '@mui/icons-material/Person';
|
||||
import AccountCircleIcon from '@mui/icons-material/AccountCircle';
|
||||
|
||||
import { AuthenticatedContext } from '../../contexts/authentication';
|
||||
|
||||
import { I18nContext } from '../../i18n/i18n-react';
|
||||
import type { Locales } from '../../i18n/i18n-types';
|
||||
import { loadLocaleAsync } from '../../i18n/i18n-util.async';
|
||||
|
||||
import { ReactComponent as NLflag } from '../../i18n/NL.svg';
|
||||
import { ReactComponent as DEflag } from '../../i18n/DE.svg';
|
||||
import { ReactComponent as GBflag } from '../../i18n/GB.svg';
|
||||
import { ReactComponent as SVflag } from '../../i18n/SV.svg';
|
||||
import { ReactComponent as PLflag } from '../../i18n/PL.svg';
|
||||
import { ReactComponent as NOflag } from '../../i18n/NO.svg';
|
||||
import { ReactComponent as FRflag } from '../../i18n/FR.svg';
|
||||
|
||||
const ItemTypography = styled(Typography)<TypographyProps>({
|
||||
maxWidth: '250px',
|
||||
whiteSpace: 'nowrap',
|
||||
@@ -23,6 +47,15 @@ const LayoutAuthMenu: FC = () => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const { locale, LL, setLocale } = useContext(I18nContext);
|
||||
|
||||
const onLocaleSelected: ChangeEventHandler<HTMLInputElement> = async ({ target }) => {
|
||||
const loc = target.value as Locales;
|
||||
localStorage.setItem('lang', loc);
|
||||
await loadLocaleAsync(loc);
|
||||
setLocale(loc);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
@@ -32,7 +65,53 @@ const LayoutAuthMenu: FC = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton id="open-auth-menu" sx={{ padding: 0 }} aria-describedby={id} color="inherit" onClick={handleClick}>
|
||||
<TextField
|
||||
name="locale"
|
||||
InputProps={{ style: { fontSize: 10 } }}
|
||||
variant="outlined"
|
||||
value={locale}
|
||||
onChange={onLocaleSelected}
|
||||
size="small"
|
||||
select
|
||||
>
|
||||
<MenuItem key="en" value="en">
|
||||
<GBflag style={{ width: 16, verticalAlign: 'middle' }} />
|
||||
EN
|
||||
</MenuItem>
|
||||
<Divider />
|
||||
<MenuItem key="de" value="de">
|
||||
<DEflag style={{ width: 16, verticalAlign: 'middle' }} />
|
||||
DE
|
||||
</MenuItem>
|
||||
<MenuItem key="fr" value="fr">
|
||||
<FRflag style={{ width: 16, verticalAlign: 'middle' }} />
|
||||
FR
|
||||
</MenuItem>
|
||||
<MenuItem key="nl" value="nl">
|
||||
<NLflag style={{ width: 16, verticalAlign: 'middle' }} />
|
||||
NL
|
||||
</MenuItem>
|
||||
<MenuItem key="no" value="no">
|
||||
<NOflag style={{ width: 16, verticalAlign: 'middle' }} />
|
||||
NO
|
||||
</MenuItem>
|
||||
<MenuItem key="pl" value="pl">
|
||||
<PLflag style={{ width: 16, verticalAlign: 'middle' }} />
|
||||
PL
|
||||
</MenuItem>
|
||||
<MenuItem key="sv" value="sv">
|
||||
<SVflag style={{ width: 16, verticalAlign: 'middle' }} />
|
||||
SV
|
||||
</MenuItem>
|
||||
</TextField>
|
||||
|
||||
<IconButton
|
||||
id="open-auth-menu"
|
||||
sx={{ ml: 1, padding: 0 }}
|
||||
aria-describedby={id}
|
||||
color="inherit"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<AccountCircleIcon />
|
||||
</IconButton>
|
||||
<Popover
|
||||
@@ -56,13 +135,15 @@ const LayoutAuthMenu: FC = () => {
|
||||
</Avatar>
|
||||
<Box pl={2}>
|
||||
<ItemTypography variant="h6">{me.username}</ItemTypography>
|
||||
<ItemTypography variant="body1">{me.admin ? 'Admin User' : 'Guest User'}</ItemTypography>
|
||||
<ItemTypography variant="body1">
|
||||
{me.admin ? LL.ADMIN() : LL.GUEST()} {LL.USER(2)}
|
||||
</ItemTypography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Divider />
|
||||
<Box p={1.5}>
|
||||
<Button variant="outlined" fullWidth color="primary" onClick={() => signOut(true)}>
|
||||
Sign Out
|
||||
{LL.SIGN_OUT()}
|
||||
</Button>
|
||||
</Box>
|
||||
</Popover>
|
||||
|
||||
@@ -15,9 +15,12 @@ import ProjectMenu from '../../project/ProjectMenu';
|
||||
import LayoutMenuItem from './LayoutMenuItem';
|
||||
import { AuthenticatedContext } from '../../contexts/authentication';
|
||||
|
||||
import { useI18nContext } from '../../i18n/i18n-react';
|
||||
|
||||
const LayoutMenu: FC = () => {
|
||||
const { features } = useContext(FeaturesContext);
|
||||
const authenticatedContext = useContext(AuthenticatedContext);
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -28,12 +31,17 @@ const LayoutMenu: FC = () => {
|
||||
</List>
|
||||
)}
|
||||
<List disablePadding component="nav">
|
||||
<LayoutMenuItem icon={SettingsEthernetIcon} label="Network Connection" to="/network" />
|
||||
<LayoutMenuItem icon={SettingsInputAntennaIcon} label="Access Point" to="/ap" />
|
||||
{features.ntp && <LayoutMenuItem icon={AccessTimeIcon} label="Network Time" to="/ntp" />}
|
||||
<LayoutMenuItem icon={SettingsEthernetIcon} label={LL.NETWORK(0)} to="/network" />
|
||||
<LayoutMenuItem icon={SettingsInputAntennaIcon} label={LL.ACCESS_POINT(0)} to="/ap" />
|
||||
{features.ntp && <LayoutMenuItem icon={AccessTimeIcon} label="NTP" to="/ntp" />}
|
||||
{features.mqtt && <LayoutMenuItem icon={DeviceHubIcon} label="MQTT" to="/mqtt" />}
|
||||
<LayoutMenuItem icon={LockIcon} label="Security" to="/security" disabled={!authenticatedContext.me.admin} />
|
||||
<LayoutMenuItem icon={SettingsIcon} label="System" to="/system" />
|
||||
<LayoutMenuItem
|
||||
icon={LockIcon}
|
||||
label={LL.SECURITY(0)}
|
||||
to="/security"
|
||||
disabled={!authenticatedContext.me.admin}
|
||||
/>
|
||||
<LayoutMenuItem icon={SettingsIcon} label={LL.SYSTEM(0)} to="/system" />
|
||||
</List>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -5,6 +5,8 @@ import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
|
||||
import { MessageBox } from '..';
|
||||
|
||||
import { useI18nContext } from '../../i18n/i18n-react';
|
||||
|
||||
interface FormLoaderProps {
|
||||
message?: string;
|
||||
errorMessage?: string;
|
||||
@@ -12,12 +14,14 @@ interface FormLoaderProps {
|
||||
}
|
||||
|
||||
const FormLoader: FC<FormLoaderProps> = ({ errorMessage, onRetry, message = 'Loading…' }) => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
if (errorMessage) {
|
||||
return (
|
||||
<MessageBox my={2} level="error" message={errorMessage}>
|
||||
{onRetry && (
|
||||
<Button startIcon={<RefreshIcon />} variant="contained" color="error" onClick={onRetry}>
|
||||
Retry
|
||||
{LL.RETRY()}
|
||||
</Button>
|
||||
)}
|
||||
</MessageBox>
|
||||
|
||||
@@ -2,23 +2,29 @@ import { FC } from 'react';
|
||||
|
||||
import { CircularProgress, Box, Typography, Theme } from '@mui/material';
|
||||
|
||||
import { useI18nContext } from '../../i18n/i18n-react';
|
||||
|
||||
interface LoadingSpinnerProps {
|
||||
height?: number | string;
|
||||
}
|
||||
|
||||
const LoadingSpinner: FC<LoadingSpinnerProps> = ({ height = '100%' }) => (
|
||||
<Box display="flex" alignItems="center" justifyContent="center" flexDirection="column" padding={2} height={height}>
|
||||
<CircularProgress
|
||||
sx={(theme: Theme) => ({
|
||||
margin: theme.spacing(4),
|
||||
color: theme.palette.text.secondary
|
||||
})}
|
||||
size={100}
|
||||
/>
|
||||
<Typography variant="h4" color="textSecondary">
|
||||
Loading…
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
const LoadingSpinner: FC<LoadingSpinnerProps> = ({ height = '100%' }) => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
return (
|
||||
<Box display="flex" alignItems="center" justifyContent="center" flexDirection="column" padding={2} height={height}>
|
||||
<CircularProgress
|
||||
sx={(theme: Theme) => ({
|
||||
margin: theme.spacing(4),
|
||||
color: theme.palette.text.secondary
|
||||
})}
|
||||
size={100}
|
||||
/>
|
||||
<Typography variant="h4" color="textSecondary">
|
||||
{LL.LOADING()}…
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoadingSpinner;
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { FC, Fragment } from 'react';
|
||||
import { useDropzone, DropzoneState } from 'react-dropzone';
|
||||
|
||||
import { AxiosProgressEvent } from 'axios';
|
||||
|
||||
import { Box, Button, LinearProgress, Theme, Typography, useTheme } from '@mui/material';
|
||||
|
||||
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
|
||||
const progressPercentage = (progress: ProgressEvent) => Math.round((progress.loaded * 100) / progress.total);
|
||||
import { useI18nContext } from '../../i18n/i18n-react';
|
||||
|
||||
const getBorderColor = (theme: Theme, props: DropzoneState) => {
|
||||
if (props.isDragAccept) {
|
||||
@@ -25,7 +27,7 @@ export interface SingleUploadProps {
|
||||
onDrop: (acceptedFiles: File[]) => void;
|
||||
onCancel: () => void;
|
||||
uploading: boolean;
|
||||
progress?: ProgressEvent;
|
||||
progress?: AxiosProgressEvent;
|
||||
}
|
||||
|
||||
const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, uploading, progress }) => {
|
||||
@@ -33,7 +35,8 @@ const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, uploading, prog
|
||||
onDrop,
|
||||
accept: {
|
||||
'application/octet-stream': ['.bin'],
|
||||
'application/json': ['.json']
|
||||
'application/json': ['.json'],
|
||||
'text/plain': ['.md5']
|
||||
},
|
||||
disabled: uploading,
|
||||
multiple: false
|
||||
@@ -41,14 +44,16 @@ const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, uploading, prog
|
||||
const { getRootProps, getInputProps } = dropzoneState;
|
||||
const theme = useTheme();
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const progressText = () => {
|
||||
if (uploading) {
|
||||
if (progress?.lengthComputable) {
|
||||
return `Uploading: ${progressPercentage(progress)}%`;
|
||||
if (progress?.total) {
|
||||
return LL.UPLOADING() + ': ' + Math.round((progress.loaded * 100) / progress.total) + '%';
|
||||
}
|
||||
return 'Uploading\u2026';
|
||||
return LL.UPLOADING() + `\u2026`;
|
||||
}
|
||||
return 'Drop file or click here';
|
||||
return LL.UPLOAD_DROP_TEXT();
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -60,7 +65,7 @@ const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, uploading, prog
|
||||
borderWidth: 2,
|
||||
borderRadius: 2,
|
||||
borderStyle: 'dashed',
|
||||
color: theme.palette.grey[700],
|
||||
color: theme.palette.grey[400],
|
||||
transition: 'border .24s ease-in-out',
|
||||
width: '100%',
|
||||
cursor: uploading ? 'default' : 'pointer',
|
||||
@@ -76,12 +81,12 @@ const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, uploading, prog
|
||||
<Fragment>
|
||||
<Box width="100%" p={2}>
|
||||
<LinearProgress
|
||||
variant={!progress || progress.lengthComputable ? 'determinate' : 'indeterminate'}
|
||||
value={!progress ? 0 : progress.lengthComputable ? progressPercentage(progress) : 0}
|
||||
variant={!progress || progress.total ? 'determinate' : 'indeterminate'}
|
||||
value={!progress ? 0 : progress.total ? Math.round((progress.loaded * 100) / progress.total) : 0}
|
||||
/>
|
||||
</Box>
|
||||
<Button startIcon={<CancelIcon />} variant="outlined" color="secondary" onClick={onCancel}>
|
||||
Cancel
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
</Fragment>
|
||||
)}
|
||||
|
||||
@@ -1,24 +1,30 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import axios, { AxiosPromise, CancelTokenSource } from 'axios';
|
||||
import axios, { AxiosPromise, CancelTokenSource, AxiosProgressEvent } from 'axios';
|
||||
import { useSnackbar } from 'notistack';
|
||||
|
||||
import { extractErrorMessage } from '../../utils';
|
||||
import { FileUploadConfig } from '../../api/endpoints';
|
||||
|
||||
import { useI18nContext } from '../../i18n/i18n-react';
|
||||
|
||||
interface MediaUploadOptions {
|
||||
upload: (file: File, config?: FileUploadConfig) => AxiosPromise<void>;
|
||||
}
|
||||
|
||||
const useFileUpload = ({ upload }: MediaUploadOptions) => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
const [uploading, setUploading] = useState<boolean>(false);
|
||||
const [uploadProgress, setUploadProgress] = useState<ProgressEvent>();
|
||||
const [md5, setMd5] = useState<string>('');
|
||||
const [uploadProgress, setUploadProgress] = useState<AxiosProgressEvent>();
|
||||
const [uploadCancelToken, setUploadCancelToken] = useState<CancelTokenSource>();
|
||||
|
||||
const resetUploadingStates = () => {
|
||||
setUploading(false);
|
||||
setUploadProgress(undefined);
|
||||
setUploadCancelToken(undefined);
|
||||
setMd5('');
|
||||
};
|
||||
|
||||
const cancelUpload = useCallback(() => {
|
||||
@@ -37,23 +43,28 @@ const useFileUpload = ({ upload }: MediaUploadOptions) => {
|
||||
const cancelToken = axios.CancelToken.source();
|
||||
setUploadCancelToken(cancelToken);
|
||||
setUploading(true);
|
||||
await upload(images[0], {
|
||||
const response = await upload(images[0], {
|
||||
onUploadProgress: setUploadProgress,
|
||||
cancelToken: cancelToken.token
|
||||
});
|
||||
resetUploadingStates();
|
||||
enqueueSnackbar('File uploaded', { variant: 'success' });
|
||||
} catch (error: unknown) {
|
||||
if (response.status === 200) {
|
||||
enqueueSnackbar(LL.UPLOAD() + ' ' + LL.SUCCESSFUL(), { variant: 'success' });
|
||||
} else if (response.status === 201) {
|
||||
setMd5(String(response.data));
|
||||
enqueueSnackbar(LL.UPLOAD() + ' MD5 ' + LL.SUCCESSFUL(), { variant: 'success' });
|
||||
}
|
||||
} catch (error) {
|
||||
if (axios.isCancel(error)) {
|
||||
enqueueSnackbar('Upload aborted', { variant: 'warning' });
|
||||
enqueueSnackbar(LL.UPLOAD() + ' ' + LL.ABORTED(), { variant: 'warning' });
|
||||
} else {
|
||||
resetUploadingStates();
|
||||
enqueueSnackbar(extractErrorMessage(error, 'Upload failed'), { variant: 'error' });
|
||||
enqueueSnackbar(extractErrorMessage(error, LL.UPLOAD() + ' ' + LL.FAILED()), { variant: 'error' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return [uploadFile, cancelUpload, uploading, uploadProgress] as const;
|
||||
return [uploadFile, cancelUpload, uploading, uploadProgress, md5] as const;
|
||||
};
|
||||
|
||||
export default useFileUpload;
|
||||
|
||||
@@ -2,6 +2,8 @@ import { FC, useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { useSnackbar } from 'notistack';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useI18nContext } from '../../i18n/i18n-react';
|
||||
|
||||
import * as AuthenticationApi from '../../api/authentication';
|
||||
import { ACCESS_TOKEN } from '../../api/endpoints';
|
||||
import { RequiredChildrenProps } from '../../utils';
|
||||
@@ -12,6 +14,8 @@ import { AuthenticationContext } from './context';
|
||||
|
||||
const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
|
||||
const { features } = useContext(FeaturesContext);
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
@@ -23,8 +27,8 @@ const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
|
||||
AuthenticationApi.getStorage().setItem(ACCESS_TOKEN, accessToken);
|
||||
const decodedMe = AuthenticationApi.decodeMeJWT(accessToken);
|
||||
setMe(decodedMe);
|
||||
enqueueSnackbar(`Logged in as ${decodedMe.username}`, { variant: 'success' });
|
||||
} catch (error: unknown) {
|
||||
enqueueSnackbar(LL.LOGGED_IN({ name: decodedMe.username }), { variant: 'success' });
|
||||
} catch (error) {
|
||||
setMe(undefined);
|
||||
throw new Error('Failed to parse JWT');
|
||||
}
|
||||
@@ -50,7 +54,7 @@ const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
|
||||
await AuthenticationApi.verifyAuthorization();
|
||||
setMe(AuthenticationApi.decodeMeJWT(accessToken));
|
||||
setInitialized(true);
|
||||
} catch (error: unknown) {
|
||||
} catch (error) {
|
||||
setMe(undefined);
|
||||
setInitialized(true);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ const FeaturesLoader: FC<RequiredChildrenProps> = (props) => {
|
||||
try {
|
||||
const response = await FeaturesApi.readFeatures();
|
||||
setFeatures(response.data);
|
||||
} catch (error: unknown) {
|
||||
} catch (error) {
|
||||
setErrorMessage(extractErrorMessage(error, 'Failed to fetch application details.'));
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -19,6 +19,8 @@ import { APProvisionMode, APSettings } from '../../types';
|
||||
import { numberValue, updateValue, useRest } from '../../utils';
|
||||
import * as APApi from '../../api/ap';
|
||||
|
||||
import { useI18nContext } from '../../i18n/i18n-react';
|
||||
|
||||
export const isAPEnabled = ({ provision_mode }: APSettings) => {
|
||||
return provision_mode === APProvisionMode.AP_MODE_ALWAYS || provision_mode === APProvisionMode.AP_MODE_DISCONNECTED;
|
||||
};
|
||||
@@ -29,6 +31,8 @@ const APSettingsForm: FC = () => {
|
||||
update: APApi.updateAPSettings
|
||||
});
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
|
||||
const updateFormValue = updateValue(setData);
|
||||
@@ -53,7 +57,7 @@ const APSettingsForm: FC = () => {
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="provision_mode"
|
||||
label="Provide Access Point…"
|
||||
label={LL.AP_PROVIDE() + '...'}
|
||||
value={data.provision_mode}
|
||||
fullWidth
|
||||
select
|
||||
@@ -61,16 +65,16 @@ const APSettingsForm: FC = () => {
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
>
|
||||
<MenuItem value={APProvisionMode.AP_MODE_ALWAYS}>Always</MenuItem>
|
||||
<MenuItem value={APProvisionMode.AP_MODE_DISCONNECTED}>When WiFi Disconnected</MenuItem>
|
||||
<MenuItem value={APProvisionMode.AP_NEVER}>Never</MenuItem>
|
||||
<MenuItem value={APProvisionMode.AP_MODE_ALWAYS}>{LL.AP_PROVIDE_TEXT_1()}</MenuItem>
|
||||
<MenuItem value={APProvisionMode.AP_MODE_DISCONNECTED}>{LL.AP_PROVIDE_TEXT_2()}</MenuItem>
|
||||
<MenuItem value={APProvisionMode.AP_NEVER}>{LL.AP_PROVIDE_TEXT_3()}</MenuItem>
|
||||
</ValidatedTextField>
|
||||
{isAPEnabled(data) && (
|
||||
<>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="ssid"
|
||||
label="Access Point SSID"
|
||||
label={LL.ACCESS_POINT(2) + ' SSID'}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.ssid}
|
||||
@@ -80,7 +84,7 @@ const APSettingsForm: FC = () => {
|
||||
<ValidatedPasswordField
|
||||
fieldErrors={fieldErrors}
|
||||
name="password"
|
||||
label="Access Point Password"
|
||||
label={LL.ACCESS_POINT(2) + ' ' + LL.PASSWORD()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.password}
|
||||
@@ -90,7 +94,7 @@ const APSettingsForm: FC = () => {
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="channel"
|
||||
label="Preferred Channel"
|
||||
label={LL.AP_PREFERRED_CHANNEL()}
|
||||
value={numberValue(data.channel)}
|
||||
fullWidth
|
||||
select
|
||||
@@ -107,12 +111,12 @@ const APSettingsForm: FC = () => {
|
||||
</ValidatedTextField>
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox name="ssid_hidden" checked={data.ssid_hidden} onChange={updateFormValue} />}
|
||||
label="Hide SSID"
|
||||
label={LL.AP_HIDE_SSID()}
|
||||
/>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="max_clients"
|
||||
label="Max Clients"
|
||||
label={LL.AP_MAX_CLIENTS()}
|
||||
value={numberValue(data.max_clients)}
|
||||
fullWidth
|
||||
select
|
||||
@@ -130,7 +134,7 @@ const APSettingsForm: FC = () => {
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="local_ip"
|
||||
label="Local IP"
|
||||
label={LL.AP_LOCAL_IP()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.local_ip}
|
||||
@@ -140,7 +144,7 @@ const APSettingsForm: FC = () => {
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="gateway_ip"
|
||||
label="Gateway"
|
||||
label={LL.NETWORK_GATEWAY()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.gateway_ip}
|
||||
@@ -150,7 +154,7 @@ const APSettingsForm: FC = () => {
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="subnet_mask"
|
||||
label="Subnet"
|
||||
label={LL.NETWORK_SUBNET()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.subnet_mask}
|
||||
@@ -168,7 +172,7 @@ const APSettingsForm: FC = () => {
|
||||
type="submit"
|
||||
onClick={validateAndSubmit}
|
||||
>
|
||||
Save
|
||||
{LL.SAVE()}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
</>
|
||||
@@ -176,7 +180,7 @@ const APSettingsForm: FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title="Access Point Settings" titleGutter>
|
||||
<SectionContent title={LL.SETTINGS_OF(LL.ACCESS_POINT(1))} titleGutter>
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
|
||||
@@ -11,6 +11,8 @@ import { APNetworkStatus, APStatus } from '../../types';
|
||||
import { ButtonRow, FormLoader, SectionContent } from '../../components';
|
||||
import { useRest } from '../../utils';
|
||||
|
||||
import { useI18nContext } from '../../i18n/i18n-react';
|
||||
|
||||
export const apStatusHighlight = ({ status }: APStatus, theme: Theme) => {
|
||||
switch (status) {
|
||||
case APNetworkStatus.ACTIVE:
|
||||
@@ -24,24 +26,26 @@ export const apStatusHighlight = ({ status }: APStatus, theme: Theme) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const apStatus = ({ status }: APStatus) => {
|
||||
switch (status) {
|
||||
case APNetworkStatus.ACTIVE:
|
||||
return 'Active';
|
||||
case APNetworkStatus.INACTIVE:
|
||||
return 'Inactive';
|
||||
case APNetworkStatus.LINGERING:
|
||||
return 'Lingering until idle';
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
};
|
||||
|
||||
const APStatusForm: FC = () => {
|
||||
const { loadData, data, errorMessage } = useRest<APStatus>({ read: APApi.readAPStatus });
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const apStatus = ({ status }: APStatus) => {
|
||||
switch (status) {
|
||||
case APNetworkStatus.ACTIVE:
|
||||
return LL.ACTIVE();
|
||||
case APNetworkStatus.INACTIVE:
|
||||
return LL.INACTIVE(0);
|
||||
case APNetworkStatus.LINGERING:
|
||||
return 'Lingering until idle';
|
||||
default:
|
||||
return LL.UNKNOWN();
|
||||
}
|
||||
};
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
|
||||
@@ -56,14 +60,14 @@ const APStatusForm: FC = () => {
|
||||
<SettingsInputAntennaIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="Status" secondary={apStatus(data)} />
|
||||
<ListItemText primary={LL.STATUS_OF('')} secondary={apStatus(data)} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>IP</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="IP Address" secondary={data.ip_address} />
|
||||
<ListItemText primary={LL.ADDRESS_OF('IP')} secondary={data.ip_address} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
@@ -72,7 +76,7 @@ const APStatusForm: FC = () => {
|
||||
<DeviceHubIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="MAC Address" secondary={data.mac_address} />
|
||||
<ListItemText primary={LL.ADDRESS_OF('MAC')} secondary={data.mac_address} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
@@ -81,13 +85,13 @@ const APStatusForm: FC = () => {
|
||||
<ComputerIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="AP Clients" secondary={data.station_num} />
|
||||
<ListItemText primary={LL.AP_CLIENTS()} secondary={data.station_num} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
</List>
|
||||
<ButtonRow>
|
||||
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
|
||||
Refresh
|
||||
{LL.REFRESH()}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
</>
|
||||
@@ -95,7 +99,7 @@ const APStatusForm: FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title="Access Point Status" titleGutter>
|
||||
<SectionContent title={LL.STATUS_OF(LL.ACCESS_POINT(1))} titleGutter>
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
|
||||
@@ -8,8 +8,12 @@ import APStatusForm from './APStatusForm';
|
||||
import APSettingsForm from './APSettingsForm';
|
||||
import { RequireAdmin, RouterTabs, useLayoutTitle, useRouterTab } from '../../components';
|
||||
|
||||
import { useI18nContext } from '../../i18n/i18n-react';
|
||||
|
||||
const AccessPoint: FC = () => {
|
||||
useLayoutTitle('Access Point');
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
useLayoutTitle(LL.ACCESS_POINT(0));
|
||||
|
||||
const authenticatedContext = useContext(AuthenticatedContext);
|
||||
|
||||
@@ -18,8 +22,8 @@ const AccessPoint: FC = () => {
|
||||
return (
|
||||
<>
|
||||
<RouterTabs value={routerTab}>
|
||||
<Tab value="status" label="Access Point Status" />
|
||||
<Tab value="settings" label="Access Point Settings" disabled={!authenticatedContext.me.admin} />
|
||||
<Tab value="status" label={LL.STATUS_OF(LL.ACCESS_POINT(1))} />
|
||||
<Tab value="settings" label={LL.SETTINGS_OF(LL.ACCESS_POINT(1))} disabled={!authenticatedContext.me.admin} />
|
||||
</RouterTabs>
|
||||
<Routes>
|
||||
<Route path="status" element={<APStatusForm />} />
|
||||
|
||||
@@ -9,7 +9,11 @@ import { AuthenticatedContext } from '../../contexts/authentication';
|
||||
import MqttStatusForm from './MqttStatusForm';
|
||||
import MqttSettingsForm from './MqttSettingsForm';
|
||||
|
||||
import { useI18nContext } from '../../i18n/i18n-react';
|
||||
|
||||
const Mqtt: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
useLayoutTitle('MQTT');
|
||||
|
||||
const authenticatedContext = useContext(AuthenticatedContext);
|
||||
@@ -18,8 +22,8 @@ const Mqtt: FC = () => {
|
||||
return (
|
||||
<>
|
||||
<RouterTabs value={routerTab}>
|
||||
<Tab value="status" label="MQTT Status" />
|
||||
<Tab value="settings" label="MQTT Settings" disabled={!authenticatedContext.me.admin} />
|
||||
<Tab value="status" label={LL.STATUS_OF('MQTT')} />
|
||||
<Tab value="settings" label={LL.SETTINGS_OF('MQTT')} disabled={!authenticatedContext.me.admin} />
|
||||
</RouterTabs>
|
||||
<Routes>
|
||||
<Route path="status" element={<MqttStatusForm />} />
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { FC, useState } from 'react';
|
||||
import { ValidateFieldsError } from 'async-validator';
|
||||
|
||||
import { Button, Checkbox, MenuItem, Grid, Typography } from '@mui/material';
|
||||
import { Button, Checkbox, MenuItem, Grid, Typography, InputAdornment } from '@mui/material';
|
||||
import SaveIcon from '@mui/icons-material/Save';
|
||||
|
||||
import { MQTT_SETTINGS_VALIDATOR, validate } from '../../validators';
|
||||
import { createMqttSettingsValidator, validate } from '../../validators';
|
||||
import {
|
||||
BlockFormControlLabel,
|
||||
ButtonRow,
|
||||
@@ -17,12 +17,16 @@ import { MqttSettings } from '../../types';
|
||||
import { numberValue, updateValue, useRest } from '../../utils';
|
||||
import * as MqttApi from '../../api/mqtt';
|
||||
|
||||
import { useI18nContext } from '../../i18n/i18n-react';
|
||||
|
||||
const MqttSettingsForm: FC = () => {
|
||||
const { loadData, saving, data, setData, saveData, errorMessage } = useRest<MqttSettings>({
|
||||
read: MqttApi.readMqttSettings,
|
||||
update: MqttApi.updateMqttSettings
|
||||
});
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
|
||||
const updateFormValue = updateValue(setData);
|
||||
@@ -35,7 +39,7 @@ const MqttSettingsForm: FC = () => {
|
||||
const validateAndSubmit = async () => {
|
||||
try {
|
||||
setFieldErrors(undefined);
|
||||
await validate(MQTT_SETTINGS_VALIDATOR, data);
|
||||
await validate(createMqttSettingsValidator(data), data);
|
||||
saveData();
|
||||
} catch (errors: any) {
|
||||
setFieldErrors(errors);
|
||||
@@ -46,14 +50,14 @@ const MqttSettingsForm: FC = () => {
|
||||
<>
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox name="enabled" checked={data.enabled} onChange={updateFormValue} />}
|
||||
label="Enable MQTT"
|
||||
label={LL.ENABLE_MQTT()}
|
||||
/>
|
||||
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
||||
<Grid item xs={6}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="host"
|
||||
label="Host"
|
||||
label={LL.ADDRESS_OF(LL.BROKER())}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.host}
|
||||
@@ -80,7 +84,7 @@ const MqttSettingsForm: FC = () => {
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="base"
|
||||
label="Base"
|
||||
label={LL.BASE_TOPIC()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.base}
|
||||
@@ -91,7 +95,7 @@ const MqttSettingsForm: FC = () => {
|
||||
<Grid item xs={6}>
|
||||
<ValidatedTextField
|
||||
name="client_id"
|
||||
label="Client ID (optional)"
|
||||
label={LL.ID_OF(LL.CLIENT()) + ' (' + LL.OPTIONAL() + ')'}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.client_id}
|
||||
@@ -104,7 +108,7 @@ const MqttSettingsForm: FC = () => {
|
||||
<Grid item xs={6}>
|
||||
<ValidatedTextField
|
||||
name="username"
|
||||
label="Username"
|
||||
label={LL.USERNAME(0)}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.username}
|
||||
@@ -115,7 +119,7 @@ const MqttSettingsForm: FC = () => {
|
||||
<Grid item xs={6}>
|
||||
<ValidatedPasswordField
|
||||
name="password"
|
||||
label="Password"
|
||||
label={LL.PASSWORD()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.password}
|
||||
@@ -129,7 +133,10 @@ const MqttSettingsForm: FC = () => {
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="keep_alive"
|
||||
label="Keep Alive (seconds)"
|
||||
label="Keep Alive"
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||
}}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.keep_alive)}
|
||||
@@ -149,7 +156,7 @@ const MqttSettingsForm: FC = () => {
|
||||
margin="normal"
|
||||
select
|
||||
>
|
||||
<MenuItem value={0}>0 (default)</MenuItem>
|
||||
<MenuItem value={0}>0</MenuItem>
|
||||
<MenuItem value={1}>1</MenuItem>
|
||||
<MenuItem value={2}>2</MenuItem>
|
||||
</ValidatedTextField>
|
||||
@@ -157,18 +164,19 @@ const MqttSettingsForm: FC = () => {
|
||||
</Grid>
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox name="clean_session" checked={data.clean_session} onChange={updateFormValue} />}
|
||||
label="Set Clean Session"
|
||||
label={LL.MQTT_CLEAN_SESSION()}
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox name="mqtt_retain" checked={data.mqtt_retain} onChange={updateFormValue} />}
|
||||
label="Always use Retain Flag"
|
||||
label={LL.MQTT_RETAIN_FLAG()}
|
||||
/>
|
||||
|
||||
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
||||
Formatting
|
||||
{LL.FORMATTING()}
|
||||
</Typography>
|
||||
<ValidatedTextField
|
||||
name="nested_format"
|
||||
label="Topic/Payload Format"
|
||||
label={LL.MQTT_FORMAT()}
|
||||
value={data.nested_format}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
@@ -176,19 +184,19 @@ const MqttSettingsForm: FC = () => {
|
||||
margin="normal"
|
||||
select
|
||||
>
|
||||
<MenuItem value={1}>Nested in a single topic</MenuItem>
|
||||
<MenuItem value={2}>As individual topics</MenuItem>
|
||||
<MenuItem value={1}>{LL.MQTT_NEST_1()}</MenuItem>
|
||||
<MenuItem value={2}>{LL.MQTT_NEST_2()}</MenuItem>
|
||||
</ValidatedTextField>
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox name="send_response" checked={data.send_response} onChange={updateFormValue} />}
|
||||
label="Publish command output to a 'response' topic"
|
||||
label={LL.MQTT_RESPONSE()}
|
||||
/>
|
||||
{!data.ha_enabled && (
|
||||
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
||||
<Grid item>
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox name="publish_single" checked={data.publish_single} onChange={updateFormValue} />}
|
||||
label="Publish single value topics on change"
|
||||
label={LL.MQTT_PUBLISH_TEXT_1()}
|
||||
/>
|
||||
</Grid>
|
||||
{data.publish_single && (
|
||||
@@ -197,7 +205,7 @@ const MqttSettingsForm: FC = () => {
|
||||
control={
|
||||
<Checkbox name="publish_single2cmd" checked={data.publish_single2cmd} onChange={updateFormValue} />
|
||||
}
|
||||
label="Publish to command topics (ioBroker)"
|
||||
label={LL.MQTT_PUBLISH_TEXT_2()}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
@@ -207,34 +215,74 @@ const MqttSettingsForm: FC = () => {
|
||||
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
||||
<Grid item>
|
||||
<BlockFormControlLabel
|
||||
sx={{ pb: 1 }}
|
||||
control={<Checkbox name="ha_enabled" checked={data.ha_enabled} onChange={updateFormValue} />}
|
||||
label="Enable MQTT Discovery (Home Assistant, Domoticz)"
|
||||
label={LL.MQTT_PUBLISH_TEXT_3()}
|
||||
/>
|
||||
</Grid>
|
||||
{data.ha_enabled && (
|
||||
<Grid item xs={6}>
|
||||
<ValidatedTextField
|
||||
name="discovery_prefix"
|
||||
label="Prefix for the Discovery topics"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.discovery_prefix}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<>
|
||||
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
||||
<Grid item>
|
||||
<ValidatedTextField
|
||||
name="discovery_prefix"
|
||||
label={LL.MQTT_PUBLISH_TEXT_4()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.discovery_prefix}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<ValidatedTextField
|
||||
name="entity_format"
|
||||
label={LL.MQTT_ENTITY_FORMAT()}
|
||||
value={data.entity_format}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
select
|
||||
>
|
||||
<MenuItem value={0}>{LL.MQTT_ENTITY_FORMAT_0()}</MenuItem>
|
||||
<MenuItem value={1}>{LL.MQTT_ENTITY_FORMAT_1()}</MenuItem>
|
||||
<MenuItem value={2}>{LL.MQTT_ENTITY_FORMAT_2()}</MenuItem>
|
||||
</ValidatedTextField>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
</Grid>
|
||||
)}
|
||||
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
||||
Publish Intervals (in seconds, 0=automatic)
|
||||
{LL.MQTT_PUBLISH_INTERVALS()} (0=auto)
|
||||
</Typography>
|
||||
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
||||
<Grid item xs={4}>
|
||||
<Grid item xs={6} sm={4}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="publish_time_heartbeat"
|
||||
label={LL.MQTT_INT_HEARTBEAT()}
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||
}}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.publish_time_heartbeat)}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6} sm={4}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="publish_time_boiler"
|
||||
label="Boilers and Heat Pumps"
|
||||
label={LL.MQTT_INT_BOILER()}
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||
}}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.publish_time_boiler)}
|
||||
@@ -243,11 +291,14 @@ const MqttSettingsForm: FC = () => {
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<Grid item xs={6} sm={4}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="publish_time_thermostat"
|
||||
label="Thermostats"
|
||||
label={LL.MQTT_INT_THERMOSTATS()}
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||
}}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.publish_time_thermostat)}
|
||||
@@ -256,11 +307,14 @@ const MqttSettingsForm: FC = () => {
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<Grid item xs={6} sm={4}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="publish_time_solar"
|
||||
label="Solar Modules"
|
||||
label={LL.MQTT_INT_SOLAR()}
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||
}}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.publish_time_solar)}
|
||||
@@ -269,11 +323,14 @@ const MqttSettingsForm: FC = () => {
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<Grid item xs={6} sm={4}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="publish_time_mixer"
|
||||
label="Mixer Modules"
|
||||
label={LL.MQTT_INT_MIXER()}
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||
}}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.publish_time_mixer)}
|
||||
@@ -282,11 +339,14 @@ const MqttSettingsForm: FC = () => {
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<Grid item xs={6} sm={4}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="publish_time_sensor"
|
||||
label="Temperature Sensors"
|
||||
label={LL.TEMP_SENSORS()}
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||
}}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.publish_time_sensor)}
|
||||
@@ -295,11 +355,14 @@ const MqttSettingsForm: FC = () => {
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<Grid item xs={6} sm={4}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="publish_time_other"
|
||||
label="Default"
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||
}}
|
||||
label={LL.DEFAULT(0)}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.publish_time_other)}
|
||||
@@ -318,7 +381,7 @@ const MqttSettingsForm: FC = () => {
|
||||
type="submit"
|
||||
onClick={validateAndSubmit}
|
||||
>
|
||||
Save
|
||||
{LL.SAVE()}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
</>
|
||||
@@ -326,7 +389,7 @@ const MqttSettingsForm: FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title="MQTT Settings" titleGutter>
|
||||
<SectionContent title={LL.SETTINGS_OF('MQTT')} titleGutter>
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
|
||||
@@ -5,12 +5,15 @@ import DeviceHubIcon from '@mui/icons-material/DeviceHub';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import ReportIcon from '@mui/icons-material/Report';
|
||||
import SpeakerNotesOffIcon from '@mui/icons-material/SpeakerNotesOff';
|
||||
import AutoAwesomeMotionIcon from '@mui/icons-material/AutoAwesomeMotion';
|
||||
|
||||
import { ButtonRow, FormLoader, SectionContent } from '../../components';
|
||||
import { MqttStatus, MqttDisconnectReason } from '../../types';
|
||||
import * as MqttApi from '../../api/mqtt';
|
||||
import { useRest } from '../../utils';
|
||||
|
||||
import { useI18nContext } from '../../i18n/i18n-react';
|
||||
|
||||
export const mqttStatusHighlight = ({ enabled, connected }: MqttStatus, theme: Theme) => {
|
||||
if (!enabled) {
|
||||
return theme.palette.info.main;
|
||||
@@ -29,80 +32,96 @@ export const mqttPublishHighlight = ({ mqtt_fails }: MqttStatus, theme: Theme) =
|
||||
return theme.palette.error.main;
|
||||
};
|
||||
|
||||
export const mqttStatus = ({ enabled, connected }: MqttStatus) => {
|
||||
if (!enabled) {
|
||||
return 'Not enabled';
|
||||
}
|
||||
if (connected) {
|
||||
return 'Connected';
|
||||
}
|
||||
return 'Disconnected';
|
||||
};
|
||||
export const mqttQueueHighlight = ({ mqtt_queued }: MqttStatus, theme: Theme) => {
|
||||
if (mqtt_queued <= 1) return theme.palette.success.main;
|
||||
|
||||
export const disconnectReason = ({ disconnect_reason }: MqttStatus) => {
|
||||
switch (disconnect_reason) {
|
||||
case MqttDisconnectReason.TCP_DISCONNECTED:
|
||||
return 'TCP disconnected';
|
||||
case MqttDisconnectReason.MQTT_UNACCEPTABLE_PROTOCOL_VERSION:
|
||||
return 'Unacceptable protocol version';
|
||||
case MqttDisconnectReason.MQTT_IDENTIFIER_REJECTED:
|
||||
return 'Client ID rejected';
|
||||
case MqttDisconnectReason.MQTT_SERVER_UNAVAILABLE:
|
||||
return 'Server unavailable';
|
||||
case MqttDisconnectReason.MQTT_MALFORMED_CREDENTIALS:
|
||||
return 'Malformed credentials';
|
||||
case MqttDisconnectReason.MQTT_NOT_AUTHORIZED:
|
||||
return 'Not authorized';
|
||||
case MqttDisconnectReason.ESP8266_NOT_ENOUGH_SPACE:
|
||||
return 'Device out of memory';
|
||||
case MqttDisconnectReason.TLS_BAD_FINGERPRINT:
|
||||
return 'Server fingerprint invalid';
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
return theme.palette.warning.main;
|
||||
};
|
||||
|
||||
const MqttStatusForm: FC = () => {
|
||||
const { loadData, data, errorMessage } = useRest<MqttStatus>({ read: MqttApi.readMqttStatus });
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const mqttStatus = ({ enabled, connected, connect_count }: MqttStatus) => {
|
||||
if (!enabled) {
|
||||
return LL.NOT_ENABLED();
|
||||
}
|
||||
if (connected) {
|
||||
return LL.CONNECTED(0) + (connect_count > 1 ? ' (' + connect_count + ')' : '');
|
||||
}
|
||||
return LL.DISCONNECTED() + (connect_count > 1 ? ' (' + connect_count + ')' : '');
|
||||
};
|
||||
|
||||
const disconnectReason = ({ disconnect_reason }: MqttStatus) => {
|
||||
switch (disconnect_reason) {
|
||||
case MqttDisconnectReason.TCP_DISCONNECTED:
|
||||
return 'TCP disconnected';
|
||||
case MqttDisconnectReason.MQTT_UNACCEPTABLE_PROTOCOL_VERSION:
|
||||
return 'Unacceptable protocol version';
|
||||
case MqttDisconnectReason.MQTT_IDENTIFIER_REJECTED:
|
||||
return 'Client ID rejected';
|
||||
case MqttDisconnectReason.MQTT_SERVER_UNAVAILABLE:
|
||||
return 'Server unavailable';
|
||||
case MqttDisconnectReason.MQTT_MALFORMED_CREDENTIALS:
|
||||
return 'Malformed credentials';
|
||||
case MqttDisconnectReason.MQTT_NOT_AUTHORIZED:
|
||||
return 'Not authorized';
|
||||
case MqttDisconnectReason.ESP8266_NOT_ENOUGH_SPACE:
|
||||
return 'Device out of memory';
|
||||
case MqttDisconnectReason.TLS_BAD_FINGERPRINT:
|
||||
return 'Server fingerprint invalid';
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
};
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
|
||||
}
|
||||
|
||||
const renderConnectionStatus = () => {
|
||||
if (data.connected) {
|
||||
return (
|
||||
<>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>#</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="Client ID" secondary={data.client_id} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: mqttPublishHighlight(data, theme) }}>
|
||||
<SpeakerNotesOffIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="MQTT Publish Errors" secondary={data.mqtt_fails} />
|
||||
</ListItem>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{!data.connected && (
|
||||
<>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<ReportIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={LL.DISCONNECT_REASON()} secondary={disconnectReason(data)} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
</>
|
||||
)}
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<ReportIcon />
|
||||
<Avatar>#</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={LL.ID_OF(LL.CLIENT())} secondary={data.client_id} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: mqttQueueHighlight(data, theme) }}>
|
||||
<AutoAwesomeMotionIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="Disconnect Reason" secondary={disconnectReason(data)} />
|
||||
<ListItemText primary={LL.MQTT_QUEUE()} secondary={data.mqtt_queued} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: mqttPublishHighlight(data, theme) }}>
|
||||
<SpeakerNotesOffIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={LL.ERRORS_OF('MQTT')} secondary={data.mqtt_fails} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
</>
|
||||
@@ -118,14 +137,14 @@ const MqttStatusForm: FC = () => {
|
||||
<DeviceHubIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="Status" secondary={mqttStatus(data)} />
|
||||
<ListItemText primary={LL.STATUS_OF('')} secondary={mqttStatus(data)} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
{data.enabled && renderConnectionStatus()}
|
||||
</List>
|
||||
<ButtonRow>
|
||||
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
|
||||
Refresh
|
||||
{LL.REFRESH()}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
</>
|
||||
@@ -133,7 +152,7 @@ const MqttStatusForm: FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title="MQTT Status" titleGutter>
|
||||
<SectionContent title={LL.STATUS_OF('MQTT')} titleGutter>
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
|
||||
@@ -11,12 +11,16 @@ import NetworkStatusForm from './NetworkStatusForm';
|
||||
import WiFiNetworkScanner from './WiFiNetworkScanner';
|
||||
import NetworkSettingsForm from './NetworkSettingsForm';
|
||||
|
||||
import { useI18nContext } from '../../i18n/i18n-react';
|
||||
|
||||
const NetworkConnection: FC = () => {
|
||||
useLayoutTitle('Network Connection');
|
||||
const { LL } = useI18nContext();
|
||||
useLayoutTitle(LL.NETWORK(0));
|
||||
|
||||
const { routerTab } = useRouterTab();
|
||||
|
||||
const authenticatedContext = useContext(AuthenticatedContext);
|
||||
const navigate = useNavigate();
|
||||
const { routerTab } = useRouterTab();
|
||||
|
||||
const [selectedNetwork, setSelectedNetwork] = useState<WiFiNetwork>();
|
||||
|
||||
@@ -41,9 +45,9 @@ const NetworkConnection: FC = () => {
|
||||
}}
|
||||
>
|
||||
<RouterTabs value={routerTab}>
|
||||
<Tab value="status" label="Network Status" />
|
||||
<Tab value="scan" label="Scan WiFi Networks" disabled={!authenticatedContext.me.admin} />
|
||||
<Tab value="settings" label="Network Settings" disabled={!authenticatedContext.me.admin} />
|
||||
<Tab value="status" label={LL.STATUS_OF(LL.NETWORK(1))} />
|
||||
<Tab value="scan" label={LL.NETWORK_SCAN()} disabled={!authenticatedContext.me.admin} />
|
||||
<Tab value="settings" label={LL.SETTINGS_OF(LL.NETWORK(1))} disabled={!authenticatedContext.me.admin} />
|
||||
</RouterTabs>
|
||||
<Routes>
|
||||
<Route path="status" element={<NetworkStatusForm />} />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { FC, useContext, useEffect, useState } from 'react';
|
||||
import { useSnackbar } from 'notistack';
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
@@ -10,13 +11,15 @@ import {
|
||||
ListItemAvatar,
|
||||
ListItemSecondaryAction,
|
||||
ListItemText,
|
||||
Typography
|
||||
Typography,
|
||||
InputAdornment
|
||||
} from '@mui/material';
|
||||
|
||||
import LockOpenIcon from '@mui/icons-material/LockOpen';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import SaveIcon from '@mui/icons-material/Save';
|
||||
import LockIcon from '@mui/icons-material/Lock';
|
||||
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
|
||||
|
||||
import {
|
||||
BlockFormControlLabel,
|
||||
@@ -24,11 +27,13 @@ import {
|
||||
FormLoader,
|
||||
SectionContent,
|
||||
ValidatedPasswordField,
|
||||
ValidatedTextField
|
||||
ValidatedTextField,
|
||||
MessageBox
|
||||
} from '../../components';
|
||||
import { NetworkSettings } from '../../types';
|
||||
import * as NetworkApi from '../../api/network';
|
||||
import { numberValue, updateValue, useRest } from '../../utils';
|
||||
import * as EMSESP from '../../project/api';
|
||||
|
||||
import { WiFiConnectionContext } from './WiFiConnectionContext';
|
||||
import { isNetworkOpen, networkSecurityMode } from './WiFiNetworkSelector';
|
||||
@@ -36,11 +41,18 @@ import { ValidateFieldsError } from 'async-validator';
|
||||
import { validate } from '../../validators';
|
||||
import { createNetworkSettingsValidator } from '../../validators/network';
|
||||
|
||||
import { useI18nContext } from '../../i18n/i18n-react';
|
||||
import RestartMonitor from '../system/RestartMonitor';
|
||||
|
||||
const WiFiSettingsForm: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const { selectedNetwork, deselectNetwork } = useContext(WiFiConnectionContext);
|
||||
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
const { loadData, saving, data, setData, saveData, errorMessage } = useRest<NetworkSettings>({
|
||||
const [restarting, setRestarting] = useState(false);
|
||||
const { loadData, saving, data, setData, saveData, errorMessage, restartNeeded } = useRest<NetworkSettings>({
|
||||
read: NetworkApi.readNetworkSettings,
|
||||
update: NetworkApi.updateNetworkSettings
|
||||
});
|
||||
@@ -57,7 +69,9 @@ const WiFiSettingsForm: FC = () => {
|
||||
bandwidth20: false,
|
||||
tx_power: 20,
|
||||
nosleep: false,
|
||||
enableMDNS: true
|
||||
enableMDNS: true,
|
||||
enableCORS: false,
|
||||
CORSOrigin: '*'
|
||||
});
|
||||
}
|
||||
setInitialized(true);
|
||||
@@ -85,6 +99,15 @@ const WiFiSettingsForm: FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const restart = async () => {
|
||||
try {
|
||||
await EMSESP.restart();
|
||||
setRestarting(true);
|
||||
} catch (error) {
|
||||
enqueueSnackbar(LL.PROBLEM_UPDATING(), { variant: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
||||
@@ -111,7 +134,7 @@ const WiFiSettingsForm: FC = () => {
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="ssid"
|
||||
label="SSID (leave blank to disable WiFi)"
|
||||
label={'SSID (' + LL.NETWORK_BLANK_SSID() + ')'}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.ssid}
|
||||
@@ -123,7 +146,7 @@ const WiFiSettingsForm: FC = () => {
|
||||
<ValidatedPasswordField
|
||||
fieldErrors={fieldErrors}
|
||||
name="password"
|
||||
label="Password"
|
||||
label={LL.PASSWORD()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.password}
|
||||
@@ -135,7 +158,10 @@ const WiFiSettingsForm: FC = () => {
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="tx_power"
|
||||
label="WiFi Tx Power (dBm)"
|
||||
label={LL.TX_POWER()}
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">dBm</InputAdornment>
|
||||
}}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.tx_power)}
|
||||
@@ -146,27 +172,22 @@ const WiFiSettingsForm: FC = () => {
|
||||
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox name="nosleep" checked={data.nosleep} onChange={updateFormValue} />}
|
||||
label="Disable WiFi Sleep Mode"
|
||||
label={LL.NETWORK_DISABLE_SLEEP()}
|
||||
/>
|
||||
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox name="bandwidth20" checked={data.bandwidth20} onChange={updateFormValue} />}
|
||||
label="Use Lower WiFi Bandwidth"
|
||||
/>
|
||||
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox name="enableMDNS" checked={data.enableMDNS} onChange={updateFormValue} />}
|
||||
label="Enable mDNS Service"
|
||||
label={LL.NETWORK_LOW_BAND()}
|
||||
/>
|
||||
|
||||
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
||||
General
|
||||
{LL.GENERAL_OPTIONS()}
|
||||
</Typography>
|
||||
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="hostname"
|
||||
label="Hostname"
|
||||
label={LL.HOSTNAME()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.hostname}
|
||||
@@ -174,21 +195,43 @@ const WiFiSettingsForm: FC = () => {
|
||||
margin="normal"
|
||||
/>
|
||||
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox name="enableMDNS" checked={data.enableMDNS} onChange={updateFormValue} />}
|
||||
label={LL.NETWORK_USE_DNS()}
|
||||
/>
|
||||
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox name="enableCORS" checked={data.enableCORS} onChange={updateFormValue} />}
|
||||
label={LL.NETWORK_ENABLE_CORS()}
|
||||
/>
|
||||
{data.enableCORS && (
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="CORSOrigin"
|
||||
label={LL.NETWORK_CORS_ORIGIN()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.CORSOrigin}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
)}
|
||||
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox name="enableIPv6" checked={data.enableIPv6} onChange={updateFormValue} />}
|
||||
label="Enable IPv6 support"
|
||||
label={LL.NETWORK_ENABLE_IPV6()}
|
||||
/>
|
||||
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox name="static_ip_config" checked={data.static_ip_config} onChange={updateFormValue} />}
|
||||
label="Use Fixed IP address"
|
||||
label={LL.NETWORK_FIXED_IP()}
|
||||
/>
|
||||
{data.static_ip_config && (
|
||||
<>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="local_ip"
|
||||
label="Local IP"
|
||||
label={LL.AP_LOCAL_IP()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.local_ip}
|
||||
@@ -198,7 +241,7 @@ const WiFiSettingsForm: FC = () => {
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="gateway_ip"
|
||||
label="Gateway"
|
||||
label={LL.NETWORK_GATEWAY()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.gateway_ip}
|
||||
@@ -208,7 +251,7 @@ const WiFiSettingsForm: FC = () => {
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="subnet_mask"
|
||||
label="Subnet"
|
||||
label={LL.NETWORK_SUBNET()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.subnet_mask}
|
||||
@@ -218,7 +261,7 @@ const WiFiSettingsForm: FC = () => {
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="dns_ip_1"
|
||||
label="DNS IP #1"
|
||||
label="DNS #1"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.dns_ip_1}
|
||||
@@ -228,7 +271,7 @@ const WiFiSettingsForm: FC = () => {
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="dns_ip_2"
|
||||
label="DNS IP #2"
|
||||
label="DNS #2"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.dns_ip_2}
|
||||
@@ -237,25 +280,34 @@ const WiFiSettingsForm: FC = () => {
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<ButtonRow>
|
||||
<Button
|
||||
startIcon={<SaveIcon />}
|
||||
disabled={saving}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
type="submit"
|
||||
onClick={validateAndSubmit}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
{restartNeeded && (
|
||||
<MessageBox my={2} level="warning" message={LL.RESTART_TEXT()}>
|
||||
<Button startIcon={<PowerSettingsNewIcon />} variant="contained" color="error" onClick={restart}>
|
||||
{LL.RESTART()}
|
||||
</Button>
|
||||
</MessageBox>
|
||||
)}
|
||||
{!restartNeeded && (
|
||||
<ButtonRow>
|
||||
<Button
|
||||
startIcon={<SaveIcon />}
|
||||
disabled={saving}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
type="submit"
|
||||
onClick={validateAndSubmit}
|
||||
>
|
||||
{LL.SAVE()}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title="Network Settings" titleGutter>
|
||||
{content()}
|
||||
<SectionContent title={LL.SETTINGS_OF(LL.NETWORK(1))} titleGutter>
|
||||
{restarting ? <RestartMonitor /> : content()}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,6 +14,8 @@ import { NetworkConnectionStatus, NetworkStatus } from '../../types';
|
||||
import * as NetworkApi from '../../api/network';
|
||||
import { useRest } from '../../utils';
|
||||
|
||||
import { useI18nContext } from '../../i18n/i18n-react';
|
||||
|
||||
const isConnected = ({ status }: NetworkStatus) =>
|
||||
status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED ||
|
||||
status === NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED;
|
||||
@@ -35,29 +37,6 @@ const networkStatusHighlight = ({ status }: NetworkStatus, theme: Theme) => {
|
||||
}
|
||||
};
|
||||
|
||||
const networkStatus = ({ status }: NetworkStatus) => {
|
||||
switch (status) {
|
||||
case NetworkConnectionStatus.WIFI_STATUS_NO_SHIELD:
|
||||
return 'Inactive';
|
||||
case NetworkConnectionStatus.WIFI_STATUS_IDLE:
|
||||
return 'Idle';
|
||||
case NetworkConnectionStatus.WIFI_STATUS_NO_SSID_AVAIL:
|
||||
return 'No SSID Available';
|
||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECTED:
|
||||
return 'Connected (WiFi)';
|
||||
case NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED:
|
||||
return 'Connected (Ethernet)';
|
||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECT_FAILED:
|
||||
return 'Connection Failed';
|
||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECTION_LOST:
|
||||
return 'Connection Lost';
|
||||
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
|
||||
return 'Disconnected';
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
};
|
||||
|
||||
export const isWiFi = ({ status }: NetworkStatus) => status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED;
|
||||
export const isEthernet = ({ status }: NetworkStatus) => status === NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED;
|
||||
|
||||
@@ -65,7 +44,7 @@ const dnsServers = ({ dns_ip_1, dns_ip_2 }: NetworkStatus) => {
|
||||
if (!dns_ip_1) {
|
||||
return 'none';
|
||||
}
|
||||
return dns_ip_1 + (dns_ip_2 === '0.0.0.0' ? '' : ',' + dns_ip_2);
|
||||
return dns_ip_1 + (!dns_ip_2 || dns_ip_2 === '0.0.0.0' ? '' : ',' + dns_ip_2);
|
||||
};
|
||||
|
||||
const IPs = (status: NetworkStatus) => {
|
||||
@@ -81,8 +60,33 @@ const IPs = (status: NetworkStatus) => {
|
||||
const NetworkStatusForm: FC = () => {
|
||||
const { loadData, data, errorMessage } = useRest<NetworkStatus>({ read: NetworkApi.readNetworkStatus });
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const networkStatus = ({ status }: NetworkStatus) => {
|
||||
switch (status) {
|
||||
case NetworkConnectionStatus.WIFI_STATUS_NO_SHIELD:
|
||||
return LL.INACTIVE(1);
|
||||
case NetworkConnectionStatus.WIFI_STATUS_IDLE:
|
||||
return LL.IDLE();
|
||||
case NetworkConnectionStatus.WIFI_STATUS_NO_SSID_AVAIL:
|
||||
return 'No SSID Available';
|
||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECTED:
|
||||
return LL.CONNECTED(0) + ' (WiFi)';
|
||||
case NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED:
|
||||
return LL.CONNECTED(0) + ' (Ethernet)';
|
||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECT_FAILED:
|
||||
return LL.CONNECTED(1) + ' ' + LL.FAILED();
|
||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECTION_LOST:
|
||||
return LL.CONNECTED(1) + ' ' + LL.LOST();
|
||||
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
|
||||
return LL.DISCONNECTED();
|
||||
default:
|
||||
return LL.UNKNOWN();
|
||||
}
|
||||
};
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
|
||||
@@ -120,7 +124,7 @@ const NetworkStatusForm: FC = () => {
|
||||
<ListItemAvatar>
|
||||
<Avatar>IP</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="IP Address" secondary={IPs(data)} />
|
||||
<ListItemText primary={LL.ADDRESS_OF('IP')} secondary={IPs(data)} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
@@ -129,14 +133,14 @@ const NetworkStatusForm: FC = () => {
|
||||
<DeviceHubIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="MAC Address" secondary={data.mac_address} />
|
||||
<ListItemText primary={LL.ADDRESS_OF('MAC')} secondary={data.mac_address} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>#</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="Subnet Mask" secondary={data.subnet_mask} />
|
||||
<ListItemText primary={LL.NETWORK_SUBNET()} secondary={data.subnet_mask} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
@@ -145,7 +149,7 @@ const NetworkStatusForm: FC = () => {
|
||||
<SettingsInputComponentIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="Gateway IP" secondary={data.gateway_ip || 'none'} />
|
||||
<ListItemText primary={LL.NETWORK_GATEWAY()} secondary={data.gateway_ip || 'none'} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
@@ -154,7 +158,7 @@ const NetworkStatusForm: FC = () => {
|
||||
<DnsIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="DNS Server IP" secondary={dnsServers(data)} />
|
||||
<ListItemText primary={LL.NETWORK_DNS()} secondary={dnsServers(data)} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
</>
|
||||
@@ -162,7 +166,7 @@ const NetworkStatusForm: FC = () => {
|
||||
</List>
|
||||
<ButtonRow>
|
||||
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
|
||||
Refresh
|
||||
{LL.REFRESH()}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
</>
|
||||
@@ -170,7 +174,7 @@ const NetworkStatusForm: FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title="Network Status" titleGutter>
|
||||
<SectionContent title={LL.STATUS_OF(LL.NETWORK(1))} titleGutter>
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { useEffect, FC, useState, useCallback, useRef } from 'react';
|
||||
import { useSnackbar } from 'notistack';
|
||||
|
||||
import { AxiosError } from 'axios';
|
||||
|
||||
import { Button } from '@mui/material';
|
||||
import PermScanWifiIcon from '@mui/icons-material/PermScanWifi';
|
||||
|
||||
@@ -12,6 +10,8 @@ import { ButtonRow, FormLoader, SectionContent } from '../../components';
|
||||
|
||||
import WiFiNetworkSelector from './WiFiNetworkSelector';
|
||||
|
||||
import { useI18nContext } from '../../i18n/i18n-react';
|
||||
|
||||
const NUM_POLLS = 10;
|
||||
const POLLING_FREQUENCY = 500;
|
||||
|
||||
@@ -22,6 +22,8 @@ const compareNetworks = (network1: WiFiNetwork, network2: WiFiNetwork) => {
|
||||
};
|
||||
|
||||
const WiFiNetworkScanner: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const pollCount = useRef(0);
|
||||
@@ -46,21 +48,21 @@ const WiFiNetworkScanner: FC = () => {
|
||||
pollCount.current = completedPollCount;
|
||||
setTimeout(pollNetworkList, POLLING_FREQUENCY);
|
||||
} else {
|
||||
finishedWithError('Device did not return network list in timely manner');
|
||||
finishedWithError(LL.PROBLEM_LOADING());
|
||||
}
|
||||
} else {
|
||||
const newNetworkList = response.data;
|
||||
newNetworkList.networks.sort(compareNetworks);
|
||||
setNetworkList(newNetworkList);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
finishedWithError('Problem listing WiFi networks ' + error.response?.data.message);
|
||||
} catch (error) {
|
||||
if (error.response) {
|
||||
finishedWithError(LL.PROBLEM_LOADING() + ' ' + error.response?.data.message);
|
||||
} else {
|
||||
finishedWithError('Problem listing WiFi networks');
|
||||
finishedWithError(LL.PROBLEM_LOADING());
|
||||
}
|
||||
}
|
||||
}, [finishedWithError]);
|
||||
}, [finishedWithError, LL]);
|
||||
|
||||
const startNetworkScan = useCallback(async () => {
|
||||
pollCount.current = 0;
|
||||
@@ -69,14 +71,14 @@ const WiFiNetworkScanner: FC = () => {
|
||||
try {
|
||||
await NetworkApi.scanNetworks();
|
||||
setTimeout(pollNetworkList, POLLING_FREQUENCY);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
finishedWithError('Problem scanning for WiFi networks ' + error.response?.data.message);
|
||||
} catch (error) {
|
||||
if (error.response) {
|
||||
finishedWithError(LL.PROBLEM_LOADING() + ' ' + error.response?.data.message);
|
||||
} else {
|
||||
finishedWithError('Problem scanning for WiFi networks');
|
||||
finishedWithError(LL.PROBLEM_LOADING());
|
||||
}
|
||||
}
|
||||
}, [finishedWithError, pollNetworkList]);
|
||||
}, [finishedWithError, pollNetworkList, LL]);
|
||||
|
||||
useEffect(() => {
|
||||
startNetworkScan();
|
||||
@@ -84,13 +86,13 @@ const WiFiNetworkScanner: FC = () => {
|
||||
|
||||
const renderNetworkScanner = () => {
|
||||
if (!networkList) {
|
||||
return <FormLoader message="Scanning…" errorMessage={errorMessage} />;
|
||||
return <FormLoader message={LL.SCANNING() + '...'} errorMessage={errorMessage} />;
|
||||
}
|
||||
return <WiFiNetworkSelector networkList={networkList} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title="Network Scanner">
|
||||
<SectionContent title={LL.NETWORK_SCANNER()}>
|
||||
{renderNetworkScanner()}
|
||||
<ButtonRow>
|
||||
<Button
|
||||
@@ -100,7 +102,7 @@ const WiFiNetworkScanner: FC = () => {
|
||||
onClick={startNetworkScan}
|
||||
disabled={!errorMessage && !networkList}
|
||||
>
|
||||
Scan again…
|
||||
{LL.SCAN_AGAIN()}…
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
</SectionContent>
|
||||
|
||||
@@ -12,6 +12,8 @@ import { WiFiEncryptionType, WiFiNetwork, WiFiNetworkList } from '../../types';
|
||||
|
||||
import { WiFiConnectionContext } from './WiFiConnectionContext';
|
||||
|
||||
import { useI18nContext } from '../../i18n/i18n-react';
|
||||
|
||||
interface WiFiNetworkSelectorProps {
|
||||
networkList: WiFiNetworkList;
|
||||
}
|
||||
@@ -39,6 +41,8 @@ export const networkSecurityMode = ({ encryption_type }: WiFiNetwork) => {
|
||||
};
|
||||
|
||||
const WiFiNetworkSelector: FC<WiFiNetworkSelectorProps> = ({ networkList }) => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const wifiConnectionContext = useContext(WiFiConnectionContext);
|
||||
|
||||
const renderNetwork = (network: WiFiNetwork) => {
|
||||
@@ -61,7 +65,7 @@ const WiFiNetworkSelector: FC<WiFiNetworkSelectorProps> = ({ networkList }) => {
|
||||
};
|
||||
|
||||
if (networkList.networks.length === 0) {
|
||||
return <MessageBox mt={2} mb={1} message="No WiFi networks found" level="info" />;
|
||||
return <MessageBox mt={2} mb={1} message={LL.NETWORK_NO_WIFI()} level="info" />;
|
||||
}
|
||||
|
||||
return <List>{networkList.networks.map(renderNetwork)}</List>;
|
||||
|
||||
@@ -12,12 +12,16 @@ import * as NTPApi from '../../api/ntp';
|
||||
import { selectedTimeZone, timeZoneSelectItems, TIME_ZONES } from './TZ';
|
||||
import { NTP_SETTINGS_VALIDATOR } from '../../validators/ntp';
|
||||
|
||||
import { useI18nContext } from '../../i18n/i18n-react';
|
||||
|
||||
const NTPSettingsForm: FC = () => {
|
||||
const { loadData, saving, data, setData, saveData, errorMessage } = useRest<NTPSettings>({
|
||||
read: NTPApi.readNTPSettings,
|
||||
update: NTPApi.updateNTPSettings
|
||||
});
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const updateFormValue = updateValue(setData);
|
||||
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
@@ -49,12 +53,12 @@ const NTPSettingsForm: FC = () => {
|
||||
<>
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox name="enabled" checked={data.enabled} onChange={updateFormValue} />}
|
||||
label="Enable NTP"
|
||||
label={LL.ENABLE_NTP()}
|
||||
/>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="server"
|
||||
label="Server"
|
||||
label={LL.NTP_SERVER()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.server}
|
||||
@@ -64,7 +68,7 @@ const NTPSettingsForm: FC = () => {
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="tz_label"
|
||||
label="Time zone"
|
||||
label={LL.TIME_ZONE()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={selectedTimeZone(data.tz_label, data.tz_format)}
|
||||
@@ -72,7 +76,7 @@ const NTPSettingsForm: FC = () => {
|
||||
margin="normal"
|
||||
select
|
||||
>
|
||||
<MenuItem disabled>Time zone...</MenuItem>
|
||||
<MenuItem disabled>{LL.TIME_ZONE()}...</MenuItem>
|
||||
{timeZoneSelectItems()}
|
||||
</ValidatedTextField>
|
||||
<ButtonRow>
|
||||
@@ -84,7 +88,7 @@ const NTPSettingsForm: FC = () => {
|
||||
type="submit"
|
||||
onClick={validateAndSubmit}
|
||||
>
|
||||
Save
|
||||
{LL.SAVE()}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
</>
|
||||
@@ -92,7 +96,7 @@ const NTPSettingsForm: FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title="NTP Settings" titleGutter>
|
||||
<SectionContent title={LL.SETTINGS_OF('NTP')} titleGutter>
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
|
||||
@@ -16,7 +16,8 @@ import {
|
||||
ListItemText,
|
||||
TextField,
|
||||
Theme,
|
||||
useTheme
|
||||
useTheme,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
||||
@@ -31,6 +32,8 @@ import { ButtonRow, FormLoader, SectionContent } from '../../components';
|
||||
import { extractErrorMessage, formatDateTime, formatLocalDateTime, useRest } from '../../utils';
|
||||
import { AuthenticatedContext } from '../../contexts/authentication';
|
||||
|
||||
import { useI18nContext } from '../../i18n/i18n-react';
|
||||
|
||||
export const isNtpActive = ({ status }: NTPStatus) => status === NTPSyncStatus.NTP_ACTIVE;
|
||||
export const isNtpEnabled = ({ status }: NTPStatus) => status !== NTPSyncStatus.NTP_DISABLED;
|
||||
|
||||
@@ -47,19 +50,6 @@ export const ntpStatusHighlight = ({ status }: NTPStatus, theme: Theme) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const ntpStatus = ({ status }: NTPStatus) => {
|
||||
switch (status) {
|
||||
case NTPSyncStatus.NTP_DISABLED:
|
||||
return 'Disabled';
|
||||
case NTPSyncStatus.NTP_INACTIVE:
|
||||
return 'Inactive';
|
||||
case NTPSyncStatus.NTP_ACTIVE:
|
||||
return 'Active';
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
};
|
||||
|
||||
const NTPStatusForm: FC = () => {
|
||||
const { loadData, data, errorMessage } = useRest<NTPStatus>({ read: NTPApi.readNTPStatus });
|
||||
const [localTime, setLocalTime] = useState<string>('');
|
||||
@@ -68,6 +58,8 @@ const NTPStatusForm: FC = () => {
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
const { me } = useContext(AuthenticatedContext);
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const updateLocalTime = (event: React.ChangeEvent<HTMLInputElement>) => setLocalTime(event.target.value);
|
||||
|
||||
const openSetTime = () => {
|
||||
@@ -77,59 +69,71 @@ const NTPStatusForm: FC = () => {
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const ntpStatus = ({ status }: NTPStatus) => {
|
||||
switch (status) {
|
||||
case NTPSyncStatus.NTP_DISABLED:
|
||||
return LL.DISABLED(0);
|
||||
case NTPSyncStatus.NTP_INACTIVE:
|
||||
return LL.INACTIVE(0);
|
||||
case NTPSyncStatus.NTP_ACTIVE:
|
||||
return LL.ACTIVE();
|
||||
default:
|
||||
return LL.UNKNOWN();
|
||||
}
|
||||
};
|
||||
|
||||
const configureTime = async () => {
|
||||
setProcessing(true);
|
||||
try {
|
||||
await NTPApi.updateTime({
|
||||
local_time: formatLocalDateTime(new Date(localTime))
|
||||
});
|
||||
enqueueSnackbar('Time set', { variant: 'success' });
|
||||
enqueueSnackbar(LL.TIME_SET(), { variant: 'success' });
|
||||
setSettingTime(false);
|
||||
loadData();
|
||||
} catch (error: unknown) {
|
||||
enqueueSnackbar(extractErrorMessage(error, 'Problem updating time'), { variant: 'error' });
|
||||
} catch (error) {
|
||||
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderSetTimeDialog = () => {
|
||||
return (
|
||||
<Dialog open={settingTime} onClose={() => setSettingTime(false)}>
|
||||
<DialogTitle>Set Time</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Box mb={2}>Enter local date and time below to set the device's time.</Box>
|
||||
<TextField
|
||||
label="Local Time"
|
||||
type="datetime-local"
|
||||
value={localTime}
|
||||
onChange={updateLocalTime}
|
||||
disabled={processing}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
InputLabelProps={{
|
||||
shrink: true
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button startIcon={<CancelIcon />} variant="outlined" onClick={() => setSettingTime(false)} color="secondary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<AccessTimeIcon />}
|
||||
variant="outlined"
|
||||
onClick={configureTime}
|
||||
disabled={processing}
|
||||
color="primary"
|
||||
autoFocus
|
||||
>
|
||||
Set Time
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
const renderSetTimeDialog = () => (
|
||||
<Dialog open={settingTime} onClose={() => setSettingTime(false)}>
|
||||
<DialogTitle>{LL.SET_TIME(1)}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Box color="warning.main" p={0} pl={0} pr={0} mt={0} mb={2}>
|
||||
<Typography variant="body2">{LL.SET_TIME_TEXT()}</Typography>
|
||||
</Box>
|
||||
<TextField
|
||||
label={LL.LOCAL_TIME()}
|
||||
type="datetime-local"
|
||||
value={localTime}
|
||||
onChange={updateLocalTime}
|
||||
disabled={processing}
|
||||
fullWidth
|
||||
InputLabelProps={{
|
||||
shrink: true
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button startIcon={<CancelIcon />} variant="outlined" onClick={() => setSettingTime(false)} color="secondary">
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<AccessTimeIcon />}
|
||||
variant="outlined"
|
||||
onClick={configureTime}
|
||||
disabled={processing}
|
||||
color="primary"
|
||||
autoFocus
|
||||
>
|
||||
{LL.SAVE()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
@@ -145,7 +149,7 @@ const NTPStatusForm: FC = () => {
|
||||
<UpdateIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="Status" secondary={ntpStatus(data)} />
|
||||
<ListItemText primary={LL.STATUS_OF('')} secondary={ntpStatus(data)} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
{isNtpEnabled(data) && (
|
||||
@@ -156,7 +160,7 @@ const NTPStatusForm: FC = () => {
|
||||
<DnsIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="NTP Server" secondary={data.server} />
|
||||
<ListItemText primary={LL.NTP_SERVER()} secondary={data.server} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
</>
|
||||
@@ -167,7 +171,7 @@ const NTPStatusForm: FC = () => {
|
||||
<AccessTimeIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="Local Time" secondary={formatDateTime(data.local_time)} />
|
||||
<ListItemText primary={LL.LOCAL_TIME()} secondary={formatDateTime(data.local_time)} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
@@ -176,7 +180,7 @@ const NTPStatusForm: FC = () => {
|
||||
<SwapVerticalCircleIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="UTC Time" secondary={formatDateTime(data.utc_time)} />
|
||||
<ListItemText primary={LL.UTC_TIME()} secondary={formatDateTime(data.utc_time)} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
</List>
|
||||
@@ -184,7 +188,7 @@ const NTPStatusForm: FC = () => {
|
||||
<Box flexGrow={1}>
|
||||
<ButtonRow>
|
||||
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
|
||||
Refresh
|
||||
{LL.REFRESH()}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
</Box>
|
||||
@@ -192,7 +196,7 @@ const NTPStatusForm: FC = () => {
|
||||
<Box flexWrap="nowrap" whiteSpace="nowrap">
|
||||
<ButtonRow>
|
||||
<Button onClick={openSetTime} variant="outlined" color="primary" startIcon={<AccessTimeIcon />}>
|
||||
Set Time
|
||||
{LL.SET_TIME(0)}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
</Box>
|
||||
@@ -204,7 +208,7 @@ const NTPStatusForm: FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title="NTP Status" titleGutter>
|
||||
<SectionContent title={LL.STATUS_OF('NTP')} titleGutter>
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
|
||||
@@ -9,8 +9,11 @@ import { AuthenticatedContext } from '../../contexts/authentication';
|
||||
import NTPStatusForm from './NTPStatusForm';
|
||||
import NTPSettingsForm from './NTPSettingsForm';
|
||||
|
||||
import { useI18nContext } from '../../i18n/i18n-react';
|
||||
|
||||
const NetworkTime: FC = () => {
|
||||
useLayoutTitle('Network Time');
|
||||
const { LL } = useI18nContext();
|
||||
useLayoutTitle('NTP');
|
||||
|
||||
const authenticatedContext = useContext(AuthenticatedContext);
|
||||
const { routerTab } = useRouterTab();
|
||||
@@ -18,8 +21,8 @@ const NetworkTime: FC = () => {
|
||||
return (
|
||||
<>
|
||||
<RouterTabs value={routerTab}>
|
||||
<Tab value="status" label="NTP Status" />
|
||||
<Tab value="settings" label="NTP Settings" disabled={!authenticatedContext.me.admin} />
|
||||
<Tab value="status" label={LL.STATUS_OF('NTP')} />
|
||||
<Tab value="settings" label={LL.SETTINGS_OF('NTP')} disabled={!authenticatedContext.me.admin} />
|
||||
</RouterTabs>
|
||||
<Routes>
|
||||
<Route path="status" element={<NTPStatusForm />} />
|
||||
|
||||
@@ -19,6 +19,8 @@ import { MessageBox } from '../../components';
|
||||
import * as SecurityApi from '../../api/security';
|
||||
import { Token } from '../../types';
|
||||
|
||||
import { useI18nContext } from '../../i18n/i18n-react';
|
||||
|
||||
interface GenerateTokenProps {
|
||||
username?: string;
|
||||
onClose: () => void;
|
||||
@@ -28,15 +30,17 @@ const GenerateToken: FC<GenerateTokenProps> = ({ username, onClose }) => {
|
||||
const [token, setToken] = useState<Token>();
|
||||
const open = !!username;
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const getToken = useCallback(async () => {
|
||||
try {
|
||||
setToken((await SecurityApi.generateToken(username)).data);
|
||||
} catch (error: unknown) {
|
||||
enqueueSnackbar(extractErrorMessage(error, 'Problem generating token'), { variant: 'error' });
|
||||
} catch (error) {
|
||||
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
|
||||
}
|
||||
}, [username, enqueueSnackbar]);
|
||||
}, [username, enqueueSnackbar, LL]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
@@ -46,16 +50,11 @@ const GenerateToken: FC<GenerateTokenProps> = ({ username, onClose }) => {
|
||||
|
||||
return (
|
||||
<Dialog onClose={onClose} aria-labelledby="generate-token-dialog-title" open={!!username} fullWidth maxWidth="sm">
|
||||
<DialogTitle id="generate-token-dialog-title">Access Token for {username}</DialogTitle>
|
||||
<DialogTitle id="generate-token-dialog-title">{LL.ACCESS_TOKEN_FOR() + ' ' + username}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
{token ? (
|
||||
<>
|
||||
<MessageBox
|
||||
message="The token below is used with REST API calls that require authorization. It can be passed either as a Bearer token in the
|
||||
'Authorization' header or in the 'access_token' URL query parameter."
|
||||
level="info"
|
||||
my={2}
|
||||
/>
|
||||
<MessageBox message={LL.ACCESS_TOKEN_TEXT()} level="info" my={2} />
|
||||
<Box mt={2} mb={2}>
|
||||
<TextField label="Token" multiline value={token.token} fullWidth contentEditable={false} />
|
||||
</Box>
|
||||
@@ -63,13 +62,13 @@ const GenerateToken: FC<GenerateTokenProps> = ({ username, onClose }) => {
|
||||
) : (
|
||||
<Box m={4} textAlign="center">
|
||||
<LinearProgress />
|
||||
<Typography variant="h6">Generating token…</Typography>
|
||||
<Typography variant="h6">{LL.GENERATING_TOKEN()}…</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button startIcon={<CloseIcon />} variant="outlined" onClick={onClose} color="secondary">
|
||||
Close
|
||||
{LL.CLOSE()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -20,6 +20,8 @@ import { createUserValidator } from '../../validators';
|
||||
import { useRest } from '../../utils';
|
||||
import { AuthenticatedContext } from '../../contexts/authentication';
|
||||
|
||||
import { useI18nContext } from '../../i18n/i18n-react';
|
||||
|
||||
import GenerateToken from './GenerateToken';
|
||||
import UserForm from './UserForm';
|
||||
|
||||
@@ -34,9 +36,11 @@ const ManageUsersForm: FC = () => {
|
||||
const [generatingToken, setGeneratingToken] = useState<string>();
|
||||
const authenticatedContext = useContext(AuthenticatedContext);
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const table_theme = useTheme({
|
||||
Table: `
|
||||
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) 90px 120px;
|
||||
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) minmax(120px, max-content) 120px;
|
||||
`,
|
||||
BaseRow: `
|
||||
font-size: 14px;
|
||||
@@ -136,8 +140,8 @@ const ManageUsersForm: FC = () => {
|
||||
<>
|
||||
<Header>
|
||||
<HeaderRow>
|
||||
<HeaderCell resize>USERNAME</HeaderCell>
|
||||
<HeaderCell stiff>IS ADMIN</HeaderCell>
|
||||
<HeaderCell resize>{LL.USERNAME(1)}</HeaderCell>
|
||||
<HeaderCell stiff>{LL.IS_ADMIN(0)}</HeaderCell>
|
||||
<HeaderCell stiff />
|
||||
</HeaderRow>
|
||||
</Header>
|
||||
@@ -169,9 +173,7 @@ const ManageUsersForm: FC = () => {
|
||||
)}
|
||||
</Table>
|
||||
|
||||
{noAdminConfigured() && (
|
||||
<MessageBox level="warning" message="You must have at least one admin user configured" my={2} />
|
||||
)}
|
||||
{noAdminConfigured() && <MessageBox level="warning" message={LL.USER_WARNING()} my={2} />}
|
||||
|
||||
<Box display="flex" flexWrap="wrap">
|
||||
<Box flexGrow={1} sx={{ '& button': { mt: 2 } }}>
|
||||
@@ -183,14 +185,14 @@ const ManageUsersForm: FC = () => {
|
||||
type="submit"
|
||||
onClick={onSubmit}
|
||||
>
|
||||
Save
|
||||
{LL.SAVE()}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Box flexWrap="nowrap" whiteSpace="nowrap">
|
||||
<ButtonRow>
|
||||
<Button startIcon={<PersonAddIcon />} variant="outlined" color="secondary" onClick={createUser}>
|
||||
Add
|
||||
{LL.ADD(0)}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
</Box>
|
||||
@@ -210,7 +212,7 @@ const ManageUsersForm: FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title="Manage Users" titleGutter>
|
||||
<SectionContent title={LL.MANAGE_USERS()} titleGutter>
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
|
||||
@@ -8,16 +8,19 @@ import { RouterTabs, useRouterTab, useLayoutTitle } from '../../components';
|
||||
import SecuritySettingsForm from './SecuritySettingsForm';
|
||||
import ManageUsersForm from './ManageUsersForm';
|
||||
|
||||
import { useI18nContext } from '../../i18n/i18n-react';
|
||||
|
||||
const Security: FC = () => {
|
||||
useLayoutTitle('Security');
|
||||
const { LL } = useI18nContext();
|
||||
useLayoutTitle(LL.SECURITY(0));
|
||||
|
||||
const { routerTab } = useRouterTab();
|
||||
|
||||
return (
|
||||
<>
|
||||
<RouterTabs value={routerTab}>
|
||||
<Tab value="users" label="Manage Users" />
|
||||
<Tab value="settings" label="Security Settings" />
|
||||
<Tab value="users" label={LL.MANAGE_USERS()} />
|
||||
<Tab value="settings" label={LL.SETTINGS_OF(LL.SECURITY(1))} />
|
||||
</RouterTabs>
|
||||
<Routes>
|
||||
<Route path="users" element={<ManageUsersForm />} />
|
||||
|
||||
@@ -11,7 +11,11 @@ import { SECURITY_SETTINGS_VALIDATOR, validate } from '../../validators';
|
||||
import { updateValue, useRest } from '../../utils';
|
||||
import { AuthenticatedContext } from '../../contexts/authentication';
|
||||
|
||||
import { useI18nContext } from '../../i18n/i18n-react';
|
||||
|
||||
const SecuritySettingsForm: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
const { loadData, saving, data, setData, saveData, errorMessage } = useRest<SecuritySettings>({
|
||||
read: SecurityApi.readSecuritySettings,
|
||||
@@ -42,18 +46,14 @@ const SecuritySettingsForm: FC = () => {
|
||||
<ValidatedPasswordField
|
||||
fieldErrors={fieldErrors}
|
||||
name="jwt_secret"
|
||||
label="su Password"
|
||||
label={LL.SU_PASSWORD()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.jwt_secret}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
<MessageBox
|
||||
level="info"
|
||||
message="The su (super user) password is used to sign authentication tokens and also enable admin privileges within the Console."
|
||||
mt={1}
|
||||
/>
|
||||
<MessageBox level="info" message={LL.SU_TEXT()} mt={1} />
|
||||
<ButtonRow>
|
||||
<Button
|
||||
startIcon={<SaveIcon />}
|
||||
@@ -63,7 +63,7 @@ const SecuritySettingsForm: FC = () => {
|
||||
type="submit"
|
||||
onClick={validateAndSubmit}
|
||||
>
|
||||
Save
|
||||
{LL.SAVE()}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
</>
|
||||
@@ -71,7 +71,7 @@ const SecuritySettingsForm: FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title="Security Settings" titleGutter>
|
||||
<SectionContent title={LL.SETTINGS_OF(LL.SECURITY(1))} titleGutter>
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import Schema, { ValidateFieldsError } from 'async-validator';
|
||||
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import PersonAddIcon from '@mui/icons-material/PersonAdd';
|
||||
import SaveIcon from '@mui/icons-material/Save';
|
||||
|
||||
import { Button, Checkbox, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material';
|
||||
|
||||
@@ -11,6 +12,8 @@ import { User } from '../../types';
|
||||
import { updateValue } from '../../utils';
|
||||
import { validate } from '../../validators';
|
||||
|
||||
import { useI18nContext } from '../../i18n/i18n-react';
|
||||
|
||||
interface UserFormProps {
|
||||
creating: boolean;
|
||||
validator: Schema;
|
||||
@@ -23,6 +26,8 @@ interface UserFormProps {
|
||||
}
|
||||
|
||||
const UserForm: FC<UserFormProps> = ({ creating, validator, user, setUser, onDoneEditing, onCancelEditing }) => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const updateFormValue = updateValue(setUser);
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
const open = !!user;
|
||||
@@ -49,12 +54,14 @@ const UserForm: FC<UserFormProps> = ({ creating, validator, user, setUser, onDon
|
||||
<Dialog onClose={onCancelEditing} open={!!user} fullWidth maxWidth="sm">
|
||||
{user && (
|
||||
<>
|
||||
<DialogTitle id="user-form-dialog-title">{creating ? 'Add' : 'Modify'} User</DialogTitle>
|
||||
<DialogTitle id="user-form-dialog-title">
|
||||
{creating ? LL.ADD(1) : LL.MODIFY()} {LL.USER(1)}
|
||||
</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="username"
|
||||
label="Username"
|
||||
label={LL.USERNAME(1)}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={user.username}
|
||||
@@ -65,7 +72,7 @@ const UserForm: FC<UserFormProps> = ({ creating, validator, user, setUser, onDon
|
||||
<ValidatedPasswordField
|
||||
fieldErrors={fieldErrors}
|
||||
name="password"
|
||||
label="Password"
|
||||
label={LL.PASSWORD()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={user.password}
|
||||
@@ -74,21 +81,21 @@ const UserForm: FC<UserFormProps> = ({ creating, validator, user, setUser, onDon
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox name="admin" checked={user.admin} onChange={updateFormValue} />}
|
||||
label="is Admin?"
|
||||
label={LL.IS_ADMIN(1)}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button startIcon={<CancelIcon />} variant="outlined" onClick={onCancelEditing} color="secondary">
|
||||
Cancel
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<PersonAddIcon />}
|
||||
startIcon={creating ? <PersonAddIcon /> : <SaveIcon />}
|
||||
variant="outlined"
|
||||
onClick={validateAndDone}
|
||||
color="primary"
|
||||
autoFocus
|
||||
>
|
||||
Add
|
||||
{creating ? LL.ADD(0) : LL.SAVE()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { AxiosPromise } from 'axios';
|
||||
import { Typography, Button, Box } from '@mui/material';
|
||||
|
||||
import { FileUploadConfig } from '../../api/endpoints';
|
||||
|
||||
import { SingleUpload, useFileUpload } from '../../components';
|
||||
|
||||
import DownloadIcon from '@mui/icons-material/GetApp';
|
||||
@@ -14,15 +15,19 @@ import { extractErrorMessage } from '../../utils';
|
||||
|
||||
import * as EMSESP from '../../project/api';
|
||||
|
||||
import { useI18nContext } from '../../i18n/i18n-react';
|
||||
|
||||
interface UploadFileProps {
|
||||
uploadGeneralFile: (file: File, config?: FileUploadConfig) => AxiosPromise<void>;
|
||||
}
|
||||
|
||||
const GeneralFileUpload: FC<UploadFileProps> = ({ uploadGeneralFile }) => {
|
||||
const [uploadFile, cancelUpload, uploading, uploadProgress] = useFileUpload({ upload: uploadGeneralFile });
|
||||
const [uploadFile, cancelUpload, uploading, uploadProgress, md5] = useFileUpload({ upload: uploadGeneralFile });
|
||||
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const saveFile = (json: any, endpoint: string) => {
|
||||
const a = document.createElement('a');
|
||||
const filename = 'emsesp_' + endpoint + '.json';
|
||||
@@ -35,19 +40,19 @@ const GeneralFileUpload: FC<UploadFileProps> = ({ uploadGeneralFile }) => {
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
enqueueSnackbar('File downloaded', { variant: 'info' });
|
||||
enqueueSnackbar(LL.DOWNLOAD_SUCCESSFUL(), { variant: 'info' });
|
||||
};
|
||||
|
||||
const downloadSettings = async () => {
|
||||
try {
|
||||
const response = await EMSESP.getSettings();
|
||||
if (response.status !== 200) {
|
||||
enqueueSnackbar('Unable to get settings', { variant: 'error' });
|
||||
enqueueSnackbar(LL.PROBLEM_LOADING(), { variant: 'error' });
|
||||
} else {
|
||||
saveFile(response.data, 'settings');
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
enqueueSnackbar(extractErrorMessage(error, 'Problem with downloading'), { variant: 'error' });
|
||||
} catch (error) {
|
||||
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_LOADING()), { variant: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -55,47 +60,50 @@ const GeneralFileUpload: FC<UploadFileProps> = ({ uploadGeneralFile }) => {
|
||||
try {
|
||||
const response = await EMSESP.getCustomizations();
|
||||
if (response.status !== 200) {
|
||||
enqueueSnackbar('Unable to get customizations', { variant: 'error' });
|
||||
enqueueSnackbar(LL.PROBLEM_LOADING(), { variant: 'error' });
|
||||
} else {
|
||||
saveFile(response.data, 'customizations');
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
enqueueSnackbar(extractErrorMessage(error, 'Problem with downloading'), { variant: 'error' });
|
||||
} catch (error) {
|
||||
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_LOADING()), { variant: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
|
||||
Upload
|
||||
</Typography>
|
||||
{!uploading && (
|
||||
<Box mb={2} color="warning.main">
|
||||
<Typography variant="body2">
|
||||
Upload a new firmware (.bin) file, settings or customizations (.json) file below.
|
||||
<>
|
||||
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
|
||||
{LL.UPLOAD()}
|
||||
</Typography>
|
||||
<Box mb={2} color="warning.main">
|
||||
<Typography variant="body2">{LL.UPLOAD_TEXT()} </Typography>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
{md5 !== '' && (
|
||||
<Box mb={2}>
|
||||
<Typography variant="body2">{'MD5: ' + md5}</Typography>
|
||||
</Box>
|
||||
)}
|
||||
<SingleUpload onDrop={uploadFile} onCancel={cancelUpload} uploading={uploading} progress={uploadProgress} />
|
||||
|
||||
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
|
||||
Download
|
||||
</Typography>
|
||||
{!uploading && (
|
||||
<>
|
||||
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
|
||||
{LL.DOWNLOAD(0)}
|
||||
</Typography>
|
||||
<Box color="warning.main">
|
||||
<Typography mb={1} variant="body2">
|
||||
Download the application settings. Be careful when sharing your settings as this file contains passwords
|
||||
and other sensitive system information.
|
||||
{LL.DOWNLOAD_SETTINGS_TEXT()}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button startIcon={<DownloadIcon />} variant="outlined" color="primary" onClick={() => downloadSettings()}>
|
||||
settings
|
||||
{LL.SETTINGS_OF('')}
|
||||
</Button>
|
||||
|
||||
<Box color="warning.main">
|
||||
<Typography mt={2} mb={1} variant="body2">
|
||||
Download the entity customizations.
|
||||
{LL.DOWNLOAD_CUSTOMIZATION_TEXT()}{' '}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
@@ -104,7 +112,7 @@ const GeneralFileUpload: FC<UploadFileProps> = ({ uploadGeneralFile }) => {
|
||||
color="primary"
|
||||
onClick={() => downloadCustomizations()}
|
||||
>
|
||||
customizations
|
||||
{LL.CUSTOMIZATION()}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
ValidatedPasswordField,
|
||||
ValidatedTextField
|
||||
} from '../../components';
|
||||
|
||||
import { OTASettings } from '../../types';
|
||||
import { numberValue, updateValue, useRest } from '../../utils';
|
||||
|
||||
@@ -19,12 +20,16 @@ import { ValidateFieldsError } from 'async-validator';
|
||||
import { validate } from '../../validators';
|
||||
import { OTA_SETTINGS_VALIDATOR } from '../../validators/system';
|
||||
|
||||
import { useI18nContext } from '../../i18n/i18n-react';
|
||||
|
||||
const OTASettingsForm: FC = () => {
|
||||
const { loadData, saving, data, setData, saveData, errorMessage } = useRest<OTASettings>({
|
||||
read: SystemApi.readOTASettings,
|
||||
update: SystemApi.updateOTASettings
|
||||
});
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const updateFormValue = updateValue(setData);
|
||||
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
@@ -48,7 +53,7 @@ const OTASettingsForm: FC = () => {
|
||||
<>
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox name="enabled" checked={data.enabled} onChange={updateFormValue} />}
|
||||
label="Enable OTA Updates"
|
||||
label={LL.ENABLE_OTA()}
|
||||
/>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
@@ -64,7 +69,7 @@ const OTASettingsForm: FC = () => {
|
||||
<ValidatedPasswordField
|
||||
fieldErrors={fieldErrors}
|
||||
name="password"
|
||||
label="Password"
|
||||
label={LL.PASSWORD()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.password}
|
||||
@@ -80,7 +85,7 @@ const OTASettingsForm: FC = () => {
|
||||
type="submit"
|
||||
onClick={validateAndSubmit}
|
||||
>
|
||||
Save
|
||||
{LL.SAVE()}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
</>
|
||||
@@ -88,7 +93,7 @@ const OTASettingsForm: FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title="OTA Settings" titleGutter>
|
||||
<SectionContent title={LL.SETTINGS_OF('OTA')} titleGutter>
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
|
||||
@@ -4,6 +4,8 @@ import { FC, useRef, useState } from 'react';
|
||||
import * as SystemApi from '../../api/system';
|
||||
import { FormLoader } from '../../components';
|
||||
|
||||
import { useI18nContext } from '../../i18n/i18n-react';
|
||||
|
||||
const RESTART_TIMEOUT = 2 * 60 * 1000;
|
||||
const POLL_TIMEOUT = 2000;
|
||||
const POLL_INTERVAL = 5000;
|
||||
@@ -12,12 +14,14 @@ const RestartMonitor: FC = () => {
|
||||
const [failed, setFailed] = useState<boolean>(false);
|
||||
const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout>();
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const timeoutAt = useRef(new Date().getTime() + RESTART_TIMEOUT);
|
||||
const poll = useRef(async () => {
|
||||
try {
|
||||
await SystemApi.readSystemStatus(POLL_TIMEOUT);
|
||||
document.location.href = '/fileUpdated';
|
||||
} catch (error: unknown) {
|
||||
} catch (error) {
|
||||
if (new Date().getTime() < timeoutAt.current) {
|
||||
setTimeoutId(setTimeout(poll.current, POLL_INTERVAL));
|
||||
} else {
|
||||
@@ -32,12 +36,7 @@ const RestartMonitor: FC = () => {
|
||||
|
||||
useEffect(() => () => timeoutId && clearTimeout(timeoutId), [timeoutId]);
|
||||
|
||||
return (
|
||||
<FormLoader
|
||||
message="EMS-ESP is restarting, please wait…"
|
||||
errorMessage={failed ? 'Timed out waiting for device to restart.' : undefined}
|
||||
/>
|
||||
);
|
||||
return <FormLoader message={LL.APPLICATION_RESTARTING() + '...'} errorMessage={failed ? 'Timed out' : undefined} />;
|
||||
};
|
||||
|
||||
export default RestartMonitor;
|
||||
|
||||
@@ -12,8 +12,12 @@ import OTASettingsForm from './OTASettingsForm';
|
||||
|
||||
import SystemLog from './SystemLog';
|
||||
|
||||
import { useI18nContext } from '../../i18n/i18n-react';
|
||||
|
||||
const System: FC = () => {
|
||||
useLayoutTitle('System');
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
useLayoutTitle(LL.SYSTEM(0));
|
||||
|
||||
const { me } = useContext(AuthenticatedContext);
|
||||
const { features } = useContext(FeaturesContext);
|
||||
@@ -22,11 +26,11 @@ const System: FC = () => {
|
||||
return (
|
||||
<>
|
||||
<RouterTabs value={routerTab}>
|
||||
<Tab value="status" label="System Status" />
|
||||
<Tab value="log" label="System Log" />
|
||||
<Tab value="status" label={LL.STATUS_OF(LL.SYSTEM(1))} />
|
||||
<Tab value="log" label={LL.LOG_OF(LL.SYSTEM(2))} />
|
||||
|
||||
{features.ota && <Tab value="ota" label="OTA Settings" disabled={!me.admin} />}
|
||||
{features.upload_firmware && <Tab value="upload" label="Upload/Download" disabled={!me.admin} />}
|
||||
{features.ota && <Tab value="ota" label={LL.SETTINGS_OF('OTA')} disabled={!me.admin} />}
|
||||
{features.upload_firmware && <Tab value="upload" label={LL.UPLOAD_DOWNLOAD()} disabled={!me.admin} />}
|
||||
</RouterTabs>
|
||||
<Routes>
|
||||
<Route path="status" element={<SystemStatusForm />} />
|
||||
|
||||
@@ -15,6 +15,9 @@ import DownloadIcon from '@mui/icons-material/GetApp';
|
||||
import { useSnackbar } from 'notistack';
|
||||
|
||||
import { EVENT_SOURCE_ROOT } from '../../api/endpoints';
|
||||
|
||||
import { useI18nContext } from '../../i18n/i18n-react';
|
||||
|
||||
export const LOG_EVENTSOURCE_URL = EVENT_SOURCE_ROOT + 'log';
|
||||
|
||||
const useWindowSize = () => {
|
||||
@@ -63,6 +66,8 @@ const levelLabel = (level: LogLevel) => {
|
||||
const SystemLog: FC = () => {
|
||||
useWindowSize();
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const { loadData, data, setData } = useRest<LogSettings>({
|
||||
read: SystemApi.readLogSettings
|
||||
});
|
||||
@@ -104,10 +109,10 @@ const SystemLog: FC = () => {
|
||||
compact: data.compact
|
||||
});
|
||||
if (response.status !== 200) {
|
||||
enqueueSnackbar('Problem applying log settings', { variant: 'error' });
|
||||
enqueueSnackbar(LL.PROBLEM_UPDATING(), { variant: 'error' });
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
enqueueSnackbar(extractErrorMessage(error, 'Problem applying log settings'), { variant: 'error' });
|
||||
} catch (error) {
|
||||
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -158,10 +163,10 @@ const SystemLog: FC = () => {
|
||||
const fetchLog = useCallback(async () => {
|
||||
try {
|
||||
setLogEntries((await SystemApi.readLogEntries()).data);
|
||||
} catch (error: unknown) {
|
||||
setErrorMessage(extractErrorMessage(error, 'Failed to fetch log'));
|
||||
} catch (error) {
|
||||
setErrorMessage(extractErrorMessage(error, LL.PROBLEM_LOADING()));
|
||||
}
|
||||
}, []);
|
||||
}, [LL]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchLog();
|
||||
@@ -197,7 +202,7 @@ const SystemLog: FC = () => {
|
||||
<Grid item xs={4}>
|
||||
<ValidatedTextField
|
||||
name="level"
|
||||
label="Log Level"
|
||||
label={LL.LOG_LEVEL()}
|
||||
value={data.level}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
@@ -205,6 +210,7 @@ const SystemLog: FC = () => {
|
||||
margin="normal"
|
||||
select
|
||||
>
|
||||
<MenuItem value={-1}>OFF</MenuItem>
|
||||
<MenuItem value={3}>ERROR</MenuItem>
|
||||
<MenuItem value={4}>WARNING</MenuItem>
|
||||
<MenuItem value={5}>NOTICE</MenuItem>
|
||||
@@ -214,7 +220,7 @@ const SystemLog: FC = () => {
|
||||
</ValidatedTextField>
|
||||
</Grid>
|
||||
<Grid item xs={3}>
|
||||
<FormLabel>Buffer size</FormLabel>
|
||||
<FormLabel>{LL.BUFFER_SIZE()}</FormLabel>
|
||||
<Slider
|
||||
value={data.max_messages}
|
||||
valueLabelDisplay="auto"
|
||||
@@ -235,12 +241,12 @@ const SystemLog: FC = () => {
|
||||
<Grid item>
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox checked={data.compact} onChange={updateFormValue} name="compact" />}
|
||||
label="Compact"
|
||||
label={LL.COMPACT()}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Button startIcon={<DownloadIcon />} variant="outlined" color="secondary" onClick={onDownload}>
|
||||
Export
|
||||
{LL.EXPORT()}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
@@ -273,7 +279,7 @@ const SystemLog: FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title="System Log" titleGutter id="log-window">
|
||||
<SectionContent title={LL.LOG_OF(LL.SYSTEM(2))} titleGutter id="log-window">
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
|
||||
@@ -22,6 +22,7 @@ import ShowChartIcon from '@mui/icons-material/ShowChart';
|
||||
import MemoryIcon from '@mui/icons-material/Memory';
|
||||
import AppsIcon from '@mui/icons-material/Apps';
|
||||
import SdStorageIcon from '@mui/icons-material/SdStorage';
|
||||
import SdCardAlertIcon from '@mui/icons-material/SdCardAlert';
|
||||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
|
||||
@@ -31,13 +32,16 @@ import TimerIcon from '@mui/icons-material/Timer';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
|
||||
import { ButtonRow, FormLoader, SectionContent, MessageBox } from '../../components';
|
||||
import { EspPlatform, SystemStatus, Version } from '../../types';
|
||||
import { SystemStatus, Version } from '../../types';
|
||||
import * as SystemApi from '../../api/system';
|
||||
import { extractErrorMessage, useRest } from '../../utils';
|
||||
|
||||
import { AuthenticatedContext } from '../../contexts/authentication';
|
||||
|
||||
import axios from 'axios';
|
||||
import RestartMonitor from './RestartMonitor';
|
||||
|
||||
import { useI18nContext } from '../../i18n/i18n-react';
|
||||
|
||||
export const VERSIONCHECK_ENDPOINT = 'https://api.github.com/repos/emsesp/EMS-ESP32/releases/latest';
|
||||
export const VERSIONCHECK_DEV_ENDPOINT = 'https://api.github.com/repos/emsesp/EMS-ESP32/releases/tags/latest';
|
||||
@@ -48,6 +52,9 @@ function formatNumber(num: number) {
|
||||
}
|
||||
|
||||
const SystemStatusForm: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
const [restarting, setRestarting] = useState<boolean>();
|
||||
|
||||
const { loadData, data, errorMessage } = useRest<SystemStatus>({ read: SystemApi.readSystemStatus });
|
||||
|
||||
const { me } = useContext(AuthenticatedContext);
|
||||
@@ -64,7 +71,7 @@ const SystemStatusForm: FC = () => {
|
||||
setLatestVersion({
|
||||
version: response.data.name,
|
||||
url: response.data.assets[1].browser_download_url,
|
||||
changelog: response.data.html_url
|
||||
changelog: response.data.assets[0].browser_download_url
|
||||
});
|
||||
});
|
||||
axios.get(VERSIONCHECK_DEV_ENDPOINT).then((response) => {
|
||||
@@ -79,10 +86,25 @@ const SystemStatusForm: FC = () => {
|
||||
const restart = async () => {
|
||||
setProcessing(true);
|
||||
try {
|
||||
await SystemApi.restart();
|
||||
enqueueSnackbar('EMS-ESP is restarting...', { variant: 'info' });
|
||||
} catch (error: unknown) {
|
||||
enqueueSnackbar(extractErrorMessage(error, 'Problem restarting device'), { variant: 'error' });
|
||||
const response = await SystemApi.restart();
|
||||
if (response.status === 200) {
|
||||
setRestarting(true);
|
||||
}
|
||||
} catch (error) {
|
||||
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_LOADING()), { variant: 'error' });
|
||||
} finally {
|
||||
setConfirmRestart(false);
|
||||
setProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const partition = async () => {
|
||||
setProcessing(true);
|
||||
try {
|
||||
await SystemApi.partition();
|
||||
setRestarting(true);
|
||||
} catch (error) {
|
||||
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_LOADING()), { variant: 'error' });
|
||||
} finally {
|
||||
setConfirmRestart(false);
|
||||
setProcessing(false);
|
||||
@@ -91,16 +113,17 @@ const SystemStatusForm: FC = () => {
|
||||
|
||||
const renderRestartDialog = () => (
|
||||
<Dialog open={confirmRestart} onClose={() => setConfirmRestart(false)}>
|
||||
<DialogTitle>Restart</DialogTitle>
|
||||
<DialogContent dividers>Are you sure you want to restart EMS-ESP?</DialogContent>
|
||||
<DialogTitle>{LL.RESTART()}</DialogTitle>
|
||||
<DialogContent dividers>{LL.RESTART_CONFIRM()}</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
onClick={() => setConfirmRestart(false)}
|
||||
disabled={processing}
|
||||
color="secondary"
|
||||
>
|
||||
Cancel
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<PowerSettingsNewIcon />}
|
||||
@@ -110,8 +133,19 @@ const SystemStatusForm: FC = () => {
|
||||
color="primary"
|
||||
autoFocus
|
||||
>
|
||||
Restart
|
||||
{LL.RESTART()}
|
||||
</Button>
|
||||
{data?.has_loader && (
|
||||
<Button
|
||||
startIcon={<PowerSettingsNewIcon />}
|
||||
variant="outlined"
|
||||
onClick={partition}
|
||||
disabled={processing}
|
||||
color="primary"
|
||||
>
|
||||
EMS-ESP-Loader
|
||||
</Button>
|
||||
)}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
@@ -119,22 +153,19 @@ const SystemStatusForm: FC = () => {
|
||||
const renderVersionDialog = () => {
|
||||
return (
|
||||
<Dialog open={showingVersion} onClose={() => setShowingVersion(false)}>
|
||||
<DialogTitle>Version Check</DialogTitle>
|
||||
<DialogTitle>{LL.VERSION_CHECK(1)}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<MessageBox
|
||||
my={0}
|
||||
level="info"
|
||||
message={'You are currently running EMS-ESP version ' + data?.emsesp_version}
|
||||
/>
|
||||
<MessageBox my={0} level="info" message={LL.SYSTEM_VERSION_RUNNING() + ' ' + data?.emsesp_version} />
|
||||
{latestVersion && (
|
||||
<Box mt={2} mb={2}>
|
||||
The latest <u>official</u> version is <b>{latestVersion.version}</b> (
|
||||
{LL.THE_LATEST()} <u>{LL.OFFICIAL()}</u> {LL.VERSION_IS()} <b>{latestVersion.version}</b>
|
||||
(
|
||||
<Link target="_blank" href={latestVersion.changelog} color="primary">
|
||||
{'release notes'}
|
||||
{LL.RELEASE_NOTES()}
|
||||
</Link>
|
||||
) (
|
||||
<Link target="_blank" href={latestVersion.url} color="primary">
|
||||
{'download'}
|
||||
{LL.DOWNLOAD(1)}
|
||||
</Link>
|
||||
)
|
||||
</Box>
|
||||
@@ -142,14 +173,15 @@ const SystemStatusForm: FC = () => {
|
||||
|
||||
{latestDevVersion && (
|
||||
<Box mt={2} mb={2}>
|
||||
The latest <u>development</u> version is <b>{latestDevVersion.version}</b>
|
||||
{LL.THE_LATEST()} <u>{LL.DEVELOPMENT()}</u> {LL.VERSION_IS()}
|
||||
<b>{latestDevVersion.version}</b>
|
||||
(
|
||||
<Link target="_blank" href={latestDevVersion.changelog} color="primary">
|
||||
{'release notes'}
|
||||
{LL.RELEASE_NOTES()}
|
||||
</Link>
|
||||
) (
|
||||
<Link target="_blank" href={latestDevVersion.url} color="primary">
|
||||
{'download'}
|
||||
{LL.DOWNLOAD(1)}
|
||||
</Link>
|
||||
)
|
||||
</Box>
|
||||
@@ -157,17 +189,17 @@ const SystemStatusForm: FC = () => {
|
||||
|
||||
<Box color="warning.main" p={0} pl={0} pr={0} mt={4} mb={0}>
|
||||
<Typography variant="body2">
|
||||
Use
|
||||
<Link target="_blank" href={uploadURL} color="primary">
|
||||
{'UPLOAD'}
|
||||
{LL.USE()}
|
||||
<Link href={uploadURL} color="primary">
|
||||
{LL.UPLOAD()}
|
||||
</Link>
|
||||
to apply the new firmware
|
||||
{LL.SYSTEM_APPLY_FIRMWARE()}
|
||||
</Typography>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="outlined" onClick={() => setShowingVersion(false)} color="secondary">
|
||||
Close
|
||||
{LL.CLOSE()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
@@ -178,9 +210,9 @@ const SystemStatusForm: FC = () => {
|
||||
setProcessing(true);
|
||||
try {
|
||||
await SystemApi.factoryReset();
|
||||
enqueueSnackbar('Device has been factory reset and will now restart', { variant: 'info' });
|
||||
} catch (error: unknown) {
|
||||
enqueueSnackbar(extractErrorMessage(error, 'Problem factory resetting the device'), { variant: 'error' });
|
||||
setRestarting(true);
|
||||
} catch (error) {
|
||||
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
|
||||
} finally {
|
||||
setConfirmFactoryReset(false);
|
||||
setProcessing(false);
|
||||
@@ -189,16 +221,17 @@ const SystemStatusForm: FC = () => {
|
||||
|
||||
const renderFactoryResetDialog = () => (
|
||||
<Dialog open={confirmFactoryReset} onClose={() => setConfirmFactoryReset(false)}>
|
||||
<DialogTitle>Factory Reset</DialogTitle>
|
||||
<DialogContent dividers>Are you sure you want to reset the device to its factory defaults?</DialogContent>
|
||||
<DialogTitle>{LL.FACTORY_RESET()}</DialogTitle>
|
||||
<DialogContent dividers>{LL.SYSTEM_FACTORY_TEXT_DIALOG()}</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
onClick={() => setConfirmFactoryReset(false)}
|
||||
disabled={processing}
|
||||
color="secondary"
|
||||
>
|
||||
Cancel
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<SettingsBackupRestoreIcon />}
|
||||
@@ -208,7 +241,7 @@ const SystemStatusForm: FC = () => {
|
||||
autoFocus
|
||||
color="error"
|
||||
>
|
||||
Factory Reset
|
||||
{LL.FACTORY_RESET()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
@@ -228,10 +261,10 @@ const SystemStatusForm: FC = () => {
|
||||
<BuildIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="EMS-ESP Version" secondary={'v' + data.emsesp_version} />
|
||||
<ListItemText primary={LL.EMS_ESP_VER()} secondary={'v' + data.emsesp_version} />
|
||||
{latestVersion && (
|
||||
<Button color="primary" onClick={() => setShowingVersion(true)}>
|
||||
Version Check
|
||||
{LL.VERSION_CHECK(0)}
|
||||
</Button>
|
||||
)}
|
||||
</ListItem>
|
||||
@@ -242,7 +275,7 @@ const SystemStatusForm: FC = () => {
|
||||
<DevicesIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="Device (Platform / SDK)" secondary={data.esp_platform + ' / ' + data.sdk_version} />
|
||||
<ListItemText primary={LL.PLATFORM()} secondary={data.esp_platform + ' / ' + data.sdk_version} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
@@ -251,7 +284,7 @@ const SystemStatusForm: FC = () => {
|
||||
<TimerIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="System Uptime" secondary={data.uptime} />
|
||||
<ListItemText primary={LL.UPTIME()} secondary={data.uptime} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
@@ -260,7 +293,7 @@ const SystemStatusForm: FC = () => {
|
||||
<ShowChartIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="CPU Frequency" secondary={data.cpu_freq_mhz + ' MHz'} />
|
||||
<ListItemText primary={LL.CPU_FREQ()} secondary={data.cpu_freq_mhz + ' MHz'} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
@@ -270,17 +303,11 @@ const SystemStatusForm: FC = () => {
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary="Heap (Free / Max Alloc)"
|
||||
secondary={
|
||||
formatNumber(data.free_heap) +
|
||||
' / ' +
|
||||
formatNumber(data.max_alloc_heap) +
|
||||
' bytes ' +
|
||||
(data.esp_platform === EspPlatform.ESP8266 ? '(' + data.heap_fragmentation + '% fragmentation)' : '')
|
||||
}
|
||||
primary={LL.HEAP()}
|
||||
secondary={formatNumber(data.free_heap) + ' KB / ' + formatNumber(data.max_alloc_heap) + ' KB '}
|
||||
/>
|
||||
</ListItem>
|
||||
{data.esp_platform === EspPlatform.ESP32 && data.psram_size > 0 && (
|
||||
{data.psram_size !== undefined && data.free_psram !== undefined && (
|
||||
<>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
@@ -290,8 +317,8 @@ const SystemStatusForm: FC = () => {
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary="PSRAM (Size / Free)"
|
||||
secondary={formatNumber(data.psram_size) + ' / ' + formatNumber(data.free_psram) + ' bytes'}
|
||||
primary={LL.PSRAM()}
|
||||
secondary={formatNumber(data.psram_size) + ' KB / ' + formatNumber(data.free_psram) + ' KB'}
|
||||
/>
|
||||
</ListItem>
|
||||
</>
|
||||
@@ -304,13 +331,25 @@ const SystemStatusForm: FC = () => {
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary="Flash Chip (Size / Speed)"
|
||||
primary={LL.FLASH()}
|
||||
secondary={
|
||||
formatNumber(data.flash_chip_size) + ' bytes / ' + (data.flash_chip_speed / 1000000).toFixed(0) + ' MHz'
|
||||
formatNumber(data.flash_chip_size) + ' KB / ' + (data.flash_chip_speed / 1000000).toFixed(0) + ' MHz'
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<SdCardAlertIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={LL.APPSIZE()}
|
||||
secondary={formatNumber(data.app_used) + ' KB / ' + formatNumber(data.app_free) + ' KB'}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
@@ -318,15 +357,8 @@ const SystemStatusForm: FC = () => {
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary="File System (Used / Total)"
|
||||
secondary={
|
||||
formatNumber(data.fs_used) +
|
||||
' / ' +
|
||||
formatNumber(data.fs_total) +
|
||||
' bytes (' +
|
||||
formatNumber(data.fs_total - data.fs_used) +
|
||||
'\xa0bytes free)'
|
||||
}
|
||||
primary={LL.FILESYSTEM()}
|
||||
secondary={formatNumber(data.fs_used) + ' KB / ' + formatNumber(data.fs_free) + ' KB'}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
@@ -335,7 +367,7 @@ const SystemStatusForm: FC = () => {
|
||||
<Box flexGrow={1} sx={{ '& button': { mt: 2 } }}>
|
||||
<ButtonRow>
|
||||
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
|
||||
Refresh
|
||||
{LL.REFRESH()}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
</Box>
|
||||
@@ -348,7 +380,7 @@ const SystemStatusForm: FC = () => {
|
||||
color="primary"
|
||||
onClick={() => setConfirmRestart(true)}
|
||||
>
|
||||
Restart
|
||||
{LL.RESTART()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<SettingsBackupRestoreIcon />}
|
||||
@@ -356,7 +388,7 @@ const SystemStatusForm: FC = () => {
|
||||
onClick={() => setConfirmFactoryReset(true)}
|
||||
color="error"
|
||||
>
|
||||
Factory reset
|
||||
{LL.FACTORY_RESET()}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
</Box>
|
||||
@@ -370,8 +402,8 @@ const SystemStatusForm: FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title="System Status" titleGutter>
|
||||
{content()}
|
||||
<SectionContent title={LL.STATUS_OF(LL.SYSTEM(1))} titleGutter>
|
||||
{restarting ? <RestartMonitor /> : content()}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,17 +7,23 @@ import { FileUploadConfig } from '../../api/endpoints';
|
||||
import GeneralFileUpload from './GeneralFileUpload';
|
||||
import RestartMonitor from './RestartMonitor';
|
||||
|
||||
import { useI18nContext } from '../../i18n/i18n-react';
|
||||
|
||||
const UploadFileForm: FC = () => {
|
||||
const [restarting, setRestarting] = useState<boolean>();
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const uploadFile = useRef(async (file: File, config?: FileUploadConfig) => {
|
||||
const response = await SystemApi.uploadFile(file, config);
|
||||
setRestarting(true);
|
||||
if (response.status === 200) {
|
||||
setRestarting(true);
|
||||
}
|
||||
return response;
|
||||
});
|
||||
|
||||
return (
|
||||
<SectionContent title="Upload/Download" titleGutter>
|
||||
<SectionContent title={LL.UPLOAD_DOWNLOAD()} titleGutter>
|
||||
{restarting ? <RestartMonitor /> : <GeneralFileUpload uploadGeneralFile={uploadFile.current} />}
|
||||
</SectionContent>
|
||||
);
|
||||
|
||||
1
interface/src/i18n/DE.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#D80027" d="M0 85.331h512v341.337H0z"/><path d="M0 85.331h512v113.775H0z"/><path fill="#FFDA44" d="M0 312.882h512v113.775H0z"/></svg>
|
||||
|
After Width: | Height: | Size: 216 B |
1
interface/src/i18n/FR.svg
Executable file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#FFF" d="M0 85.331h512v341.337H0z"/><path fill="#0052B4" d="M0 85.331h170.663v341.337H0z"/><path fill="#D80027" d="M341.337 85.331H512v341.337H341.337z"/></svg>
|
||||
|
After Width: | Height: | Size: 243 B |
1
interface/src/i18n/GB.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#FFF" d="M0 85.333h512V426.67H0z"/><path fill="#D80027" d="M288 85.33h-64v138.666H0v64h224v138.666h64V287.996h224v-64H288z"/><g fill="#0052B4"><path d="M393.785 315.358 512 381.034v-65.676zM311.652 315.358 512 426.662v-31.474l-143.693-79.83zM458.634 426.662l-146.982-81.664v81.664z"/></g><path fill="#FFF" d="M311.652 315.358 512 426.662v-31.474l-143.693-79.83z"/><path fill="#D80027" d="M311.652 315.358 512 426.662v-31.474l-143.693-79.83z"/><g fill="#0052B4"><path d="M90.341 315.356 0 365.546v-50.19zM200.348 329.51v97.151H25.491z"/></g><path fill="#D80027" d="M143.693 315.358 0 395.188v31.474l200.348-111.304z"/><g fill="#0052B4"><path d="M118.215 196.634 0 130.958v65.676zM200.348 196.634 0 85.33v31.474l143.693 79.83zM53.366 85.33l146.982 81.664V85.33z"/></g><path fill="#FFF" d="M200.348 196.634 0 85.33v31.474l143.693 79.83z"/><path fill="#D80027" d="M200.348 196.634 0 85.33v31.474l143.693 79.83z"/><g fill="#0052B4"><path d="M421.659 196.636 512 146.446v50.19zM311.652 182.482V85.331h174.857z"/></g><path fill="#D80027" d="M368.307 196.634 512 116.804V85.33L311.652 196.634z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
1
interface/src/i18n/NL.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.5 513 342"><path fill="#FFF" d="M0 85.5h513v342H0z"/><path fill="#cd1f2a" d="M0 85.5h513v114H0z"/><path fill="#1d4185" d="M0 312h513v114H0z"/></svg>
|
||||
|
After Width: | Height: | Size: 202 B |
1
interface/src/i18n/NO.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#D80027" d="M0 85.334h512v341.337H0z"/><path fill="#FFF" d="M512 295.883H202.195v130.783H122.435V295.883H0V216.111h122.435V85.329H202.195v130.782H512V277.329z"/><path fill="#2E52B2" d="M512 234.666v42.663H183.652v149.337h-42.674V277.329H0v-42.663h140.978V85.329h42.674v149.337z"/></svg>
|
||||
|
After Width: | Height: | Size: 369 B |
1
interface/src/i18n/PL.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><g fill="#FFF"><path d="M0 85.337h512v341.326H0z"/><path d="M0 85.337h512V256H0z"/></g><path fill="#D80027" d="M0 256h512v170.663H0z"/></svg>
|
||||
|
After Width: | Height: | Size: 212 B |
1
interface/src/i18n/SV.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#0052B4" d="M0 85.333h512V426.67H0z"/><path fill="#FFDA44" d="M192 85.33h-64v138.666H0v64h128v138.666h64V287.996h320v-64H192z"/></svg>
|
||||
|
After Width: | Height: | Size: 217 B |
309
interface/src/i18n/de/index.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
import type { Translation } from '../i18n-types';
|
||||
/* prettier-ignore */
|
||||
/* eslint-disable */
|
||||
|
||||
const de: Translation = {
|
||||
LANGUAGE: 'Sprache',
|
||||
RETRY: 'Neuer Versuch',
|
||||
LOADING: 'Laden',
|
||||
IS_REQUIRED: '{0} ist erforderlich',
|
||||
SIGN_IN: 'Einloggen',
|
||||
SIGN_OUT: 'Ausloggen',
|
||||
USERNAME: 'Nutzername',
|
||||
PASSWORD: 'Passwort',
|
||||
SU_PASSWORD: 'su Passwort',
|
||||
DASHBOARD: 'Kontrollzentrum',
|
||||
SETTINGS_OF: '{0} Einstellungen',
|
||||
SAVED: 'gespeichert',
|
||||
HELP_OF: '{0} Hilfe',
|
||||
LOGGED_IN: 'Eingeloggt als {name}',
|
||||
PLEASE_SIGNIN: 'Bitte einloggen, um fortzufahren',
|
||||
UPLOAD_SUCCESSFUL: 'Hochladen erfolgreich',
|
||||
DOWNLOAD_SUCCESSFUL: 'Herunterladen erfolgreich',
|
||||
INVALID_LOGIN: 'Ungültige Login Daten',
|
||||
NETWORK: 'Netzwerk',
|
||||
SECURITY: 'Sicherheit',
|
||||
ONOFF_CAP: 'AN/AUS',
|
||||
ONOFF: 'an/aus',
|
||||
TYPE: 'Typ',
|
||||
DESCRIPTION: 'Bezeichnung',
|
||||
ENTITIES: 'Entitäten',
|
||||
REFRESH: 'Aktualisieren',
|
||||
EXPORT: 'Exportieren',
|
||||
DEVICE_DETAILS: 'Geräte Details',
|
||||
BRAND: 'Marke',
|
||||
ID_OF: '{0} ID',
|
||||
DEVICE: 'Geräte',
|
||||
PRODUCT: 'Produkt',
|
||||
VERSION: 'Version',
|
||||
ENTITY_NAME: 'Entitätsname',
|
||||
VALUE: '{{Wert|wert}}',
|
||||
SHOW_FAV: 'nur Favoriten anzeigen',
|
||||
DEVICE_SENSOR_DATA: 'Geräte- und Sensordaten',
|
||||
DEVICES_SENSORS: 'Geräte & Sensoren',
|
||||
ATTACHED_SENSORS: 'Angeschlossene EMS-ESP Sensoren',
|
||||
RUN_COMMAND: 'Befehl ausführen',
|
||||
CHANGE_VALUE: 'Wert ändern',
|
||||
CANCEL: 'Abbrechen',
|
||||
RESET: 'Zurücksetzen',
|
||||
SEND: 'Senden',
|
||||
SAVE: 'Speichern',
|
||||
REMOVE: 'Entfernen',
|
||||
PROBLEM_UPDATING: 'Problem beim Aktualisieren',
|
||||
PROBLEM_LOADING: 'Problem beim Laden',
|
||||
ACCESS_DENIED: 'Zugriff abgelehnt',
|
||||
ANALOG_SENSOR: 'Analogsensor',
|
||||
ANALOG_SENSORS: 'Analogsensoren',
|
||||
UPDATED_OF: '{0} Aktualisiert',
|
||||
UPDATE_OF: '{0} Aktualisieren',
|
||||
REMOVED_OF: '{0} Entfernt',
|
||||
DELETION_OF: '{0} Löschung',
|
||||
OFFSET: 'Addition',
|
||||
FACTOR: 'Faktor',
|
||||
FREQ: 'Frequenz',
|
||||
DUTY_CYCLE: 'Duty Cycle',
|
||||
UNIT: 'UoM',
|
||||
STARTVALUE: 'Startwert',
|
||||
WARN_GPIO: 'Warnung: Vorsicht bei der korrekten Wahl des GPIO!',
|
||||
EDIT: 'Editiere',
|
||||
SENSOR: 'Sensor',
|
||||
TEMP_SENSOR: 'Temperatursensor',
|
||||
TEMP_SENSORS: 'Temperatursensoren',
|
||||
WRITE_CMD_SENT: 'Befehl schreiben wurde gesendet',
|
||||
WRITE_CMD_FAILED: 'Befehl schreiben failed', // TODO translate
|
||||
EMS_BUS_WARNING: 'EMS-Bus getrennt. Wenn diese Warnung nach einigen Sekunden immer noch besteht, überprüfen Sie bitte die Einstellungen und das Board-Profil',
|
||||
EMS_BUS_SCANNING: 'Suche nach EMS Geräten...',
|
||||
CONNECTED: 'Verbunden',
|
||||
TX_ISSUES: 'Tx-Probleme - versuchen Sie einen anderen Tx-Modus',
|
||||
DISCONNECTED: 'Getrennt',
|
||||
EMS_SCAN: 'Möchten Sie wirklich eine vollständige Gerätesuche des EMS-Busses starten?',
|
||||
EMS_BUS_STATUS: 'EMS-Busstatus',
|
||||
ACTIVE_DEVICES: 'Aktive Geräte und Sensoren',
|
||||
EMS_DEVICE: 'EMS Gerät',
|
||||
SUCCESS: 'ERFOLG',
|
||||
FAIL: 'FEHLER',
|
||||
QUALITY: 'QUALITÄT',
|
||||
SCAN_DEVICES: 'Nach neuen Geräten suchen',
|
||||
EMS_BUS_STATUS_TITLE: 'EMS-Bus- und Aktivitätsstatus',
|
||||
SCAN: 'Suche',
|
||||
STATUS_NAMES: [
|
||||
'EMS-Telegramme empfangen (Rx)',
|
||||
'EMS-Telegramme gelesen (Tx)',
|
||||
'EMS-Telegramme geschrieben (Tx)',
|
||||
'Temperatursensoren gelesen',
|
||||
'Analogsensoren gelesen',
|
||||
'MQTT-Nachrichten gesendet',
|
||||
'API-Aufrufe',
|
||||
'Syslog-Mitteilungen'
|
||||
],
|
||||
NUM_DEVICES: '{num} Gerät{{e}}',
|
||||
NUM_TEMP_SENSORS: '{num} Temperatursensor{{en}}',
|
||||
NUM_ANALOG_SENSORS: '{num} Analogsensor{{en}}',
|
||||
NUM_DAYS: '{num} Tag{{e}}',
|
||||
NUM_SECONDS: '{num} Sekunde{{n}}',
|
||||
NUM_HOURS: '{num} Stunde{{n}}',
|
||||
NUM_MINUTES: '{num} Minute{{n}}',
|
||||
APPLICATION_SETTINGS: 'Anwendungseinstellungen',
|
||||
CUSTOMIZATION: 'Anpassungen',
|
||||
APPLICATION_RESTARTING: 'EMS-ESP startet neu',
|
||||
INTERFACE_BOARD_PROFILE: 'Interface Platinenprofil',
|
||||
BOARD_PROFILE_TEXT: 'Wählen Sie ein vorkonfiguriertes Platinenprofil aus der Liste unten aus oder wählen Sie "Custom", um Ihre eigenen Hardwareeinstellungen zu konfigurieren',
|
||||
BOARD_PROFILE: 'Platinenprofil',
|
||||
CUSTOM: 'Custom',
|
||||
GPIO_OF: '{0} GPIO',
|
||||
BUTTON: 'Taste',
|
||||
TEMPERATURE: 'Temperatur',
|
||||
PHY_TYPE: 'Eth PHY Typ',
|
||||
DISABLED: 'deaktiviert',
|
||||
TX_MODE: 'Tx Modus',
|
||||
HARDWARE: 'Hardware',
|
||||
EMS_BUS: '{{BUS|EMS BUS}}',
|
||||
GENERAL_OPTIONS: 'Allgemeine Optionen',
|
||||
LANGUAGE_ENTITIES: 'Sprache (für Geräteentitäten)',
|
||||
HIDE_LED: 'LED ausblenden',
|
||||
ENABLE_TELNET: 'Aktiviere Telnet Konsole',
|
||||
ENABLE_ANALOG: 'Aktiviere Analogsensoren',
|
||||
CONVERT_FAHRENHEIT: 'Konvertiere Temperaturwerte in Fahrenheit',
|
||||
BYPASS_TOKEN: 'Zugriffstoken-Autorisierung bei API-Aufrufen umgehen',
|
||||
READONLY: 'Nur-Lese-Modus aktivieren (blockiert alle ausgehenden EMS Tx Write-Befehle)',
|
||||
UNDERCLOCK_CPU: 'CPU-Geschwindigkeit untertakten',
|
||||
ENABLE_SHOWER_TIMER: 'Duschtimer aktivieren',
|
||||
ENABLE_SHOWER_ALERT: 'Duschalarm aktivieren',
|
||||
TRIGGER_TIME: 'Auslösezeit',
|
||||
COLD_SHOT_DURATION: 'Kaltschussdauer',
|
||||
FORMATTING_OPTIONS: 'Formatierungsoptionen',
|
||||
BOOLEAN_FORMAT_DASHBOARD: 'Boolsches Format für Web',
|
||||
BOOLEAN_FORMAT_API: 'Boolesches Format API/MQTT',
|
||||
ENUM_FORMAT: 'Enum Format API/MQTT',
|
||||
INDEX: 'Index',
|
||||
ENABLE_PARASITE: 'Parasitäre Stomversorgung',
|
||||
LOGGING: 'Protokollierung',
|
||||
LOG_HEX: 'EMS-Telegramme hexadezimal protokollieren',
|
||||
ENABLE_SYSLOG: 'Syslog aktivieren',
|
||||
LOG_LEVEL: 'Log Level',
|
||||
MARK_INTERVAL: 'Intervallmarke',
|
||||
SECONDS: 'Sekunden',
|
||||
MINUTES: 'Minuten',
|
||||
HOURS: 'Stunden',
|
||||
RESTART: 'Neu starten',
|
||||
RESTART_TEXT: 'EMS-ESP muss neu gestartet werden, um geänderte Systemeinstellungen zu übernehmen',
|
||||
RESTART_CONFIRM: 'EMS-ESP wirklich neu starten?',
|
||||
COMMAND: 'Befehl',
|
||||
CUSTOMIZATIONS_RESTART: 'Alle Anpassungen wurden entfernt. Neustart...',
|
||||
CUSTOMIZATIONS_FULL: 'Ausgewählte Entitäten haben das Limit überschritten. Bitte stapelweise speichern',
|
||||
CUSTOMIZATIONS_SAVED: 'Anpassungen gespeichert',
|
||||
CUSTOMIZATIONS_HELP_1: 'Wählen Sie ein Gerät aus und passen Sie die Entitäten mithilfe der Optionen an',
|
||||
CUSTOMIZATIONS_HELP_2: 'als Favorit markieren',
|
||||
CUSTOMIZATIONS_HELP_3: 'Schreibaktion deaktivieren',
|
||||
CUSTOMIZATIONS_HELP_4: 'von MQTT und API ausschließen',
|
||||
CUSTOMIZATIONS_HELP_5: 'Aus dem Kontrollzentrum ausblenden',
|
||||
CUSTOMIZATIONS_HELP_6: 'Aus dem Speicher löschen',
|
||||
SELECT_DEVICE: 'Wählen Sie ein Gerät aus',
|
||||
SET_ALL: 'setzen Sie alle',
|
||||
OPTIONS: 'Optionen',
|
||||
NAME: 'Name',
|
||||
CUSTOMIZATIONS_RESET: 'Möchten Sie wirklich alle Anpassungen entfernen, einschließlich der benutzerdefinierten Einstellungen der Temperatur- und Analogsensoren?',
|
||||
DEVICE_ENTITIES: 'Geräteentitäten',
|
||||
USER_CUSTOMIZATION: 'Benutzeranpassung',
|
||||
SUPPORT_INFORMATION: 'Unterstützende Informationen',
|
||||
CLICK_HERE: 'Hier klicken',
|
||||
HELP_INFORMATION_1: 'EMS-ESP Konfigurationsanweisungen und mehr finden Sie im Online-Wiki',
|
||||
HELP_INFORMATION_2: 'Für einen Live-Community-Chat besuchen Sie unseren Discord-Server',
|
||||
HELP_INFORMATION_3: 'Um neue Funktionen anzufragen oder Fehler zu melden, eröffnen Sie ein Issue auf Github',
|
||||
HELP_INFORMATION_4: 'Bitte laden Sie die System-Details und hängen Sie sie an das Support-Issue an. ',
|
||||
HELP_INFORMATION_5: 'EMS-ESP ist ein freies Open-Source Projekt. Bitte unterstützen Sie die zukünftige Entwicklung mit einem "Star" auf Github!',
|
||||
SUPPORT_INFO: 'Support Info',
|
||||
UPLOAD_OF: '{0} Hochladen',
|
||||
UPLOAD: 'Hochladen',
|
||||
DOWNLOAD: 'Herunterladen',
|
||||
ABORTED: 'abgebrochen',
|
||||
FAILED: 'gescheitert',
|
||||
SUCCESSFUL: 'erfolgreich',
|
||||
SYSTEM: 'System',
|
||||
LOG_OF: '{0} Log',
|
||||
STATUS_OF: '{0} Status',
|
||||
UPLOAD_DOWNLOAD: 'Hoch-/Herunterladen',
|
||||
SYSTEM_VERSION_RUNNING: 'Sie verwenden die Version',
|
||||
SYSTEM_APPLY_FIRMWARE: 'um die neue Firmware anzuwenden',
|
||||
CLOSE: 'Schließen',
|
||||
USE: 'Verwenden Sie',
|
||||
FACTORY_RESET: 'Werkseinstellung',
|
||||
SYSTEM_FACTORY_TEXT: 'EMS-ESP wurde auf Werkseinstellung gesetzt und startet als Zugangspunkt neu',
|
||||
SYSTEM_FACTORY_TEXT_DIALOG: 'Sind Sie sicher alle Einstellungen auf Werkseinstellung zu setzen?',
|
||||
VERSION_CHECK: 'Versionsprüfung',
|
||||
THE_LATEST: 'Die neueste',
|
||||
OFFICIAL: 'offizielle',
|
||||
DEVELOPMENT: 'Entwicklungs',
|
||||
VERSION_IS: 'Version ist',
|
||||
RELEASE_NOTES: 'Versionshinweise',
|
||||
EMS_ESP_VER: 'EMS-ESP Version',
|
||||
PLATFORM: 'Platform (Platform / SDK)',
|
||||
UPTIME: 'System Betriebszeit',
|
||||
CPU_FREQ: 'CPU Frequenz',
|
||||
HEAP: 'freier RAM Speicher (Gesamt / max. Block)',
|
||||
PSRAM: 'PSRAM (Größe / Frei)',
|
||||
FLASH: 'Flash Speicher (Größe / Geschwindigkeit)',
|
||||
APPSIZE: 'Programm (Genutzt / Frei)',
|
||||
FILESYSTEM: 'Dateisystem (Genutzt / Frei)',
|
||||
BUFFER_SIZE: 'max. Puffergröße',
|
||||
COMPACT: 'Kompakte Darstellung',
|
||||
ENABLE_OTA: 'OTA Updates verwenden',
|
||||
DOWNLOAD_CUSTOMIZATION_TEXT: 'Herunterladen der individuellen Entitätsanpassungen',
|
||||
DOWNLOAD_SETTINGS_TEXT: 'Herunterladen der Anwendungseinstellungen. Vorsicht beim Teilen der Einstellungen, da sie Passwörter und andere sensitive Einstellungen enthalten',
|
||||
UPLOAD_TEXT: 'Hochladen von neuer Firmware (.bin), Geräte- oder Entitätseinstellungen (.json), zur optionalen Validitätsprüfung zuerst die (.md5) Datei hochladen',
|
||||
UPLOADING: 'Hochladen',
|
||||
UPLOAD_DROP_TEXT: 'Klicken Sie hier, oder ziehen eine Datei hierher',
|
||||
ERROR: 'Unerwarteter Fehler, bitter versuchen Sie es erneut',
|
||||
TIME_SET: 'Zeit gesetzt',
|
||||
MANAGE_USERS: 'Nutzerverwaltung',
|
||||
IS_ADMIN: 'ist Admin',
|
||||
USER_WARNING: 'Sie müssen mindestens einen Admin-Nutzer konfigurieren',
|
||||
ADD: 'Hinzufügen',
|
||||
ACCESS_TOKEN_FOR: 'Zugangs-Token für',
|
||||
ACCESS_TOKEN_TEXT: 'Dieses Token ist für REST API Aufrufe bestimmt, die eine Authentifizierung benötigen. Es kann entweder als Bearer Token im `Authorization-Header` oder in der Access_Token URL verwendet werden.',
|
||||
GENERATING_TOKEN: 'Erzeuge Token',
|
||||
USER: 'Nutzer',
|
||||
MODIFY: 'Ändern',
|
||||
SU_TEXT: 'Das su (super user) Passwort wird zum Signieren der Authentifikations-Tokens verwendet und ermöglicht Admin-Berechtigung in der Konsole.',
|
||||
NOT_ENABLED: 'Nicht aktiviert',
|
||||
ERRORS_OF: '{0} Fehler',
|
||||
DISCONNECT_REASON: 'Grund der Verbindungsunterbrechung',
|
||||
ENABLE_MQTT: 'MQTT aktivieren',
|
||||
BROKER: 'Broker',
|
||||
CLIENT: 'Client',
|
||||
BASE_TOPIC: 'Base',
|
||||
OPTIONAL: 'Optional',
|
||||
FORMATTING: 'Formattierung',
|
||||
MQTT_FORMAT: 'Topic/Payload Format',
|
||||
MQTT_NEST_1: 'Eingebettet in einem Gesamttopic',
|
||||
MQTT_NEST_2: 'Als einzelne Topics',
|
||||
MQTT_RESPONSE: 'Veröffentliche die Kommandoantwort als `response` Topic',
|
||||
MQTT_PUBLISH_TEXT_1: 'Veröffentliche einzelne Werte bei Veränderung als eigene Topics',
|
||||
MQTT_PUBLISH_TEXT_2: 'Veröffentliche als Kommando-Topic (ioBroker)',
|
||||
MQTT_PUBLISH_TEXT_3: 'Aktiviere `MQTT Discovery` (Home Assistant, Domoticz)',
|
||||
MQTT_PUBLISH_TEXT_4: 'Prefix für die `Discovery`-Topics',
|
||||
MQTT_PUBLISH_INTERVALS: 'Veröffentlichungs-Intervalle',
|
||||
MQTT_INT_BOILER: 'Boiler und Wärmepumpen',
|
||||
MQTT_INT_THERMOSTATS: 'Thermostate',
|
||||
MQTT_INT_SOLAR: 'Solarmodule',
|
||||
MQTT_INT_MIXER: 'Mischermodule',
|
||||
MQTT_INT_HEARTBEAT: 'Heartbeat',
|
||||
MQTT_QUEUE: 'MQTT Queue',
|
||||
DEFAULT: 'Standard',
|
||||
MQTT_ENTITY_FORMAT: 'Entitäts-ID Format',
|
||||
MQTT_ENTITY_FORMAT_0: 'Einzelinstanz, Langname (v3.4)',
|
||||
MQTT_ENTITY_FORMAT_1: 'Einzelinstanz, MQTT-Namen',
|
||||
MQTT_ENTITY_FORMAT_2: 'Mehrfachinstanzen, MQTT-Namen',
|
||||
MQTT_CLEAN_SESSION: 'Setze `Clean Session`',
|
||||
MQTT_RETAIN_FLAG: 'Setze `Retain flag` immer',
|
||||
INACTIVE: 'Inaktiv',
|
||||
ACTIVE: 'Aktiv',
|
||||
UNKNOWN: 'Unbekannt',
|
||||
SET_TIME: 'Zeiteinstellung',
|
||||
SET_TIME_TEXT: 'Geben Sie das lokale Datum und die Zeit ein',
|
||||
LOCAL_TIME: 'Lokalzeit',
|
||||
UTC_TIME: 'UTC Zeit',
|
||||
ENABLE_NTP: 'Aktiviere NTP',
|
||||
NTP_SERVER: 'NTP Server',
|
||||
TIME_ZONE: 'Zeitzone',
|
||||
ACCESS_POINT: 'Zugangspunkt',
|
||||
AP_PROVIDE: 'Aktiviere Zugangspunkt',
|
||||
AP_PROVIDE_TEXT_1: 'Immer',
|
||||
AP_PROVIDE_TEXT_2: 'Wenn WiFi nicht verbunden',
|
||||
AP_PROVIDE_TEXT_3: 'Niemals',
|
||||
AP_PREFERRED_CHANNEL: 'Bevorzugter Kanal',
|
||||
AP_HIDE_SSID: 'Verstecke SSID',
|
||||
AP_CLIENTS: 'AP-Klienten',
|
||||
AP_MAX_CLIENTS: 'Max Anzahl AP-Klienten',
|
||||
AP_LOCAL_IP: 'Lokale IP',
|
||||
NETWORK_SCAN: 'Suche nach WiFi Netzwerken',
|
||||
IDLE: 'Leerlauf',
|
||||
LOST: 'Verloren',
|
||||
SCANNING: 'Suche',
|
||||
SCAN_AGAIN: 'Erneute Suche',
|
||||
NETWORK_SCANNER: 'Netzwerk Suche',
|
||||
NETWORK_NO_WIFI: 'Keine WiFi Netzwerke gefunden',
|
||||
NETWORK_BLANK_SSID: 'Freilassen um WiFi zu deaktivieren',
|
||||
TX_POWER: 'Tx Leistung',
|
||||
HOSTNAME: 'Hostname',
|
||||
NETWORK_DISABLE_SLEEP: 'Deaktiviere WiFi Schlafmodus',
|
||||
NETWORK_LOW_BAND: 'Verwende niedrige WiFi Bandbreite',
|
||||
NETWORK_USE_DNS: 'Aktiviere mDNS Service',
|
||||
NETWORK_ENABLE_CORS: 'Aktiviere CORS',
|
||||
NETWORK_CORS_ORIGIN: 'CORS origin',
|
||||
NETWORK_ENABLE_IPV6: 'Aktiviere IPv6 Unterstützung',
|
||||
NETWORK_FIXED_IP: 'Feste IP Adresse',
|
||||
NETWORK_GATEWAY: 'Gateway',
|
||||
NETWORK_SUBNET: 'Subnetz Maske',
|
||||
NETWORK_DNS: 'DNS Server',
|
||||
ADDRESS_OF: '{0} Adresse',
|
||||
ADMIN: 'Administrator',
|
||||
GUEST: 'Gast',
|
||||
NEW: 'Neuer',
|
||||
NEW_NAME_OF: 'Ändere {0}',
|
||||
ENTITY: 'Entität',
|
||||
MIN: 'min',
|
||||
MAX: 'max'
|
||||
};
|
||||
|
||||
export default de;
|
||||
309
interface/src/i18n/en/index.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
import type { Translation } from '../i18n-types';
|
||||
/* prettier-ignore */
|
||||
/* eslint-disable */
|
||||
|
||||
const en: Translation = {
|
||||
LANGUAGE: 'Language',
|
||||
RETRY: 'Retry',
|
||||
LOADING: 'Loading',
|
||||
IS_REQUIRED: '{0} is required',
|
||||
SIGN_IN: 'Sign In',
|
||||
SIGN_OUT: 'Sign Out',
|
||||
USERNAME: 'Username',
|
||||
PASSWORD: 'Password',
|
||||
SU_PASSWORD: 'su Password',
|
||||
DASHBOARD: 'Dashboard',
|
||||
SETTINGS_OF: '{0} Settings',
|
||||
SAVED: 'saved',
|
||||
HELP_OF: '{0} Help',
|
||||
LOGGED_IN: 'Logged in as {name}',
|
||||
PLEASE_SIGNIN: 'Please sign in to continue',
|
||||
UPLOAD_SUCCESSFUL: 'Upload finished',
|
||||
DOWNLOAD_SUCCESSFUL: 'Download finished',
|
||||
INVALID_LOGIN: 'Invalid login details',
|
||||
NETWORK: 'Network',
|
||||
SECURITY: 'Security',
|
||||
ONOFF_CAP: 'ON/OFF',
|
||||
ONOFF: 'on/off',
|
||||
TYPE: 'Type',
|
||||
DESCRIPTION: 'Description',
|
||||
ENTITIES: 'Entities',
|
||||
REFRESH: 'Refresh',
|
||||
EXPORT: 'Export',
|
||||
DEVICE_DETAILS: 'Device Details',
|
||||
ID_OF: '{0} ID',
|
||||
DEVICE: 'Device',
|
||||
PRODUCT: 'Product',
|
||||
VERSION: 'Version',
|
||||
BRAND: 'Brand',
|
||||
ENTITY_NAME: 'Entity Name',
|
||||
VALUE: '{{Value|value}}',
|
||||
SHOW_FAV: 'only show favorites',
|
||||
DEVICE_SENSOR_DATA: 'Device and Sensor Data',
|
||||
DEVICES_SENSORS: 'Devices & Sensors',
|
||||
ATTACHED_SENSORS: 'Attached EMS-ESP Sensors',
|
||||
RUN_COMMAND: 'Call Command',
|
||||
CHANGE_VALUE: 'Change Value',
|
||||
CANCEL: 'Cancel',
|
||||
RESET: 'Reset',
|
||||
SEND: 'Send',
|
||||
SAVE: 'Save',
|
||||
REMOVE: 'Remove',
|
||||
PROBLEM_UPDATING: 'Problem updating',
|
||||
PROBLEM_LOADING: 'Problem loading',
|
||||
ACCESS_DENIED: 'Access Denied',
|
||||
ANALOG_SENSOR: 'Analog Sensor',
|
||||
ANALOG_SENSORS: 'Analog Sensors',
|
||||
UPDATED_OF: '{0} Updated',
|
||||
UPDATE_OF: '{0} Update',
|
||||
REMOVED_OF: '{0} Removed',
|
||||
DELETION_OF: '{0} Deletion',
|
||||
OFFSET: 'Offset',
|
||||
FACTOR: 'Factor',
|
||||
FREQ: 'Frequency',
|
||||
DUTY_CYCLE: 'Duty Cycle',
|
||||
UNIT: 'UoM',
|
||||
STARTVALUE: 'Start value',
|
||||
WARN_GPIO: 'Warning: be careful when assigning a GPIO!',
|
||||
EDIT: 'Edit',
|
||||
SENSOR: 'Sensor',
|
||||
TEMP_SENSOR: 'Temperature Sensor',
|
||||
TEMP_SENSORS: 'Temperature Sensors',
|
||||
WRITE_CMD_SENT: 'Write command has been sent',
|
||||
WRITE_CMD_FAILED: 'Write command failed',
|
||||
EMS_BUS_WARNING: 'EMS bus disconnected. If this warning still persists after a few seconds please check settings and board profile',
|
||||
EMS_BUS_SCANNING: 'Scanning for EMS devices...',
|
||||
CONNECTED: 'Connected',
|
||||
TX_ISSUES: 'Tx issues - try a different Tx Mode',
|
||||
DISCONNECTED: 'Disconnected',
|
||||
EMS_SCAN: 'Are you sure you want to initiate a full device scan of the EMS bus?',
|
||||
EMS_BUS_STATUS: 'EMS Bus Status',
|
||||
ACTIVE_DEVICES: 'Active Devices & Sensors',
|
||||
EMS_DEVICE: 'EMS Device',
|
||||
SUCCESS: 'SUCCESS',
|
||||
FAIL: 'FAIL',
|
||||
QUALITY: 'QUALITY',
|
||||
SCAN_DEVICES: 'Scan for new devices',
|
||||
EMS_BUS_STATUS_TITLE: 'EMS Bus & Activity Status',
|
||||
SCAN: 'Scan',
|
||||
STATUS_NAMES: [
|
||||
'EMS Telegrams Received (Rx)',
|
||||
'EMS Reads (Tx)',
|
||||
'EMS Writes (Tx)',
|
||||
'Temperature Sensor Reads',
|
||||
'Analog Sensor Reads',
|
||||
'MQTT Publishes',
|
||||
'API Calls',
|
||||
'Syslog Messages'
|
||||
],
|
||||
NUM_DEVICES: '{num} Device{{s}}',
|
||||
NUM_TEMP_SENSORS: '{num} Temperature Sensor{{s}}',
|
||||
NUM_ANALOG_SENSORS: '{num} Analog Sensor{{s}}',
|
||||
NUM_DAYS: '{num} day{{s}}',
|
||||
NUM_SECONDS: '{num} second{{s}}',
|
||||
NUM_HOURS: '{num} hour{{s}}',
|
||||
NUM_MINUTES: '{num} minute{{s}}',
|
||||
APPLICATION_SETTINGS: 'Application Settings',
|
||||
CUSTOMIZATION: 'Customization',
|
||||
APPLICATION_RESTARTING: 'EMS-ESP is restarting',
|
||||
INTERFACE_BOARD_PROFILE: 'Interface Board Profile',
|
||||
BOARD_PROFILE_TEXT: 'Select a pre-configured interface board profile from the list below or choose Custom to configure your own hardware settings',
|
||||
BOARD_PROFILE: 'Board Profile',
|
||||
CUSTOM: 'Custom',
|
||||
GPIO_OF: '{0} GPIO',
|
||||
BUTTON: 'Button',
|
||||
TEMPERATURE: 'Temperature',
|
||||
PHY_TYPE: 'Eth PHY Type',
|
||||
DISABLED: 'disabled',
|
||||
TX_MODE: 'Tx Mode',
|
||||
HARDWARE: 'Hardware',
|
||||
EMS_BUS: '{{BUS|EMS BUS}}',
|
||||
GENERAL_OPTIONS: 'General Options',
|
||||
LANGUAGE_ENTITIES: 'Language (for device entities)',
|
||||
HIDE_LED: 'Hide LED',
|
||||
ENABLE_TELNET: 'Enable Telnet Console',
|
||||
ENABLE_ANALOG: 'Enable Analog Sensors',
|
||||
CONVERT_FAHRENHEIT: 'Convert temperature values to Fahrenheit',
|
||||
BYPASS_TOKEN: 'Bypass Access Token authorization on API calls',
|
||||
READONLY: 'Enable read-only mode (blocks all outgoing EMS Tx Write commands)',
|
||||
UNDERCLOCK_CPU: 'Underclock CPU speed',
|
||||
ENABLE_SHOWER_TIMER: 'Enable Shower Timer',
|
||||
ENABLE_SHOWER_ALERT: 'Enable Shower Alert',
|
||||
TRIGGER_TIME: 'Trigger Time',
|
||||
COLD_SHOT_DURATION: 'Cold Shot Duration',
|
||||
FORMATTING_OPTIONS: 'Formatting Options',
|
||||
BOOLEAN_FORMAT_DASHBOARD: 'Boolean Format Dashboard',
|
||||
BOOLEAN_FORMAT_API: 'Boolean Format API/MQTT',
|
||||
ENUM_FORMAT: 'Enum Format API/MQTT',
|
||||
INDEX: 'Index',
|
||||
ENABLE_PARASITE: 'Enable parasite power',
|
||||
LOGGING: 'Logging',
|
||||
LOG_HEX: 'Log EMS telegrams in hexadecimal',
|
||||
ENABLE_SYSLOG: 'Enable Syslog',
|
||||
LOG_LEVEL: 'Log Level',
|
||||
MARK_INTERVAL: 'Mark Interval',
|
||||
SECONDS: 'seconds',
|
||||
MINUTES: 'minutes',
|
||||
HOURS: 'hours',
|
||||
RESTART: 'Restart',
|
||||
RESTART_TEXT: 'EMS-ESP needs to be restarted to apply changed system settings',
|
||||
RESTART_CONFIRM: 'Are you sure you want to restart EMS-ESP?',
|
||||
COMMAND: 'Command',
|
||||
CUSTOMIZATIONS_RESTART: 'All customizations have been removed. Restarting...',
|
||||
CUSTOMIZATIONS_FULL: 'Selected entities exceeded limit. Please save in batches',
|
||||
CUSTOMIZATIONS_SAVED: 'Customizations saved',
|
||||
CUSTOMIZATIONS_HELP_1: 'Select a device and customize the entities options or click to rename',
|
||||
CUSTOMIZATIONS_HELP_2: 'mark as favorite',
|
||||
CUSTOMIZATIONS_HELP_3: 'disable write action',
|
||||
CUSTOMIZATIONS_HELP_4: 'exclude from MQTT and API',
|
||||
CUSTOMIZATIONS_HELP_5: 'hide from Dashboard',
|
||||
CUSTOMIZATIONS_HELP_6: 'remove from memory',
|
||||
SELECT_DEVICE: 'Select a device',
|
||||
SET_ALL: 'set all',
|
||||
OPTIONS: 'Options',
|
||||
NAME: 'Name',
|
||||
CUSTOMIZATIONS_RESET: 'Are you sure you want remove all customizations including the custom settings of the Temperature and Analog sensors?',
|
||||
DEVICE_ENTITIES: 'Device Entities',
|
||||
USER_CUSTOMIZATION: 'User Customization',
|
||||
SUPPORT_INFORMATION: 'Support Information',
|
||||
CLICK_HERE: 'Click Here',
|
||||
HELP_INFORMATION_1: 'Visit the online wiki to get instructions on how to configure EMS-ESP',
|
||||
HELP_INFORMATION_2: 'For live community chat join our Discord server',
|
||||
HELP_INFORMATION_3: 'To request a feature or report a bug',
|
||||
HELP_INFORMATION_4: 'remember to download and attach your system information for a faster response when reporting an issue',
|
||||
HELP_INFORMATION_5: 'EMS-ESP is a free and open-source project. Please support its future development by giving it a star on Github!',
|
||||
SUPPORT_INFO: 'Support Info',
|
||||
UPLOAD_OF: '{0} Upload',
|
||||
UPLOAD: 'Upload',
|
||||
DOWNLOAD: 'Download',
|
||||
ABORTED: 'aborted',
|
||||
FAILED: 'failed',
|
||||
SUCCESSFUL: 'successful',
|
||||
SYSTEM: 'System',
|
||||
LOG_OF: '{0} Log',
|
||||
STATUS_OF: '{0} Status',
|
||||
UPLOAD_DOWNLOAD: 'Upload/Download',
|
||||
SYSTEM_VERSION_RUNNING: 'You are currently running version',
|
||||
SYSTEM_APPLY_FIRMWARE: 'to apply the new firmware',
|
||||
CLOSE: 'Close',
|
||||
USE: 'Use',
|
||||
FACTORY_RESET: 'Factory Reset',
|
||||
SYSTEM_FACTORY_TEXT: 'Device has been factory reset and will now restart',
|
||||
SYSTEM_FACTORY_TEXT_DIALOG: 'Are you sure you want to reset the device to its factory defaults?',
|
||||
VERSION_CHECK: 'Version Check',
|
||||
THE_LATEST: 'The latest',
|
||||
OFFICIAL: 'official',
|
||||
DEVELOPMENT: 'development',
|
||||
VERSION_IS: 'version is',
|
||||
RELEASE_NOTES: 'release notes',
|
||||
EMS_ESP_VER: 'EMS-ESP Version',
|
||||
PLATFORM: 'Device (Platform / SDK)',
|
||||
UPTIME: 'System Uptime',
|
||||
CPU_FREQ: 'CPU Frequency',
|
||||
HEAP: 'Heap (Free / Max Alloc)',
|
||||
PSRAM: 'PSRAM (Size / Free)',
|
||||
FLASH: 'Flash Chip (Size / Speed)',
|
||||
APPSIZE: 'Application (Used / Free)',
|
||||
FILESYSTEM: 'File System (Used / Free)',
|
||||
BUFFER_SIZE: 'Max Buffer Size',
|
||||
COMPACT: 'Compact',
|
||||
ENABLE_OTA: 'Enable OTA Updates',
|
||||
DOWNLOAD_CUSTOMIZATION_TEXT: 'Download the entity customizations',
|
||||
DOWNLOAD_SETTINGS_TEXT: 'Download the application settings. Be careful when sharing your settings as this file contains passwords and other sensitive system information',
|
||||
UPLOAD_TEXT: 'Upload a new firmware (.bin) file, settings or customizations (.json) file below, for optional validation upload (.md5) first',
|
||||
UPLOADING: 'Uploading',
|
||||
UPLOAD_DROP_TEXT: 'Drop file or click here',
|
||||
ERROR: 'Unexpected Error, please try again',
|
||||
TIME_SET: 'Time set',
|
||||
MANAGE_USERS: 'Manage Users',
|
||||
IS_ADMIN: 'is Admin',
|
||||
USER_WARNING: 'You must have at least one admin user configured',
|
||||
ADD: 'Add',
|
||||
ACCESS_TOKEN_FOR: 'Access Token for',
|
||||
ACCESS_TOKEN_TEXT: 'The token below is used with REST API calls that require authorization. It can be passed either as a Bearer token in the Authorization header or in the access_token URL query parameter.',
|
||||
GENERATING_TOKEN: 'Generating token',
|
||||
USER: 'User',
|
||||
MODIFY: 'Modify',
|
||||
SU_TEXT: 'The su (super user) password is used to sign authentication tokens and also enable admin privileges within the Console.',
|
||||
NOT_ENABLED: 'Not enabled',
|
||||
ERRORS_OF: '{0} Errors',
|
||||
DISCONNECT_REASON: 'Disconnect Reason',
|
||||
ENABLE_MQTT: 'Enable MQTT',
|
||||
BROKER: 'Broker',
|
||||
CLIENT: 'Client',
|
||||
BASE_TOPIC: 'Base',
|
||||
OPTIONAL: 'Optional',
|
||||
FORMATTING: 'Formatting',
|
||||
MQTT_FORMAT: 'Topic/Payload Format',
|
||||
MQTT_NEST_1: 'Nested in a single topic',
|
||||
MQTT_NEST_2: 'As individual topics',
|
||||
MQTT_RESPONSE: 'Publish command output to a `response` topic',
|
||||
MQTT_PUBLISH_TEXT_1: 'Publish single value topics on change',
|
||||
MQTT_PUBLISH_TEXT_2: 'Publish to command topics (ioBroker)',
|
||||
MQTT_PUBLISH_TEXT_3: 'Enable MQTT Discovery (Home Assistant, Domoticz)',
|
||||
MQTT_PUBLISH_TEXT_4: 'Prefix for the Discovery topics',
|
||||
MQTT_PUBLISH_INTERVALS: 'Publish Intervals',
|
||||
MQTT_INT_BOILER: 'Boilers and Heat Pumps',
|
||||
MQTT_INT_THERMOSTATS: 'Thermostats',
|
||||
MQTT_INT_SOLAR: 'Solar Modules',
|
||||
MQTT_INT_MIXER: 'Mixer Modules',
|
||||
MQTT_INT_HEARTBEAT: 'Heartbeat',
|
||||
MQTT_QUEUE: 'MQTT Queue',
|
||||
DEFAULT: 'Default',
|
||||
MQTT_ENTITY_FORMAT: 'Entity ID format',
|
||||
MQTT_ENTITY_FORMAT_0: 'Single instance, long name (v3.4)',
|
||||
MQTT_ENTITY_FORMAT_1: 'Single instance, short name',
|
||||
MQTT_ENTITY_FORMAT_2: 'Multiple instances, short name',
|
||||
MQTT_CLEAN_SESSION: 'Set Clean Session',
|
||||
MQTT_RETAIN_FLAG: 'Always set Retain flag',
|
||||
INACTIVE: 'Inactive',
|
||||
ACTIVE: 'Active',
|
||||
UNKNOWN: 'Unknown',
|
||||
SET_TIME: 'Set Time',
|
||||
SET_TIME_TEXT: 'Enter local date and time below to set the time',
|
||||
LOCAL_TIME: 'Local Time',
|
||||
UTC_TIME: 'UTC Time',
|
||||
ENABLE_NTP: 'Enable NTP',
|
||||
NTP_SERVER: 'NTP Server',
|
||||
TIME_ZONE: 'Time Zone',
|
||||
ACCESS_POINT: 'Access Point',
|
||||
AP_PROVIDE: 'Enable Access Point',
|
||||
AP_PROVIDE_TEXT_1: 'always',
|
||||
AP_PROVIDE_TEXT_2: 'when WiFi is disconnected',
|
||||
AP_PROVIDE_TEXT_3: 'never',
|
||||
AP_PREFERRED_CHANNEL: 'Preferred Channel',
|
||||
AP_HIDE_SSID: 'Hide SSID',
|
||||
AP_CLIENTS: 'AP Clients',
|
||||
AP_MAX_CLIENTS: 'Max Clients',
|
||||
AP_LOCAL_IP: 'Local IP',
|
||||
NETWORK_SCAN: 'Scan WiFi Networks',
|
||||
IDLE: 'Idle',
|
||||
LOST: 'Lost',
|
||||
SCANNING: 'Scanning',
|
||||
SCAN_AGAIN: 'Scan again',
|
||||
NETWORK_SCANNER: 'Network Scanner',
|
||||
NETWORK_NO_WIFI: 'No WiFi networks found',
|
||||
NETWORK_BLANK_SSID: 'leave blank to disable WiFi',
|
||||
TX_POWER: 'Tx Power',
|
||||
HOSTNAME: 'Hostname',
|
||||
NETWORK_DISABLE_SLEEP: 'Disable WiFi Sleep Mode',
|
||||
NETWORK_LOW_BAND: 'Use Lower WiFi Bandwidth',
|
||||
NETWORK_USE_DNS: 'Enable mDNS Service',
|
||||
NETWORK_ENABLE_CORS: 'Enable CORS',
|
||||
NETWORK_CORS_ORIGIN: 'CORS origin',
|
||||
NETWORK_ENABLE_IPV6: 'Enable IPv6 support',
|
||||
NETWORK_FIXED_IP: 'Use Fixed IP address',
|
||||
NETWORK_GATEWAY: 'Gateway',
|
||||
NETWORK_SUBNET: 'Subnet Mask',
|
||||
NETWORK_DNS: 'DNS Servers',
|
||||
ADDRESS_OF: '{0} Address',
|
||||
ADMIN: 'Admin',
|
||||
GUEST: 'Guest',
|
||||
NEW: 'New',
|
||||
NEW_NAME_OF: 'New {0} name',
|
||||
ENTITY: 'entity',
|
||||
MIN: 'min',
|
||||
MAX: 'max'
|
||||
};
|
||||
|
||||
export default en;
|
||||
10
interface/src/i18n/formatters.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { FormattersInitializer } from 'typesafe-i18n';
|
||||
import type { Locales, Formatters } from './i18n-types';
|
||||
|
||||
export const initFormatters: FormattersInitializer<Locales, Formatters> = (locale: Locales) => {
|
||||
const formatters: Formatters = {
|
||||
// add your formatter functions here
|
||||
};
|
||||
|
||||
return formatters;
|
||||
};
|
||||
309
interface/src/i18n/fr/index.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
import type { Translation } from '../i18n-types';
|
||||
/* prettier-ignore */
|
||||
/* eslint-disable */
|
||||
|
||||
const fr: Translation = {
|
||||
LANGUAGE: 'Langue',
|
||||
RETRY: 'Réessayer',
|
||||
LOADING: 'Chargement',
|
||||
IS_REQUIRED: '{0} est requis',
|
||||
SIGN_IN: 'Se connecter',
|
||||
SIGN_OUT: 'Se déconnecter',
|
||||
USERNAME: 'Nom d\'utilisateur',
|
||||
PASSWORD: 'Mot de passe',
|
||||
SU_PASSWORD: 'Mot de passe su',
|
||||
DASHBOARD: 'Tableau de bord',
|
||||
SETTINGS_OF: 'Paramètres {0}',
|
||||
SAVED: 'sauvegardé',
|
||||
HELP_OF: 'Aide {0}',
|
||||
LOGGED_IN: 'Connecté en tant que {name}',
|
||||
PLEASE_SIGNIN: 'Veuillez vous connecter pour continuer',
|
||||
UPLOAD_SUCCESSFUL: 'Upload terminée',
|
||||
DOWNLOAD_SUCCESSFUL: 'Téléchargement terminé',
|
||||
INVALID_LOGIN: 'Informations de connexion invalides',
|
||||
NETWORK: 'Réseau',
|
||||
SECURITY: 'Sécurité',
|
||||
ONOFF_CAP: 'ON/OFF',
|
||||
ONOFF: 'on/off',
|
||||
TYPE: 'Type',
|
||||
DESCRIPTION: 'Description',
|
||||
ENTITIES: 'Entités',
|
||||
REFRESH: 'Rafraîchir',
|
||||
EXPORT: 'Exporter',
|
||||
DEVICE_DETAILS: 'Détails de l\'appareil',
|
||||
ID_OF: 'ID {0}',
|
||||
DEVICE: 'Appareil',
|
||||
PRODUCT: 'Produit',
|
||||
VERSION: 'Version',
|
||||
BRAND: 'Marque',
|
||||
ENTITY_NAME: 'Nom de l\'entité',
|
||||
VALUE: 'Valeur',
|
||||
SHOW_FAV: 'ne montrer que les favoris',
|
||||
DEVICE_SENSOR_DATA: 'Données des appareils et capteurs',
|
||||
DEVICES_SENSORS: 'Appareils et capteurs',
|
||||
ATTACHED_SENSORS: 'Capteurs EMS-ESP connectés',
|
||||
RUN_COMMAND: 'Lancer une commande',
|
||||
CHANGE_VALUE: 'Changer la valeur',
|
||||
CANCEL: 'Annuler',
|
||||
RESET: 'Réinitialiser',
|
||||
SEND: 'Envoyer',
|
||||
SAVE: 'Sauvegarder',
|
||||
REMOVE: 'Enlever',
|
||||
PROBLEM_UPDATING: 'Problème lors de la mise à jour',
|
||||
PROBLEM_LOADING: 'Problème lors du chargement',
|
||||
ACCESS_DENIED: 'Accès refusé',
|
||||
ANALOG_SENSOR: 'Capteur analogique',
|
||||
ANALOG_SENSORS: 'Capteurs analogiques',
|
||||
UPDATED_OF: '{0} mis à jour',
|
||||
UPDATE_OF: 'Mise à jour de {0}',
|
||||
REMOVED_OF: '{0} enlevé',
|
||||
DELETION_OF: '{0} supprimé',
|
||||
OFFSET: 'Offset',
|
||||
FACTOR: 'Facteur',
|
||||
FREQ: 'Fréquence',
|
||||
DUTY_CYCLE: 'Cycle de fonctionnement',
|
||||
UNIT: 'Unité',
|
||||
STARTVALUE: 'Valeur de départ',
|
||||
WARN_GPIO: 'Attention: soyez vigilant en choisissant un GPIO!',
|
||||
EDIT: 'Éditer',
|
||||
SENSOR: 'Capteur',
|
||||
TEMP_SENSOR: 'Capteur de température',
|
||||
TEMP_SENSORS: 'Capteurs de température',
|
||||
WRITE_CMD_SENT: 'Envoyer la commande sent', // TODO translate
|
||||
WRITE_CMD_FAILED: 'Envoyer la commande failed', // TODO translate
|
||||
EMS_BUS_WARNING: 'Bus EMS déconnecté. Si ce message persiste après quelques secondes, vérifiez les paramètres et la configuration de la carte.',
|
||||
EMS_BUS_SCANNING: 'Scan des appareils EMS...',
|
||||
CONNECTED: 'Connecté',
|
||||
TX_ISSUES: 'Problèmes de transmission (Tx) - Essayez un autre mode Tx',
|
||||
DISCONNECTED: 'Déconnecté',
|
||||
EMS_SCAN: 'Etes-vous sûr de vouloir lancer un scan complet du bus EMS ?',
|
||||
EMS_BUS_STATUS: 'Statut du bus EMS',
|
||||
ACTIVE_DEVICES: 'Appareils et capteurs actifs',
|
||||
EMS_DEVICE: 'Appareils EMS',
|
||||
SUCCESS: 'SUCCÈS',
|
||||
FAIL: 'ÉCHEC',
|
||||
QUALITY: 'QUALITÉ',
|
||||
SCAN_DEVICES: 'Rechercher de nouveaux appareils',
|
||||
EMS_BUS_STATUS_TITLE: 'Statut du bus et de l\'activité EMS',
|
||||
SCAN: 'Scan',
|
||||
STATUS_NAMES: [
|
||||
'Télégrammes EMS reçus (Rx)',
|
||||
'Lectures EMS (Tx)',
|
||||
'Écritures EMS (Tx)',
|
||||
'Lectures capteurs de température',
|
||||
'Lectures capteurs analogiques',
|
||||
'Publications MQTT',
|
||||
'Appels à l\'API',
|
||||
'Messages Syslog'
|
||||
],
|
||||
NUM_DEVICES: '{num} Appareil{{s}}',
|
||||
NUM_TEMP_SENSORS: '{num} Capteur{{s}} de température',
|
||||
NUM_ANALOG_SENSORS: '{num} Capteur{{s}} analogique{{s}}',
|
||||
NUM_DAYS: '{num} jour{{s}}',
|
||||
NUM_SECONDS: '{num} seconde{{s}}',
|
||||
NUM_HOURS: '{num} heure{{s}}',
|
||||
NUM_MINUTES: '{num} minute{{s}}',
|
||||
APPLICATION_SETTINGS: 'Paramètres de l\'application',
|
||||
CUSTOMIZATION: 'Personnalisation',
|
||||
APPLICATION_RESTARTING: 'EMS-ESP redémarre',
|
||||
INTERFACE_BOARD_PROFILE: 'Profile de carte d\'interface',
|
||||
BOARD_PROFILE_TEXT: 'Sélectionnez un profil de carte d\'interface préconfiguré dans la liste ci-dessous ou choisissez Personnalisé pour configurer vos propres paramètres matériels',
|
||||
BOARD_PROFILE: 'Profil de carte',
|
||||
CUSTOM: 'Personnalisé',
|
||||
GPIO_OF: 'GPIO {0}',
|
||||
BUTTON: 'Bouton',
|
||||
TEMPERATURE: 'Température',
|
||||
PHY_TYPE: 'Eth PHY Type',
|
||||
DISABLED: 'désactivé',
|
||||
TX_MODE: 'Tx Mode',
|
||||
HARDWARE: 'Hardware',
|
||||
EMS_BUS: '{{BUS|EMS BUS}}',
|
||||
GENERAL_OPTIONS: 'Options générales',
|
||||
LANGUAGE_ENTITIES: 'Langue (pour les entités du matériel)',
|
||||
HIDE_LED: 'Cacher la LED',
|
||||
ENABLE_TELNET: 'Activer la console Telnet',
|
||||
ENABLE_ANALOG: 'Activer les capteurs analogiques',
|
||||
CONVERT_FAHRENHEIT: 'Convertir les températures en Fahrenheit',
|
||||
BYPASS_TOKEN: 'Contourner l\'autorisation du jeton d\'accès sur les appels API',
|
||||
READONLY: 'Activer le mode lecture uniquement (bloque toutes les commandes EMS sortantes en écriture Tx)',
|
||||
UNDERCLOCK_CPU: 'Underclock du CPU',
|
||||
ENABLE_SHOWER_TIMER: 'Activer la minuterie de la douche',
|
||||
ENABLE_SHOWER_ALERT: 'Activer les alertes de durée de douche',
|
||||
TRIGGER_TIME: 'Durée avant déclenchement',
|
||||
COLD_SHOT_DURATION: 'Durée du coup d\'eau froide',
|
||||
FORMATTING_OPTIONS: 'Options de mise en forme',
|
||||
BOOLEAN_FORMAT_DASHBOARD: 'Tableau de bord du format booléen',
|
||||
BOOLEAN_FORMAT_API: 'Format booléen API/MQTT',
|
||||
ENUM_FORMAT: 'Format enum API/MQTT',
|
||||
INDEX: 'Index',
|
||||
ENABLE_PARASITE: 'Activer la puissance parasite',
|
||||
LOGGING: 'Journal',
|
||||
LOG_HEX: 'Enregistrer les télégrammes EMS en hexadécimal',
|
||||
ENABLE_SYSLOG: 'Activer les logs système',
|
||||
LOG_LEVEL: 'Niveau de log',
|
||||
MARK_INTERVAL: 'Intervalle de marquage',
|
||||
SECONDS: 'secondes',
|
||||
MINUTES: 'minutes',
|
||||
HOURS: 'heures',
|
||||
RESTART: 'Redémarrer',
|
||||
RESTART_TEXT: 'EMS-ESP a besoin de redémarrer pour appliquer les changements de paramètres du système',
|
||||
RESTART_CONFIRM: 'Etes-vous sûr de vouloir redémarrer EMS-ESP ?',
|
||||
COMMAND: 'Commande',
|
||||
CUSTOMIZATIONS_RESTART: 'Toutes les personnalisations ont été supprimées. Redémarrage...',
|
||||
CUSTOMIZATIONS_FULL: 'Les entités sélectionnées ont dépassé la limite. Veuillez sauvegarder par lots',
|
||||
CUSTOMIZATIONS_SAVED: 'Personnalisations enregistrées',
|
||||
CUSTOMIZATIONS_HELP_1: 'Sélectionnez un appareil et personnalisez les options des entités ou cliquez pour renommer',
|
||||
CUSTOMIZATIONS_HELP_2: 'marquer comme favori',
|
||||
CUSTOMIZATIONS_HELP_3: 'désactiver l\'action d\'écriture',
|
||||
CUSTOMIZATIONS_HELP_4: 'exclure de MQTT et de l\'API',
|
||||
CUSTOMIZATIONS_HELP_5: 'cacher du Tableau de bord',
|
||||
CUSTOMIZATIONS_HELP_6: 'remove from memory',
|
||||
SELECT_DEVICE: 'Sélectionnez un appareil',
|
||||
SET_ALL: 'tout régler',
|
||||
OPTIONS: 'Options',
|
||||
NAME: 'Nom',
|
||||
CUSTOMIZATIONS_RESET: 'Êtes-vous sûr de vouloir supprimer toutes les personnalisations, y compris les paramètres personnalisés des capteurs de température et analogiques ?',
|
||||
DEVICE_ENTITIES: 'Entités de l\'appareil',
|
||||
USER_CUSTOMIZATION: 'Personnalisation de l\'utilisateur',
|
||||
SUPPORT_INFORMATION: 'Information de support',
|
||||
CLICK_HERE: 'Cliquez ici',
|
||||
HELP_INFORMATION_1: 'Visitez le wiki en ligne pour obtenir des instructions sur la façon de configurer EMS-ESP.',
|
||||
HELP_INFORMATION_2: 'Pour une discussion en direct avec la communauté, rejoignez notre serveur Discord',
|
||||
HELP_INFORMATION_3: 'Pour demander une fonctionnalité ou signaler un problème',
|
||||
HELP_INFORMATION_4: 'n\'oubliez pas de télécharger et de joindre les informations relatives à votre système pour obtenir une réponse plus rapide lorsque vous signalez un problème',
|
||||
HELP_INFORMATION_5: 'EMS-ESP est un projet libre et open-source. Merci de soutenir son développement futur en lui donnant une étoile sur Github !',
|
||||
SUPPORT_INFO: 'Information de support',
|
||||
UPLOAD_OF: 'Upload de {0}',
|
||||
UPLOAD: 'Upload',
|
||||
DOWNLOAD: 'Download',
|
||||
ABORTED: 'annulé',
|
||||
FAILED: 'échoué',
|
||||
SUCCESSFUL: 'réussi',
|
||||
SYSTEM: 'Système',
|
||||
LOG_OF: '{0} Log',
|
||||
STATUS_OF: 'Statut {0}',
|
||||
UPLOAD_DOWNLOAD: 'Upload/Download',
|
||||
SYSTEM_VERSION_RUNNING: 'Vous utilisez actuellement la version',
|
||||
SYSTEM_APPLY_FIRMWARE: 'pour appliquer le nouveau firmware',
|
||||
CLOSE: 'Fermer',
|
||||
USE: 'Utiliser',
|
||||
FACTORY_RESET: 'Réinitialisation',
|
||||
SYSTEM_FACTORY_TEXT: 'L\'appareil a été réinitialisé et va maintenant redémarrer',
|
||||
SYSTEM_FACTORY_TEXT_DIALOG: 'Êtes-vous sûr de vouloir réinitialiser l\'appareil à ses paramètres d\'usine ?',
|
||||
VERSION_CHECK: 'Vérification de la version',
|
||||
THE_LATEST: 'La dernière',
|
||||
OFFICIAL: 'officielle',
|
||||
DEVELOPMENT: 'développement',
|
||||
VERSION_IS: 'version est',
|
||||
RELEASE_NOTES: 'notes de version',
|
||||
EMS_ESP_VER: 'Version EMS-ESP',
|
||||
PLATFORM: 'Appareil (Plateforme / SDK)',
|
||||
UPTIME: 'Durée de fonctionnement du système',
|
||||
CPU_FREQ: 'Fréquence du CPU',
|
||||
HEAP: 'Heap (Libre / Max Allouée)',
|
||||
PSRAM: 'PSRAM (Taille / Libre)',
|
||||
FLASH: 'Flash Chip (Taille / Vitesse)',
|
||||
APPSIZE: 'Application (Utilisée / Libre)',
|
||||
FILESYSTEM: 'File System (Utilisée / Libre)',
|
||||
BUFFER_SIZE: 'Max taille du buffer',
|
||||
COMPACT: 'Compact',
|
||||
ENABLE_OTA: 'Activer les updates OTA',
|
||||
DOWNLOAD_CUSTOMIZATION_TEXT: 'Télécharger les personnalisations d\'entités',
|
||||
DOWNLOAD_SETTINGS_TEXT: 'Téléchargez les paramètres de l\'application. Soyez prudent lorsque vous partagez vos paramètres car ce fichier contient des mots de passe et d\'autres informations système sensibles.',
|
||||
UPLOAD_TEXT: 'Téléchargez un nouveau fichier de firmware (.bin), un fichier de paramètres ou de personnalisations (.json) ci-dessous, pour une validation optionnelle téléchargez d\'abord un fichier (.md5)',
|
||||
UPLOADING: 'Téléchargement',
|
||||
UPLOAD_DROP_TEXT: 'Déposer le fichier ou cliquer ici',
|
||||
ERROR: 'Erreur inattendue, veuillez réessayer',
|
||||
TIME_SET: 'Time set',
|
||||
MANAGE_USERS: 'Gérer les utilisateurs',
|
||||
IS_ADMIN: 'admin',
|
||||
USER_WARNING: 'Vous devez avoir au moins un utilisateur admin configuré',
|
||||
ADD: 'Ajouter',
|
||||
ACCESS_TOKEN_FOR: 'Jeton d\'accès pour',
|
||||
ACCESS_TOKEN_TEXT: 'Le jeton ci-dessous est utilisé avec les appels d\'API REST qui nécessitent une autorisation. Il peut être passé soit en tant que jeton Bearer dans l\'en-tête Authorization, soit dans le paramètre de requête URL access_token.',
|
||||
GENERATING_TOKEN: 'Génération de jeton',
|
||||
USER: 'Utilisateur',
|
||||
MODIFY: 'Modifier',
|
||||
SU_TEXT: 'Le mot de passe su (super utilisateur) est utilisé pour signer les jetons d\'authentification et activer les privilèges d\'administrateur dans la console.',
|
||||
NOT_ENABLED: 'Non activé',
|
||||
ERRORS_OF: 'Erreurs {0}',
|
||||
DISCONNECT_REASON: 'Raison de la déconnexion',
|
||||
ENABLE_MQTT: 'Activer le MQTT',
|
||||
BROKER: 'Broker',
|
||||
CLIENT: 'Client',
|
||||
BASE_TOPIC: 'Base',
|
||||
OPTIONAL: 'Optionnel',
|
||||
FORMATTING: 'Mise en forme',
|
||||
MQTT_FORMAT: 'Format du Topic/Payload',
|
||||
MQTT_NEST_1: 'Englobé dans un topic unique',
|
||||
MQTT_NEST_2: 'En tant que topics individuels',
|
||||
MQTT_RESPONSE: 'Publier le résultat des commandes dans un topic `response`',
|
||||
MQTT_PUBLISH_TEXT_1: 'Publier des topics à valeur unique sur changement',
|
||||
MQTT_PUBLISH_TEXT_2: 'Publier vers des topics de commande (ioBroker)',
|
||||
MQTT_PUBLISH_TEXT_3: 'Activer la découverte MQTT (Home Assistant, Domoticz)',
|
||||
MQTT_PUBLISH_TEXT_4: 'Préfixe pour les topics découverte',
|
||||
MQTT_PUBLISH_INTERVALS: 'Intervalles de publication',
|
||||
MQTT_INT_BOILER: 'Chaudières et pompes à chaleur',
|
||||
MQTT_INT_THERMOSTATS: 'Thermostats',
|
||||
MQTT_INT_SOLAR: 'Modules solaires',
|
||||
MQTT_INT_MIXER: 'Modules mélangeurs',
|
||||
MQTT_INT_HEARTBEAT: 'Battements',
|
||||
MQTT_QUEUE: 'Queue MQTT',
|
||||
DEFAULT: 'Défaut',
|
||||
MQTT_ENTITY_FORMAT: 'Entity ID format', // TODO translate
|
||||
MQTT_ENTITY_FORMAT_0: 'Single instance, long name (v3.4)',// TODO translate
|
||||
MQTT_ENTITY_FORMAT_1: 'Single instance, short name', // TODO translate
|
||||
MQTT_ENTITY_FORMAT_2: 'Multiple instances, short name', // TODO translate
|
||||
MQTT_CLEAN_SESSION: 'Flag Clean Session',
|
||||
MQTT_RETAIN_FLAG: 'Toujours activer le Retain Flag',
|
||||
INACTIVE: 'Inactif',
|
||||
ACTIVE: 'Actif',
|
||||
UNKNOWN: 'Inconnu',
|
||||
SET_TIME: 'Définir l\'heure',
|
||||
SET_TIME_TEXT: 'Entrer la date et l\'heure locale ci-dessous pour régler l\'heure',
|
||||
LOCAL_TIME: 'Heure locale',
|
||||
UTC_TIME: 'Heure UTC',
|
||||
ENABLE_NTP: 'Activer le NTP',
|
||||
NTP_SERVER: 'Serveur NTP',
|
||||
TIME_ZONE: 'Fuseau horaire',
|
||||
ACCESS_POINT: 'Point d\'accès',
|
||||
AP_PROVIDE: 'Activer le Point d\'Accès',
|
||||
AP_PROVIDE_TEXT_1: 'toujours',
|
||||
AP_PROVIDE_TEXT_2: 'quand le WiFi est déconnecté',
|
||||
AP_PROVIDE_TEXT_3: 'jamais',
|
||||
AP_PREFERRED_CHANNEL: 'Canal préféré',
|
||||
AP_HIDE_SSID: 'Cacher le SSID',
|
||||
AP_CLIENTS: 'AP Clients',
|
||||
AP_MAX_CLIENTS: 'Max Clients',
|
||||
AP_LOCAL_IP: 'IP locale',
|
||||
NETWORK_SCAN: 'Scanner les réseaux WiFi',
|
||||
IDLE: 'Inactif',
|
||||
LOST: 'Perdu',
|
||||
SCANNING: 'Scan en cours',
|
||||
SCAN_AGAIN: 'Rescanner',
|
||||
NETWORK_SCANNER: 'Scan réseau',
|
||||
NETWORK_NO_WIFI: 'Pas de réseau WiFi trouvé',
|
||||
NETWORK_BLANK_SSID: 'laisser vide pour désactiver le WiFi',
|
||||
TX_POWER: 'Puissance Tx',
|
||||
HOSTNAME: 'Nom d\'hôte',
|
||||
NETWORK_DISABLE_SLEEP: 'Désactiver le mode veille du WiFi',
|
||||
NETWORK_LOW_BAND: 'Utiliser une bande passante WiFi plus faible',
|
||||
NETWORK_USE_DNS: 'Activer le service mDNS',
|
||||
NETWORK_ENABLE_CORS: 'Activer CORS',
|
||||
NETWORK_CORS_ORIGIN: 'Origine CORS',
|
||||
NETWORK_ENABLE_IPV6: 'Activer le support de l\'IPv6',
|
||||
NETWORK_FIXED_IP: 'Utiliser une adresse IP fixe',
|
||||
NETWORK_GATEWAY: 'Passerelle',
|
||||
NETWORK_SUBNET: 'Masque de sous-réseau',
|
||||
NETWORK_DNS: 'Serveurs DNS',
|
||||
ADDRESS_OF: 'Adresse de {0}',
|
||||
ADMIN: 'Admin',
|
||||
GUEST: 'Invité',
|
||||
NEW: 'Nouveau',
|
||||
NEW_NAME_OF: 'Nouveau nom de {0}',
|
||||
ENTITY: 'entité',
|
||||
MIN: 'min',
|
||||
MAX: 'max'
|
||||
};
|
||||
|
||||
export default fr;
|
||||
309
interface/src/i18n/nl/index.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
import type { Translation } from '../i18n-types';
|
||||
/* prettier-ignore */
|
||||
/* eslint-disable */
|
||||
|
||||
const nl: Translation = {
|
||||
LANGUAGE: 'Taal',
|
||||
RETRY: 'Opnieuw proberen',
|
||||
LOADING: 'Laden',
|
||||
IS_REQUIRED: '{0} is verplicht',
|
||||
SIGN_IN: 'Inloggen',
|
||||
SIGN_OUT: 'Uitloggen',
|
||||
USERNAME: 'Gebruikersnaam',
|
||||
PASSWORD: 'Wachtwoord',
|
||||
SU_PASSWORD: 'su Wachtwoord',
|
||||
DASHBOARD: 'Dashboard',
|
||||
SETTINGS_OF: '{0} Instellingen',
|
||||
SAVED: 'opgeslagen',
|
||||
HELP_OF: '{0} Help',
|
||||
LOGGED_IN: 'Ingelogd als {name}',
|
||||
PLEASE_SIGNIN: 'Log in om verder te gaan',
|
||||
UPLOAD_SUCCESSFUL: 'Upload successvol',
|
||||
DOWNLOAD_SUCCESSFUL: 'Download successvol',
|
||||
INVALID_LOGIN: 'Logingegevens fout',
|
||||
NETWORK: 'Netwerk',
|
||||
SECURITY: 'Beveiliging',
|
||||
ONOFF_CAP: 'AAN/UIT',
|
||||
ONOFF: 'aan/uit',
|
||||
TYPE: 'Type',
|
||||
DESCRIPTION: 'Beschrijving',
|
||||
ENTITIES: 'Entiteiten',
|
||||
REFRESH: 'Ververs',
|
||||
EXPORT: 'Export',
|
||||
DEVICE_DETAILS: 'Device Gegevens',
|
||||
ID_OF: '{0} ID',
|
||||
DEVICE: 'Apparaat',
|
||||
PRODUCT: 'Product',
|
||||
VERSION: 'Versie',
|
||||
BRAND: 'Merk',
|
||||
ENTITY_NAME: 'Entiteit',
|
||||
VALUE: '{{Waarde|waarde}}',
|
||||
SHOW_FAV: 'alleen favorieten weergeven',
|
||||
DEVICE_SENSOR_DATA: 'Apparaat en Sensor data',
|
||||
DEVICES_SENSORS: 'Apparaten & Sensoren',
|
||||
ATTACHED_SENSORS: 'Aangesloten EMS-ESP sensoren',
|
||||
RUN_COMMAND: 'Call commando',
|
||||
CHANGE_VALUE: 'Wijzig waarde',
|
||||
CANCEL: 'Annuleren',
|
||||
RESET: 'Reset',
|
||||
SEND: 'Verzenden',
|
||||
SAVE: 'Opslaan',
|
||||
REMOVE: 'Verwijderen',
|
||||
PROBLEM_UPDATING: 'Probleem met updaten',
|
||||
PROBLEM_LOADING: 'Probleem met laden',
|
||||
ACCESS_DENIED: 'Toegang geweigerd',
|
||||
ANALOG_SENSOR: 'Analoge sensor',
|
||||
ANALOG_SENSORS: 'Analoge Sensoren',
|
||||
UPDATED_OF: '{0} Bijgewerkt',
|
||||
UPDATE_OF: '{0} Bijwerken',
|
||||
REMOVED_OF: '{0} Verwijderd',
|
||||
DELETION_OF: '{0} Verwijder',
|
||||
OFFSET: 'Offset',
|
||||
FACTOR: 'Factor',
|
||||
FREQ: 'Frequentie',
|
||||
DUTY_CYCLE: 'Duty Cycle',
|
||||
UNIT: 'UoM',
|
||||
STARTVALUE: 'Startwaarde',
|
||||
WARN_GPIO: 'Waarschuwing: let op met het koppelen van de juiste GPIO pin!',
|
||||
EDIT: 'Wijzigen',
|
||||
SENSOR: 'Sensor',
|
||||
TEMP_SENSOR: 'Temperatuur sensor',
|
||||
TEMP_SENSORS: 'Temperatuur Sensoren',
|
||||
WRITE_CMD_SENT: 'Schrijf commando sent', // TODO translate
|
||||
WRITE_CMD_FAILED: 'Schrijf commando failed', // TODO translate
|
||||
EMS_BUS_WARNING: 'EMS bus niet gevonden. Als deze waarschuwing blijft staan na een paar seconden dan loop de instellingen na en in het bijzonder het apparaat type profiel na.',
|
||||
EMS_BUS_SCANNING: 'Scannen naar EMS apparaten...',
|
||||
CONNECTED: 'Verbonden',
|
||||
TX_ISSUES: 'Tx bus probleem. Probeer een andere Tx verzendmodus',
|
||||
DISCONNECTED: 'Niet verbonden',
|
||||
EMS_SCAN: 'Weet je zeker dat je een volledige EMS bus scan uit wilt voeren?',
|
||||
EMS_BUS_STATUS: 'EMS busstatus',
|
||||
ACTIVE_DEVICES: 'Actieve Apparaten & Sensoren',
|
||||
EMS_DEVICE: 'EMS Apparaat',
|
||||
SUCCESS: 'SUCCESS',
|
||||
FAIL: 'MISLUKT',
|
||||
QUALITY: 'QUALITEIT',
|
||||
SCAN_DEVICES: 'Scannen naar nieuwe apparaten',
|
||||
EMS_BUS_STATUS_TITLE: 'EMS Bus & Activiteitenstatus',
|
||||
SCAN: 'Scan',
|
||||
STATUS_NAMES: [
|
||||
'EMS Telegrammen ontvangen (Rx)',
|
||||
'EMS Leesopdrachten (Tx)',
|
||||
'EMS Schrijfopdrachten (Tx)',
|
||||
'Temperatuursensoren uitgelezen',
|
||||
'Analoge sensoren uitgelezen',
|
||||
'MQTT publicaties',
|
||||
'API calls',
|
||||
'Syslog berichten'
|
||||
],
|
||||
NUM_DEVICES: '{num} Apparaat{{en}}',
|
||||
NUM_TEMP_SENSORS: '{num} Temperatuursensor{{en}}',
|
||||
NUM_ANALOG_SENSORS: '{num} Analoge sensor{{en}}',
|
||||
NUM_DAYS: '{num} dag{{en}}',
|
||||
NUM_SECONDS: '{num} second{{en}}',
|
||||
NUM_HOURS: '{num} {{uur|uren}}',
|
||||
NUM_MINUTES: '{num} {{minuut|minuten}}',
|
||||
APPLICATION_SETTINGS: 'Applicatieinstellingen',
|
||||
CUSTOMIZATION: 'Custom aanpassingen',
|
||||
APPLICATION_RESTARTING: 'EMS-ESP herstarten',
|
||||
INTERFACE_BOARD_PROFILE: 'Interface Apparaatprofiel',
|
||||
BOARD_PROFILE_TEXT: 'Selecteer een vooraf ingesteld apparaat profiel uit de lijst of kies Eigen om zelf uw hardware te configureren',
|
||||
BOARD_PROFILE: 'Apparaatprofiel',
|
||||
CUSTOM: 'Custom',
|
||||
GPIO_OF: '{0} GPIO',
|
||||
BUTTON: 'Toets',
|
||||
TEMPERATURE: 'Temperatuur',
|
||||
PHY_TYPE: 'Eth PHY Type',
|
||||
TX_MODE: 'Tx Mode',
|
||||
HARDWARE: 'Hardware',
|
||||
EMS_BUS: '{{BUS|EMS BUS}}',
|
||||
DISABLED: 'Uitgeschakeld',
|
||||
GENERAL_OPTIONS: 'Algemene Opties',
|
||||
LANGUAGE_ENTITIES: 'Taal (voor apparaat entiteiten)',
|
||||
HIDE_LED: 'Verberg LED',
|
||||
ENABLE_TELNET: 'Activeer Telnet console',
|
||||
ENABLE_ANALOG: 'Activeer analoge sensoren',
|
||||
CONVERT_FAHRENHEIT: 'Converteer temperatuurwaarden naar Fahrenheit',
|
||||
BYPASS_TOKEN: 'API Access Token authenticatie uitschakelen',
|
||||
READONLY: 'Activeer read-only modus (blokkeert alle outgaande EMS Tx schrijf commandos)',
|
||||
UNDERCLOCK_CPU: 'Underclock CPU snelheid',
|
||||
ENABLE_SHOWER_TIMER: 'Activeer Douche Timer (tijdmeting)',
|
||||
ENABLE_SHOWER_ALERT: 'Activeer Douchemelding',
|
||||
TRIGGER_TIME: 'Trigger tijd',
|
||||
COLD_SHOT_DURATION: 'Tijd Shot koud water',
|
||||
FORMATTING_OPTIONS: 'Formatteringsopties',
|
||||
BOOLEAN_FORMAT_DASHBOARD: 'Boolean formaat dashboard',
|
||||
BOOLEAN_FORMAT_API: 'Boolean formaat API/MQTT',
|
||||
ENUM_FORMAT: 'Enum formaat API/MQTT',
|
||||
INDEX: 'Index',
|
||||
ENABLE_PARASITE: 'Activeer Dallas parasitaire modus',
|
||||
LOGGING: 'Logging',
|
||||
LOG_HEX: 'Log EMS telegrammen in hexadecimaal',
|
||||
ENABLE_SYSLOG: 'Activeer Syslog',
|
||||
LOG_LEVEL: 'Log Level',
|
||||
MARK_INTERVAL: 'Markeringsinterval',
|
||||
SECONDS: 'seconden',
|
||||
MINUTES: 'minuten',
|
||||
HOURS: 'uren',
|
||||
RESTART: 'Herstarten',
|
||||
RESTART_TEXT: 'EMS-ESP dient opnieuw gestart te worden om de wijzingen toe te passen',
|
||||
RESTART_CONFIRM: 'Weet je zeker dat je EMS-ESP wilt herstarten?',
|
||||
COMMAND: 'Commando',
|
||||
CUSTOMIZATIONS_RESTART: 'Alle custom profielen worden verwijderd. Herstarten...',
|
||||
CUSTOMIZATIONS_FULL: 'Te veel entiteiten geselecteerd. Sla op in delen aub',
|
||||
CUSTOMIZATIONS_SAVED: 'Custom aanpassingen opgeslagen',
|
||||
CUSTOMIZATIONS_HELP_1: 'Selecteer een apparaat en pas de entiteiten aan door middel van de opties',
|
||||
CUSTOMIZATIONS_HELP_2: 'Markeer as favoriet',
|
||||
CUSTOMIZATIONS_HELP_3: 'Zet schrijfacties uit',
|
||||
CUSTOMIZATIONS_HELP_4: 'Uitsluiten van MQTT en API',
|
||||
CUSTOMIZATIONS_HELP_5: 'verberg van het Dashboard',
|
||||
CUSTOMIZATIONS_HELP_6: 'remove from memory',
|
||||
SELECT_DEVICE: 'Selecteer een apparaat',
|
||||
SET_ALL: 'Alles aanzetten',
|
||||
OPTIONS: 'Opties',
|
||||
NAME: 'Naam',
|
||||
CUSTOMIZATIONS_RESET: 'Weet je zeker dat je alle custom aanpassingen wilt verwijderen inclusief de custom instellingen voor analoge temperatuursensoren?',
|
||||
DEVICE_ENTITIES: 'Apparaat Entiteiten',
|
||||
USER_CUSTOMIZATION: 'Custom Instellingen',
|
||||
SUPPORT_INFORMATION: 'Support Informatie',
|
||||
CLICK_HERE: 'Klik Hier',
|
||||
HELP_INFORMATION_1: 'Bezoek de online wiki om instructies te vinden om EMS-ESP te configureren',
|
||||
HELP_INFORMATION_2: 'Voor de live community ga naar de Discord server',
|
||||
HELP_INFORMATION_3: 'Om een nieuwe feature te vragen of een bug te rapporteren',
|
||||
HELP_INFORMATION_4: 'zorg dat je ook je systeem details zijn toevoeged voor een sneller antwoord',
|
||||
HELP_INFORMATION_5: 'EMS-ESP is een gratis en open source project. Steun ons met een Star op Github!',
|
||||
SUPPORT_INFO: 'Support Info',
|
||||
UPLOAD_OF: '{0} Upload',
|
||||
UPLOAD: 'Upload',
|
||||
DOWNLOAD: 'Download',
|
||||
ABORTED: 'afgebroken',
|
||||
FAILED: 'mislukt',
|
||||
SUCCESSFUL: 'successvol',
|
||||
SYSTEM: 'Systeem',
|
||||
LOG_OF: '{0} Log',
|
||||
STATUS_OF: '{0} Status',
|
||||
UPLOAD_DOWNLOAD: 'Upload/Download',
|
||||
SYSTEM_VERSION_RUNNING: 'op dit moment draai je versie',
|
||||
SYSTEM_APPLY_FIRMWARE: 'om de nieuwe firmware te activeren',
|
||||
CLOSE: 'Sluiten',
|
||||
USE: 'Gebruik',
|
||||
FACTORY_RESET: 'Fabrieksinstellingen',
|
||||
SYSTEM_FACTORY_TEXT: 'Gateway is gereset en start nu weer op met fabrieksinstellingen',
|
||||
SYSTEM_FACTORY_TEXT_DIALOG: 'Weet je zeker dat je een reset naar fabrieksinstellingen uit wilt voeren?',
|
||||
VERSION_CHECK: 'Versie Check',
|
||||
THE_LATEST: 'De laatste',
|
||||
OFFICIAL: 'official',
|
||||
DEVELOPMENT: 'development',
|
||||
VERSION_IS: 'versie is',
|
||||
RELEASE_NOTES: 'release notes',
|
||||
EMS_ESP_VER: 'EMS-ESP Version',
|
||||
PLATFORM: 'Apparaat (Platform / SDK)',
|
||||
UPTIME: 'Systeem Uptime',
|
||||
CPU_FREQ: 'CPU Frequency',
|
||||
HEAP: 'Heap (Free / Max Alloc)',
|
||||
PSRAM: 'PSRAM (Size / Free)',
|
||||
FLASH: 'Flash Chip (Size / Speed)',
|
||||
APPSIZE: 'Application (Used / Free)',
|
||||
FILESYSTEM: 'File System (Used / Free)',
|
||||
BUFFER_SIZE: 'Max Buffer Size',
|
||||
COMPACT: 'Compact',
|
||||
ENABLE_OTA: 'Acitveer OTA Updates',
|
||||
DOWNLOAD_CUSTOMIZATION_TEXT: 'Download alle custom instellingen',
|
||||
DOWNLOAD_SETTINGS_TEXT: 'Download de applicatie settings. Wees voorzichting met het delen van dit bestand want het bevat o.a. de wachtwoorden in plain text',
|
||||
UPLOAD_TEXT: 'Upload een nieuwe firmware (.bin) file, instellingen of custom instellingen (.json) bestand hieronder',
|
||||
UPLOADING: 'Uploading',
|
||||
UPLOAD_DROP_TEXT: 'Sleep bestand hierheen of klik hier',
|
||||
ERROR: 'Onverwachte fout, probeer opnieuw',
|
||||
TIME_SET: 'Tijd ingesteld',
|
||||
MANAGE_USERS: 'Beheer Gebruikers',
|
||||
IS_ADMIN: 'is Admin',
|
||||
USER_WARNING: 'U dient tenminste 1 admin gebruiker te configureren',
|
||||
ADD: 'Toevoegen',
|
||||
ACCESS_TOKEN_FOR: 'Access Token voor',
|
||||
ACCESS_TOKEN_TEXT: 'Het token hieronder wordt gebruikt voor de REST API calls die authorisatie nodig hebben. Het kan zowel als Bearer token in de Authorization header of in acccess_token URL query parameter gebruikt worden',
|
||||
GENERATING_TOKEN: 'Token aan het genereren',
|
||||
USER: 'Gebruiker',
|
||||
MODIFY: 'Aanpassen',
|
||||
SU_TEXT: 'Het su (super user) wachtwoord wordt gebruikt om authorisatie tokens te signeren en ook om admin privileges te activeren in de console.',
|
||||
NOT_ENABLED: 'Niet geactiveerd',
|
||||
ERRORS_OF: '{0} Foutmeldingen',
|
||||
DISCONNECT_REASON: 'Verbinding verbroken vanwege',
|
||||
ENABLE_MQTT: 'Activeer MQTT',
|
||||
BROKER: 'Broker',
|
||||
CLIENT: 'Client',
|
||||
BASE_TOPIC: 'Base',
|
||||
OPTIONAL: 'Optioneel',
|
||||
FORMATTING: 'Formatteren',
|
||||
MQTT_FORMAT: 'Topic/Payload Formattering',
|
||||
MQTT_NEST_1: 'Genest in 1 topic',
|
||||
MQTT_NEST_2: 'Als individuele topics',
|
||||
MQTT_RESPONSE: 'Publiceer commando output naar een `response` topic',
|
||||
MQTT_PUBLISH_TEXT_1: 'Publiceer enkele waarde topics on change',
|
||||
MQTT_PUBLISH_TEXT_2: 'Publiceer naar commando topics (ioBroker)',
|
||||
MQTT_PUBLISH_TEXT_3: 'Activeer MQTT Discovery (Home Assistant, Domoticz)',
|
||||
MQTT_PUBLISH_TEXT_4: 'Prefix voor de Discovery topics',
|
||||
MQTT_PUBLISH_INTERVALS: 'Publicatie intervallen',
|
||||
MQTT_INT_BOILER: 'CV ketels en warmtepompen',
|
||||
MQTT_INT_THERMOSTATS: 'Thermostaten',
|
||||
MQTT_INT_SOLAR: 'Solar Modules',
|
||||
MQTT_INT_MIXER: 'Mixer Modules',
|
||||
MQTT_INT_HEARTBEAT: 'Heartbeat',
|
||||
MQTT_QUEUE: 'MQTT Queue',
|
||||
DEFAULT: 'Default',
|
||||
MQTT_ENTITY_FORMAT: 'Entity ID format', // TODO translate
|
||||
MQTT_ENTITY_FORMAT_0: 'Single instance, long name (v3.4)', // TODO translate
|
||||
MQTT_ENTITY_FORMAT_1: 'Single instance, short name', // TODO translate
|
||||
MQTT_ENTITY_FORMAT_2: 'Multiple instances, short name', // TODO translate
|
||||
MQTT_CLEAN_SESSION: 'Clean Session aan',
|
||||
MQTT_RETAIN_FLAG: 'Retain flag aan',
|
||||
INACTIVE: 'Inactief',
|
||||
ACTIVE: 'Actief',
|
||||
UNKNOWN: 'Onbekend',
|
||||
SET_TIME: 'Tijd instellen',
|
||||
SET_TIME_TEXT: 'Geef de locale datum en tijd in',
|
||||
LOCAL_TIME: 'Locale Tijd',
|
||||
UTC_TIME: 'UTC Tijd',
|
||||
ENABLE_NTP: 'Activeer NTP',
|
||||
NTP_SERVER: 'NTP Server',
|
||||
TIME_ZONE: 'Tijdzone',
|
||||
ACCESS_POINT: 'Access Point',
|
||||
AP_PROVIDE: 'Activeer Access Point',
|
||||
AP_PROVIDE_TEXT_1: 'altijd',
|
||||
AP_PROVIDE_TEXT_2: 'als WiFi niet is verbonden',
|
||||
AP_PROVIDE_TEXT_3: 'nooit',
|
||||
AP_PREFERRED_CHANNEL: 'Voorkeurskanaal',
|
||||
AP_HIDE_SSID: 'SSID verbergen',
|
||||
AP_CLIENTS: 'AP Clients',
|
||||
AP_MAX_CLIENTS: 'Max Clients',
|
||||
AP_LOCAL_IP: 'Local IP',
|
||||
NETWORK_SCAN: 'Scan WiFi Networken',
|
||||
IDLE: 'Idle',
|
||||
LOST: 'Verloren',
|
||||
SCANNING: 'Scannen',
|
||||
SCAN_AGAIN: 'Opnieuw scannen',
|
||||
NETWORK_SCANNER: 'Netwerk Scanner',
|
||||
NETWORK_NO_WIFI: 'Geen WiFi networken gevonden',
|
||||
NETWORK_BLANK_SSID: 'laat leeg om WiFi uit te schakelen',
|
||||
TX_POWER: 'Tx Vermogen',
|
||||
HOSTNAME: 'Hostname',
|
||||
NETWORK_DISABLE_SLEEP: 'WiFi Sleep Mode uitzetten',
|
||||
NETWORK_LOW_BAND: 'Lagere WiFi bandbreedte gebruiken',
|
||||
NETWORK_USE_DNS: 'Activeer mDNS Service',
|
||||
NETWORK_ENABLE_CORS: 'Activeer CORS',
|
||||
NETWORK_CORS_ORIGIN: 'CORS origin',
|
||||
NETWORK_ENABLE_IPV6: 'Activeer IPv6 support',
|
||||
NETWORK_FIXED_IP: 'Gebruik vast IP addres',
|
||||
NETWORK_GATEWAY: 'Gateway',
|
||||
NETWORK_SUBNET: 'Subnetmasker',
|
||||
NETWORK_DNS: 'DNS Servers',
|
||||
ADDRESS_OF: '{0} Address',
|
||||
ADMIN: 'Admin',
|
||||
GUEST: 'Gast',
|
||||
NEW: 'Nieuwe',
|
||||
NEW_NAME_OF: 'Hernoem {0}',
|
||||
ENTITY: 'Entiteit',
|
||||
MIN: 'min',
|
||||
MAX: 'max'
|
||||
};
|
||||
|
||||
export default nl;
|
||||
309
interface/src/i18n/no/index.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
import type { Translation } from '../i18n-types';
|
||||
/* prettier-ignore */
|
||||
/* eslint-disable */
|
||||
|
||||
const no: Translation = {
|
||||
LANGUAGE: 'Språk',
|
||||
RETRY: 'Forsøk igjen',
|
||||
LOADING: 'Laster',
|
||||
IS_REQUIRED: '{0} er nødvendig',
|
||||
SIGN_IN: 'Logg inn',
|
||||
SIGN_OUT: 'Logg ut',
|
||||
USERNAME: 'Brukernavn',
|
||||
PASSWORD: 'Passord',
|
||||
SU_PASSWORD: 'su Passord',
|
||||
DASHBOARD: 'Dashboard',
|
||||
SETTINGS_OF: '{0} Innstillinger',
|
||||
SAVED: 'lagret',
|
||||
HELP_OF: '{0} Hjelp',
|
||||
LOGGED_IN: 'Logget in som {name}',
|
||||
PLEASE_SIGNIN: 'Venligst logge inn for å fortsetta',
|
||||
UPLOAD_SUCCESSFUL: 'Opplasting lykkes',
|
||||
DOWNLOAD_SUCCESSFUL: 'Nedlasting lykkes',
|
||||
INVALID_LOGIN: 'Ugyldig innlogging',
|
||||
NETWORK: 'Nettverk',
|
||||
SECURITY: 'Sikkerhet',
|
||||
ONOFF_CAP: 'PÅ/AV',
|
||||
ONOFF: 'på/av',
|
||||
TYPE: 'Type',
|
||||
DESCRIPTION: 'Beskrivelse',
|
||||
ENTITIES: 'Ojekter',
|
||||
REFRESH: 'Oppdater',
|
||||
EXPORT: 'Eksport',
|
||||
DEVICE_DETAILS: 'Enhetsdetaljer',
|
||||
ID_OF: '{0}-ID',
|
||||
DEVICE: 'Enhets',
|
||||
PRODUCT: 'Produkt',
|
||||
VERSION: 'Versjon',
|
||||
BRAND: 'Fabrikat',
|
||||
ENTITY_NAME: 'Objektsnavn',
|
||||
VALUE: '{{Verdi|verdi}}',
|
||||
SHOW_FAV: ' Vis kun favoritter',
|
||||
DEVICE_SENSOR_DATA: 'Enheter og Sensordata',
|
||||
DEVICES_SENSORS: 'Enheter og Sensorer',
|
||||
ATTACHED_SENSORS: 'Tilkoblede EMS-ESP Sensorer',
|
||||
RUN_COMMAND: 'Kjør kommando',
|
||||
CHANGE_VALUE: 'Endre Verdi',
|
||||
CANCEL: 'Avbryt',
|
||||
RESET: 'Nullstill',
|
||||
SEND: 'Send',
|
||||
SAVE: 'Lagre',
|
||||
REMOVE: 'Fjern',
|
||||
PROBLEM_UPDATING: 'Problem med oppdatering',
|
||||
PROBLEM_LOADING: 'Problem med opplasting',
|
||||
ACCESS_DENIED: 'Tilgang nektet',
|
||||
ANALOG_SENSOR: 'Analog Sensor',
|
||||
ANALOG_SENSORS: 'Analoge Sensorer',
|
||||
UPDATED_OF: '{0} Oppdatert',
|
||||
UPDATE_OF: '{0} Oppdater',
|
||||
REMOVED_OF: '{0} Slettet',
|
||||
DELETION_OF: '{0} Sletting',
|
||||
OFFSET: 'Kompensering',
|
||||
FACTOR: 'Faktor',
|
||||
FREQ: 'Frekvens',
|
||||
DUTY_CYCLE: 'Duty Cycle',
|
||||
UNIT: 'UoM',
|
||||
STARTVALUE: 'Startverdi',
|
||||
WARN_GPIO: 'Advarsel: vær forsiktig ved aktivering av GPIO!',
|
||||
EDIT: 'Endre',
|
||||
SENSOR: 'Sensor',
|
||||
TEMP_SENSOR: 'Temperatursensor',
|
||||
TEMP_SENSORS: 'Temperaturesensorer',
|
||||
WRITE_CMD_SENT: 'Skriv kommando sent', // TODO translate
|
||||
WRITE_CMD_FAILED: 'Skriv kommando failed', // TODO translate
|
||||
EMS_BUS_WARNING: 'EMS bussen koblet ned. Hvis denne advarselen fortsetter etter noen f¨sekunder sjekk instillinger og prosessorkort',
|
||||
EMS_BUS_SCANNING: 'Søker etter EMS enheter...',
|
||||
CONNECTED: 'Tilkoblet',
|
||||
TX_ISSUES: 'Tx problemer - prøv en annen Tx Modus',
|
||||
DISCONNECTED: 'Frakoblet',
|
||||
EMS_SCAN: 'Er du sikker på du vil starte full søking av EMS bussen?',
|
||||
EMS_BUS_STATUS: 'EMS Buss Status',
|
||||
ACTIVE_DEVICES: 'Aktive Enheter og Sensorer',
|
||||
EMS_DEVICE: 'EMS Enhet',
|
||||
SUCCESS: 'VELLYKKET',
|
||||
FAIL: 'MISLYKKET',
|
||||
QUALITY: 'KVALITET',
|
||||
SCAN_DEVICES: 'Søk etter nye enheter',
|
||||
EMS_BUS_STATUS_TITLE: 'EMS Buss & Aktivitet Status',
|
||||
SCAN: 'Søk',
|
||||
STATUS_NAMES: [
|
||||
'EMS Telegrammer Mottatt (Rx)',
|
||||
'EMS Lest (Tx)',
|
||||
'EMS Skrevet (Tx)',
|
||||
'Temperatur Sensor Lest',
|
||||
'Analog Sensor Lest',
|
||||
'MQTT Publiseringer',
|
||||
'API Anrop',
|
||||
'Syslog Meldinger'
|
||||
],
|
||||
NUM_DEVICES: '{num} Enhet{{er}}',
|
||||
NUM_TEMP_SENSORS: '{num} Temperatursensor{{er}}',
|
||||
NUM_ANALOG_SENSORS: '{num} Analogsensor{{er}}',
|
||||
NUM_DAYS: '{num} sag{{er}}',
|
||||
NUM_SECONDS: '{num} sekund{{er}}',
|
||||
NUM_HOURS: '{num} time{{r}}',
|
||||
NUM_MINUTES: '{num} minutt{{er}}',
|
||||
APPLICATION_SETTINGS: 'Innstillinger',
|
||||
CUSTOMIZATION: 'Tilpasninger',
|
||||
APPLICATION_RESTARTING: 'EMS-ESP restarter',
|
||||
INTERFACE_BOARD_PROFILE: 'Interface Prosessor Profil',
|
||||
BOARD_PROFILE_TEXT: 'Velg en pre-konfigurert prosessor profil fra listen under eller velg Tilpasset for å konfigurere dine egne innstillinger',
|
||||
BOARD_PROFILE: 'Prosessor Profil',
|
||||
CUSTOM: 'Custom',
|
||||
GPIO_OF: '{0} GPIO',
|
||||
BUTTON: 'Knapp',
|
||||
TEMPERATURE: 'Temperatur',
|
||||
PHY_TYPE: 'Eth PHY Type',
|
||||
DISABLED: 'avslått',
|
||||
TX_MODE: 'Tx Mode',
|
||||
HARDWARE: 'Hardware',
|
||||
EMS_BUS: '{{BUS|EMS BUS}}',
|
||||
GENERAL_OPTIONS: 'Generelle Innstillinger',
|
||||
LANGUAGE_ENTITIES: 'Språk (for objekter)',
|
||||
HIDE_LED: 'Skjul LED',
|
||||
ENABLE_TELNET: 'Aktiver Telnet',
|
||||
ENABLE_ANALOG: 'Aktiver Analoge Sensorer',
|
||||
CONVERT_FAHRENHEIT: 'Konverter temperatur til Fahrenheit',
|
||||
BYPASS_TOKEN: 'Utelat Aksess Token authorisering av API kall',
|
||||
READONLY: 'Aktiver read-only modus (blokker all EMS Tx Skriving)',
|
||||
UNDERCLOCK_CPU: 'Underklokking av prosessorhastighet',
|
||||
ENABLE_SHOWER_TIMER: 'Aktiver Dusjtimer',
|
||||
ENABLE_SHOWER_ALERT: 'Aktiver Dusj-varsling',
|
||||
TRIGGER_TIME: 'Aktiveringstid',
|
||||
COLD_SHOT_DURATION: 'Tid på kaldt vann',
|
||||
FORMATTING_OPTIONS: 'Formatteringsalternativs',
|
||||
BOOLEAN_FORMAT_DASHBOARD: 'Bool Format Dashboard',
|
||||
BOOLEAN_FORMAT_API: 'Bool Format API/MQTT',
|
||||
ENUM_FORMAT: 'Enum Format API/MQTT',
|
||||
INDEX: 'Indeks',
|
||||
ENABLE_PARASITE: 'Aktiver parasitt strømforsyning',
|
||||
LOGGING: 'Logging',
|
||||
LOG_HEX: 'Logg EMS telegrammer i hexadesimal',
|
||||
ENABLE_SYSLOG: 'Aktiver Syslog',
|
||||
LOG_LEVEL: 'Log Level',
|
||||
MARK_INTERVAL: 'Oppdateringsintervall',
|
||||
SECONDS: 'sekunder',
|
||||
MINUTES: 'minutter',
|
||||
HOURS: 'timer',
|
||||
RESTART: 'Omstart',
|
||||
RESTART_TEXT: 'EMS-ESP må omstartes for å iverksette endrede systeminstillinger',
|
||||
RESTART_CONFIRM: 'Er du sikker på at du vil omstarte EMS-ESP?',
|
||||
COMMAND: 'Kommando',
|
||||
CUSTOMIZATIONS_RESTART: 'Alle tilpasninger har blitt slettet. Restarter...',
|
||||
CUSTOMIZATIONS_FULL: 'Antall valgte objekter for høyt. Largre i mindre antall om gangen',
|
||||
CUSTOMIZATIONS_SAVED: 'Tilpasninger lagret',
|
||||
CUSTOMIZATIONS_HELP_1: 'Velg en enhet og tilpass underenheter med hjelp av alternativer eller velg å gi nytt navn',
|
||||
CUSTOMIZATIONS_HELP_2: 'merk som favoritt',
|
||||
CUSTOMIZATIONS_HELP_3: 'inaktiviser skriving',
|
||||
CUSTOMIZATIONS_HELP_4: 'ekskludere fra MQTT og API',
|
||||
CUSTOMIZATIONS_HELP_5: 'gjemme fra Dashboard',
|
||||
CUSTOMIZATIONS_HELP_6: 'remove from memory',
|
||||
SELECT_DEVICE: 'Velg en enhet',
|
||||
SET_ALL: 'sett alle',
|
||||
OPTIONS: 'Alternativ',
|
||||
NAME: 'Navn',
|
||||
CUSTOMIZATIONS_RESET: 'Er du sikker på att du vil fjerne tilpassninger inkludert innstillinger for Temperatur og Analoge sensorer?',
|
||||
DEVICE_ENTITIES: 'Enhets objekter',
|
||||
USER_CUSTOMIZATION: 'Brukertilpasninger',
|
||||
SUPPORT_INFORMATION: 'Supportinformasjon',
|
||||
CLICK_HERE: 'Klikk her',
|
||||
HELP_INFORMATION_1: 'Besøk wiki for instruksjoner for å konfigurere EMS-ESP',
|
||||
HELP_INFORMATION_2: 'For community-support besøk vår Discord-server',
|
||||
HELP_INFORMATION_3: 'For å be om en ny funksjon eller melde feil',
|
||||
HELP_INFORMATION_4: 'husk å laste ned og legg ved din systeminformasjon for en raskere respons når du rapporterer et problem',
|
||||
HELP_INFORMATION_5: 'EMS-ESP er gratis og åpen kildekode. Bidra til utviklingen ved å gi oss en stjerne på GitHub!',
|
||||
SUPPORT_INFO: 'Supportinfo',
|
||||
UPLOAD_OF: '{0} Opplasning',
|
||||
UPLOAD: 'Opplasning',
|
||||
DOWNLOAD: 'Nedlasting',
|
||||
ABORTED: 'avbrutt',
|
||||
FAILED: 'feilet',
|
||||
SUCCESSFUL: 'vellykket',
|
||||
SYSTEM: 'System',
|
||||
LOG_OF: '{0} Logg',
|
||||
STATUS_OF: '{0} Status',
|
||||
UPLOAD_DOWNLOAD: 'Opp/Nedlasting',
|
||||
SYSTEM_VERSION_RUNNING: 'Du benytter versjon',
|
||||
SYSTEM_APPLY_FIRMWARE: 'for å aktivere ny firmware',
|
||||
CLOSE: 'Steng',
|
||||
USE: 'Bruk',
|
||||
FACTORY_RESET: 'Sett tilbake til fabrikkinstilling',
|
||||
SYSTEM_FACTORY_TEXT: 'Enhet har blitt satt tilbake til fabrikkinstilling og vil restarte',
|
||||
SYSTEM_FACTORY_TEXT_DIALOG: 'Er du sikker på at du vil resette enheten til fabrikkinstillinger?',
|
||||
VERSION_CHECK: 'Versjonsjekk',
|
||||
THE_LATEST: 'Den nyeste',
|
||||
OFFICIAL: 'official',
|
||||
DEVELOPMENT: 'development',
|
||||
VERSION_IS: 'versjonen er',
|
||||
RELEASE_NOTES: 'release notes',
|
||||
EMS_ESP_VER: 'EMS-ESP Version',
|
||||
PLATFORM: 'Enhet (Platform / SDK)',
|
||||
UPTIME: 'System Oppetid',
|
||||
CPU_FREQ: 'CPU Frekvens',
|
||||
HEAP: 'Heap (Ledig / Max Allokert)',
|
||||
PSRAM: 'PSRAM (Størrelse / Ledig)',
|
||||
FLASH: 'Flash Chip (Størrelse / Hastighet)',
|
||||
APPSIZE: 'Applikasjon (Brukt / Ledig)',
|
||||
FILESYSTEM: 'File System (Brukt / Ledig)',
|
||||
BUFFER_SIZE: 'Max Buffer Størrelse',
|
||||
COMPACT: 'Komprimere',
|
||||
ENABLE_OTA: 'Aktiviser OTA oppdateringer',
|
||||
DOWNLOAD_CUSTOMIZATION_TEXT: 'Last ned objektstilpasninger',
|
||||
DOWNLOAD_SETTINGS_TEXT: 'Last ned applikasjonskonfigurasjon. Vær varsom med å dele fila da den inneholder passord og annen sensitiv system informasjon',
|
||||
UPLOAD_TEXT: 'Last opp en ny firmware (.bin) fil, innstillinger eller tilpassninger (.json) fil nedenfor',
|
||||
UPLOADING: 'Opplasting',
|
||||
UPLOAD_DROP_TEXT: 'Slipp fil eller klikk her',
|
||||
ERROR: 'Ukjent feil, prøv igjen',
|
||||
TIME_SET: 'Still in tid',
|
||||
MANAGE_USERS: 'Administrer Brukere',
|
||||
IS_ADMIN: 'er Admin',
|
||||
USER_WARNING: 'Du må ha minst en admin bruker konfigurert',
|
||||
ADD: 'Legg til',
|
||||
ACCESS_TOKEN_FOR: 'Aksess Token for',
|
||||
ACCESS_TOKEN_TEXT: 'Token nedenfor benyttes med REST API-kall som krever autorisering. Den kan sendes med enten som en Bearer token i Authorization-headern eller i access_token URL query-parameter.',
|
||||
GENERATING_TOKEN: 'Generer token',
|
||||
USER: 'Bruker',
|
||||
MODIFY: 'Endre',
|
||||
SU_TEXT: 'su brukeren (super user) passord benyttes for å signere autentiserings token samt å tillate admin privileger i konsoll modus.',
|
||||
NOT_ENABLED: 'Ikke aktiv',
|
||||
ERRORS_OF: '{0} Feil',
|
||||
DISCONNECT_REASON: 'Årsak til nedkobling',
|
||||
ENABLE_MQTT: 'Aktiver MQTT',
|
||||
BROKER: 'Broker',
|
||||
CLIENT: 'Client',
|
||||
BASE_TOPIC: 'Base',
|
||||
OPTIONAL: 'Valgfritt',
|
||||
FORMATTING: 'Formatering',
|
||||
MQTT_FORMAT: 'Topic/Payload Format',
|
||||
MQTT_NEST_1: 'Nestet i en topic',
|
||||
MQTT_NEST_2: 'Som individuelle topics',
|
||||
MQTT_RESPONSE: 'Publiser kommandoer til en `response` topic',
|
||||
MQTT_PUBLISH_TEXT_1: 'Publiser singel verdi topics ved endringer',
|
||||
MQTT_PUBLISH_TEXT_2: 'Publiser til kommando topics (ioBroker)',
|
||||
MQTT_PUBLISH_TEXT_3: 'Aktiver MQTT Discovery (Home Assistant, Domoticz)',
|
||||
MQTT_PUBLISH_TEXT_4: 'Prefiks for Discovery topics',
|
||||
MQTT_PUBLISH_INTERVALS: 'Publiseringsintervall',
|
||||
MQTT_INT_BOILER: 'Fyr/Varmepumpe',
|
||||
MQTT_INT_THERMOSTATS: 'Termostat',
|
||||
MQTT_INT_SOLAR: 'Solpaneler',
|
||||
MQTT_INT_MIXER: 'Blandeventil',
|
||||
MQTT_INT_HEARTBEAT: 'Heartbeat',
|
||||
MQTT_QUEUE: 'MQTT Queue',
|
||||
DEFAULT: 'Standard',
|
||||
MQTT_ENTITY_FORMAT: 'Entity ID format', // TODO translate
|
||||
MQTT_ENTITY_FORMAT_0: 'Single instance, long name (v3.4)', // TODO translate
|
||||
MQTT_ENTITY_FORMAT_1: 'Single instance, short name', // TODO translate
|
||||
MQTT_ENTITY_FORMAT_2: 'Multiple instances, short name', // TODO translate
|
||||
MQTT_CLEAN_SESSION: 'Benytt Clean Session',
|
||||
MQTT_RETAIN_FLAG: 'Alltid sett Retain flag',
|
||||
INACTIVE: 'Innaktiv',
|
||||
ACTIVE: 'Aktiv',
|
||||
UNKNOWN: 'Ukjent',
|
||||
SET_TIME: 'Sett Tid',
|
||||
SET_TIME_TEXT: 'Skriv inn dato og klokke nedenfor',
|
||||
LOCAL_TIME: 'Lokaltid',
|
||||
UTC_TIME: 'UTC Tid',
|
||||
ENABLE_NTP: 'Aktiver NTP',
|
||||
NTP_SERVER: 'NTP Server',
|
||||
TIME_ZONE: 'Tidssone',
|
||||
ACCESS_POINT: 'Aksesspunkt',
|
||||
AP_PROVIDE: 'Aktiver Aksesspunkt',
|
||||
AP_PROVIDE_TEXT_1: 'alltid',
|
||||
AP_PROVIDE_TEXT_2: 'når WiFi er utilgjengelig',
|
||||
AP_PROVIDE_TEXT_3: 'aldri',
|
||||
AP_PREFERRED_CHANNEL: 'Foretrukket kanal',
|
||||
AP_HIDE_SSID: 'Skjul SSID',
|
||||
AP_CLIENTS: 'AP Clients',
|
||||
AP_MAX_CLIENTS: 'Max Clients',
|
||||
AP_LOCAL_IP: 'Local IP',
|
||||
NETWORK_SCAN: 'Søk etter trådløst nettverk',
|
||||
IDLE: 'Klar',
|
||||
LOST: 'Mistet',
|
||||
SCANNING: 'Søker',
|
||||
SCAN_AGAIN: 'Søk igjen',
|
||||
NETWORK_SCANNER: 'Nettverk Scanner',
|
||||
NETWORK_NO_WIFI: 'Ingen trådløse nett funnet',
|
||||
NETWORK_BLANK_SSID: 'la feltet være blankt for å deaktivisere trådløst nettverk',
|
||||
TX_POWER: 'Tx Effekt',
|
||||
HOSTNAME: 'Hostname',
|
||||
NETWORK_DISABLE_SLEEP: 'Hindre at trådløst nettverk går i Sleep Mode',
|
||||
NETWORK_LOW_BAND: 'Benytt smalere båndbredde på trådløst nettverk',
|
||||
NETWORK_USE_DNS: 'Aktiviser mDNS Service',
|
||||
NETWORK_ENABLE_CORS: 'Aktiviser CORS',
|
||||
NETWORK_CORS_ORIGIN: 'CORS origin',
|
||||
NETWORK_ENABLE_IPV6: 'Aktiviser IPv6 støtte',
|
||||
NETWORK_FIXED_IP: 'Benytt statisk IP adresse',
|
||||
NETWORK_GATEWAY: 'Gateway',
|
||||
NETWORK_SUBNET: 'Nettverksmaske',
|
||||
NETWORK_DNS: 'DNS Servers',
|
||||
ADDRESS_OF: '{0} Address',
|
||||
ADMIN: 'Admin',
|
||||
GUEST: 'Gjest',
|
||||
NEW: 'Ny',
|
||||
NEW_NAME_OF: 'Bytt navn {0}',
|
||||
ENTITY: 'Entitet',
|
||||
MIN: 'min',
|
||||
MAX: 'max'
|
||||
};
|
||||
|
||||
export default no;
|
||||
309
interface/src/i18n/pl/index.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
import type { BaseTranslation } from '../i18n-types';
|
||||
/* prettier-ignore */
|
||||
/* eslint-disable */
|
||||
|
||||
const pl: BaseTranslation = {
|
||||
LANGUAGE: 'Język',
|
||||
RETRY: 'Ponów',
|
||||
LOADING: 'Ładowanie',
|
||||
IS_REQUIRED: 'Pole {0} nie może być puste!',
|
||||
SIGN_IN: 'Zaloguj się',
|
||||
SIGN_OUT: 'Wyloguj się',
|
||||
USERNAME: '{{Użytkownik|Nazwa użytkownika|}}',
|
||||
PASSWORD: 'Hasło',
|
||||
SU_PASSWORD: 'Hasło "su"',
|
||||
DASHBOARD: 'Pulpit',
|
||||
SETTINGS_OF: 'Ustawienia {0}',
|
||||
SAVED: 'zostały zapisane.',
|
||||
HELP_OF: 'Pomoc {0}',
|
||||
LOGGED_IN: 'Zalogowano użytkownika {name}.',
|
||||
PLEASE_SIGNIN: 'Zaloguj się aby kontynuować.',
|
||||
UPLOAD_SUCCESSFUL: 'Wysyłanie zakończone.',
|
||||
DOWNLOAD_SUCCESSFUL: 'Pobieranie zakończone.',
|
||||
INVALID_LOGIN: 'Nieprawidłowy użytkownik lub hasło!',
|
||||
NETWORK: '{{Sieć|sieci|}}',
|
||||
SECURITY: '{{B|b|}}ezpieczeństw{{o|a|}}',
|
||||
ONOFF_CAP: 'wł./wył.',
|
||||
ONOFF: 'włączono/wyłączono',
|
||||
TYPE: 'Typ',
|
||||
DESCRIPTION: 'Opis',
|
||||
ENTITIES: 'Encje',
|
||||
REFRESH: 'Odśwież',
|
||||
EXPORT: 'Eksportuj',
|
||||
DEVICE_DETAILS: 'Szczegóły urządzenia',
|
||||
ID_OF: 'ID {0}',
|
||||
DEVICE: 'urządzenia',
|
||||
PRODUCT: 'produktu',
|
||||
BRAND: 'Marka',
|
||||
VERSION: 'Wersja',
|
||||
ENTITY_NAME: 'Nazwa encji',
|
||||
VALUE: '{{W|w|}}artość',
|
||||
SHOW_FAV: 'Pokaż tylko "ulubione"',
|
||||
DEVICE_SENSOR_DATA: 'Dane z urządzeń i czujników',
|
||||
DEVICES_SENSORS: 'Urządzenia i czujniki',
|
||||
ATTACHED_SENSORS: 'Urządzenia podłączone do EMS-ESP (czujniki temperatury/analogowe/cyfrowe, wyjścia cyfrowe)',
|
||||
RUN_COMMAND: 'Wykonaj komendę',
|
||||
CHANGE_VALUE: 'Zmień wartość',
|
||||
CANCEL: 'Anuluj',
|
||||
RESET: 'Reset{{uj|owanie|}}',
|
||||
SEND: 'Wyślij',
|
||||
SAVE: 'Zapisz',
|
||||
REMOVE: 'Usuń',
|
||||
PROBLEM_UPDATING: 'Problem z uaktualnieniem!',
|
||||
PROBLEM_LOADING: 'Problem z załadowaniem!',
|
||||
ACCESS_DENIED: 'Brak dostępu!',
|
||||
ANALOG_SENSOR: 'urządzenia podłączonego do EMS-ESP',
|
||||
ANALOG_SENSORS: 'Urządzenia podłączone do EMS-ESP',
|
||||
UPDATED_OF: 'Zaktualizowano ustawienia {0}.',
|
||||
UPDATE_OF: 'Aktualizacja {0}',
|
||||
REMOVED_OF: 'Usunięto ustawienia {0}.',
|
||||
DELETION_OF: 'Kasowanie {0}',
|
||||
OFFSET: 'Korekta ±',
|
||||
FACTOR: 'Mnożnik',
|
||||
FREQ: 'Częstotliwość',
|
||||
DUTY_CYCLE: 'Wypełnienie',
|
||||
UNIT: 'J.m.',
|
||||
STARTVALUE: 'Wartość początkowa',
|
||||
WARN_GPIO: 'Uwaga! Zachowaj ostrożność przypisując GPIO do urządzenia!',
|
||||
EDIT: 'Edycja',
|
||||
SENSOR: 'czujnika',
|
||||
TEMP_SENSOR: 'czujnika temperatury',
|
||||
TEMP_SENSORS: 'Czujniki temperatury 1-Wire®',
|
||||
WRITE_CMD_SENT: 'Komenda zapisu została wysłana.',
|
||||
WRITE_CMD_FAILED: 'Komenda zapisu nie powiodła się!',
|
||||
EMS_BUS_WARNING: 'Brak połączenia z magistralą EMS. Jeśli ten błąd występuje dłużej niż kilka sekund, sprawdź ustawienia oraz profil płytki interfejsu.',
|
||||
EMS_BUS_SCANNING: 'Trwa skanowanie urządzeń na magistrali EMS...',
|
||||
CONNECTED: '{{połączono|połączenie|}}',
|
||||
TX_ISSUES: 'problem z zapisem na magistralę EMS, spróbuj wybrać inny "Tryb transmisji (Tx)"',
|
||||
DISCONNECTED: 'brak połączenia',
|
||||
EMS_SCAN: 'Czy na pewno wykonać pełne skanowanie magistrali EMS?',
|
||||
EMS_BUS_STATUS: 'Status magistrali EMS',
|
||||
ACTIVE_DEVICES: 'Aktywne urządzenia i czujniki',
|
||||
EMS_DEVICE: 'Urządzenie EMS',
|
||||
SUCCESS: 'Udane',
|
||||
FAIL: 'Nieudane',
|
||||
QUALITY: 'Jakość',
|
||||
SCAN_DEVICES: 'Wyszukiwanie nowych urządzeń',
|
||||
EMS_BUS_STATUS_TITLE: 'Aktywność',
|
||||
SCAN: 'Skanuj',
|
||||
STATUS_NAMES: [
|
||||
'EMS, telegramy odebrane (Rx)',
|
||||
'EMS, wysłane telegramy "odczyt" (Tx)',
|
||||
'EMS, wysłane telegramy "zapis" (Tx)',
|
||||
'Odczyty czujników temperatury 1-Wire®',
|
||||
'Odczyty czujników analogowych i cyfrowych',
|
||||
'Publikacje MQTT',
|
||||
'Wywołania API',
|
||||
'Wpisy w SysLog'
|
||||
],
|
||||
NUM_DEVICES: '{num} urządze{{ń|nie|nia|nia|ń}} EMS',
|
||||
NUM_TEMP_SENSORS: '{num} czujni{{ków|k|ki|ki|ków}} temperatury',
|
||||
NUM_ANALOG_SENSORS: '{num} inn{{ych|e|e|e|ych}} urządze{{ń|nie|nia(two)|nia|ń}} podłączon{{ych|e|e|e|ych}} do EMS-ESP',
|
||||
NUM_DAYS: '{num} d{{ni|zień|ni|ni|ni}}',
|
||||
NUM_SECONDS: '{num} sekun{{d|da|dy|dy|d}}',
|
||||
NUM_HOURS: '{num} godzi{{n|na|ny|ny|n}}',
|
||||
NUM_MINUTES: '{num} minu{{t|ta|ty|ty|t}}',
|
||||
APPLICATION_SETTINGS: 'Ustawienia aplikacji',
|
||||
CUSTOMIZATION: 'Personalizacja',
|
||||
APPLICATION_RESTARTING: 'Trwa ponowne uruchamianie',
|
||||
INTERFACE_BOARD_PROFILE: 'Profil płytki interfejsu',
|
||||
BOARD_PROFILE_TEXT: 'Wybierz z listy gotowy profil płytki interfejsu lub "własny..." i samodzielnie skonfiguruj posiadany sprzęt.',
|
||||
BOARD_PROFILE: 'Profil płytki',
|
||||
CUSTOM: 'własny',
|
||||
GPIO_OF: 'GPIO {0}',
|
||||
BUTTON: 'przycisku',
|
||||
TEMPERATURE: '1-Wire®',
|
||||
PHY_TYPE: 'Typ układu ethernetowego (PHY)',
|
||||
DISABLED: '{{wyłączono|brak|}}',
|
||||
TX_MODE: 'Tryb transmisji (Tx)',
|
||||
EMS_BUS: '{{magistrali EMS|na magistrali|}}',
|
||||
HARDWARE: 'sprzętowy',
|
||||
GENERAL_OPTIONS: 'Opcje podstawowe',
|
||||
LANGUAGE_ENTITIES: 'Język encji',
|
||||
HIDE_LED: 'Wyłącz LED',
|
||||
ENABLE_TELNET: 'Aktywuj dostęp dla konsoli Telnet',
|
||||
ENABLE_ANALOG: 'Aktywuj urządzenia GPIO (czujniki analogowe i cyfrowe oraz wyjścia cyfrowe)',
|
||||
CONVERT_FAHRENHEIT: 'Konwertuj temperatury do skali Fahrenheita',
|
||||
BYPASS_TOKEN: 'Pomiń autoryzację tokenem w wywołaniach API',
|
||||
READONLY: 'Tryb pracy "tylko do odczytu" (blokuje wszystkie komendy zapisu na magistralę EMS)',
|
||||
UNDERCLOCK_CPU: 'Obniż taktowanie CPU',
|
||||
ENABLE_SHOWER_TIMER: 'Aktywuj minutnik prysznica',
|
||||
ENABLE_SHOWER_ALERT: 'Aktywuj alarm prysznica',
|
||||
TRIGGER_TIME: 'Wyzwalaj po czasie',
|
||||
COLD_SHOT_DURATION: 'Czas trwania tryśnięcia zimnej wody',
|
||||
FORMATTING_OPTIONS: 'Opcje formatowania',
|
||||
BOOLEAN_FORMAT_DASHBOARD: 'Wartości dwustanowe na pulpicie',
|
||||
BOOLEAN_FORMAT_API: 'Wartości dwustanowe w API/MQTT',
|
||||
ENUM_FORMAT: 'Wartości z listy w API/MQTT',
|
||||
INDEX: 'indeks',
|
||||
ENABLE_PARASITE: 'Aktywuj zasilanie pasożytnicze',
|
||||
LOGGING: 'Logowanie',
|
||||
LOG_HEX: 'Loguj telegramy EMS w systemie szesnastkowym (hex)',
|
||||
ENABLE_SYSLOG: 'Aktywuj SysLog',
|
||||
LOG_LEVEL: 'Poziom logowania',
|
||||
MARK_INTERVAL: 'Znaczniki interwałów (0=brak)',
|
||||
SECONDS: 'sekund',
|
||||
MINUTES: 'minut',
|
||||
HOURS: 'godzin',
|
||||
RESTART: 'Restart',
|
||||
RESTART_TEXT: 'Aby zastosować wprowadzone zmiany interfejs EMS-ESP musi zostać zrestartowany.',
|
||||
RESTART_CONFIRM: 'Jesteś pewien, że chcesz zrestartować interfejs EMS-ESP?',
|
||||
COMMAND: 'KOMENDA',
|
||||
CUSTOMIZATIONS_RESTART: 'Wszystkie personalizacje zostały usunięte. Restartuję...',
|
||||
CUSTOMIZATIONS_FULL: 'Wybrano za dużo obiektów. Wprowadź zmiany w mniejszych partiach.',
|
||||
CUSTOMIZATIONS_SAVED: 'Personalizacje zostały zapisane.',
|
||||
CUSTOMIZATIONS_HELP_1: 'Wybierz urządzenie EMS, dostosuj opcje lub kliknij by zmienić nazwę encji.',
|
||||
CUSTOMIZATIONS_HELP_2: 'oznacz jako ulubioną',
|
||||
CUSTOMIZATIONS_HELP_3: 'zablokuj akcje zapisu',
|
||||
CUSTOMIZATIONS_HELP_4: 'wyklucz z MQTT i API',
|
||||
CUSTOMIZATIONS_HELP_5: 'ukryj na pulpicie',
|
||||
CUSTOMIZATIONS_HELP_6: 'remove from memory',
|
||||
SELECT_DEVICE: 'wybierz urządzenie',
|
||||
SET_ALL: 'Ustaw wszystko jako',
|
||||
OPTIONS: 'Opcje',
|
||||
NAME: '{{Nazwa|nazwa|}}',
|
||||
CUSTOMIZATIONS_RESET: 'Czy jesteś pewien, że chcesz usunąć wszystkie personalizacje łącznie z ustawieniami dla czujników temperatury 1-Wire® i urządzeń podłączonych do EMS-ESP?',
|
||||
DEVICE_ENTITIES: 'Encje urządzenia',
|
||||
USER_CUSTOMIZATION: 'Personalizacje użytkownika',
|
||||
SUPPORT_INFORMATION: 'Informacje dotyczące wsparcia',
|
||||
CLICK_HERE: 'Kliknij tu',
|
||||
HELP_INFORMATION_1: 'Aby uzyskać instrukcje dotyczące konfiguracji EMS-ESP skorzystaj z wiki w internecie',
|
||||
HELP_INFORMATION_2: 'Aby dołączyć do naszego serwera Discord i komunikować się na żywo ze społecznością',
|
||||
HELP_INFORMATION_3: 'Aby zaproponować nową funkcjonalność lub zgłosić problem',
|
||||
HELP_INFORMATION_4: 'Zgłaszając problem, nie zapomnij dołączyć informacji o swoim systemie!',
|
||||
HELP_INFORMATION_5: 'EMS-ESP jest darmowym projektem typu open-source. Aby go wesprzeć, rozważ przyznanie nam gwiazdki na Github!',
|
||||
SUPPORT_INFO: 'Pobierz informacje',
|
||||
UPLOAD_OF: 'Wysyłanie {0}',
|
||||
UPLOAD: 'Wysyłanie',
|
||||
DOWNLOAD: '{{P|p||P}}obier{{anie|z||z}}',
|
||||
ABORTED: 'zostało przerwane!',
|
||||
FAILED: 'nie powiodło się!',
|
||||
SUCCESSFUL: 'powiodło się.',
|
||||
SYSTEM: '{{S|s||s}}yste{{m|mu||mowy}}',
|
||||
LOG_OF: 'Log {0}',
|
||||
STATUS_OF: 'Status {0}',
|
||||
UPLOAD_DOWNLOAD: 'Przesyłanie plików',
|
||||
SYSTEM_VERSION_RUNNING: 'Obecnie zainstalowana wersja to:',
|
||||
SYSTEM_APPLY_FIRMWARE: '',
|
||||
CLOSE: 'Zamknij',
|
||||
USE: 'Aby zaktualizować firmware skorzystaj z funkcji',
|
||||
FACTORY_RESET: 'Ustawienia fabryczne',
|
||||
SYSTEM_FACTORY_TEXT: 'Interfejs EMS-ESP został przywrócony do ustawień fabrycznych i zostanie teraz ponownie uruchomiony.',
|
||||
SYSTEM_FACTORY_TEXT_DIALOG: 'Czy jesteś pewien, że chcesz przywrócić ustawienia fabryczne interfejsu EMS-ESP? ',
|
||||
VERSION_CHECK: 'Sprawd{{ź|zanie|}} wersj{{ę|i|}}',
|
||||
THE_LATEST: 'Najnowsza',
|
||||
OFFICIAL: 'oficjalna',
|
||||
DEVELOPMENT: 'testowa',
|
||||
VERSION_IS: 'wersja to',
|
||||
RELEASE_NOTES: 'lista zmian',
|
||||
EMS_ESP_VER: 'Wersja EMS-ESP',
|
||||
PLATFORM: 'Urządzenie (platforma / SDK)',
|
||||
UPTIME: 'Czas działania systemu',
|
||||
CPU_FREQ: 'Taktowanie CPU',
|
||||
HEAP: 'HEAP (wolne / maksymalny przydział)',
|
||||
PSRAM: 'PSRAM (rozmiar / wolne)',
|
||||
FLASH: 'Flash (rozmiar / taktowanie)',
|
||||
APPSIZE: 'Aplikacja (wykorzystane / wolne)',
|
||||
FILESYSTEM: 'System plików (wykorzystane / wolne)',
|
||||
BUFFER_SIZE: 'Maksymalna pojemność bufora (ilość wpisów)',
|
||||
COMPACT: 'Kompaktowy',
|
||||
ENABLE_OTA: 'Aktywuj aktualizację OTA',
|
||||
DOWNLOAD_CUSTOMIZATION_TEXT: 'Pobierz personalizacje',
|
||||
DOWNLOAD_SETTINGS_TEXT: 'Pobierz ustawienia aplikacji. Uważaj jeśli udostępniasz plik z ustawieniami, ponieważ zawiera on hasła oraz inne wrażliwe informacje!',
|
||||
UPLOAD_TEXT: 'Wyślij firmware (.bin), ustawienia lub personalizacje (.json). Opcjonalnie, wyślij wcześniej plik walidacji (.md5).',
|
||||
UPLOADING: 'Wysłano',
|
||||
UPLOAD_DROP_TEXT: 'Przeciągnij tutaj plik lub kliknij',
|
||||
ERROR: 'Nieoczekiwany błąd, spróbuj ponownie!',
|
||||
TIME_SET: 'Zegar został ustawiony.',
|
||||
MANAGE_USERS: 'Zarządzanie użytkownikami',
|
||||
IS_ADMIN: '{{Administrator|Uprawnienia administratora|}}',
|
||||
USER_WARNING: 'Przynajmniej jeden użytkownik musi mieć uprawnienia administratora!',
|
||||
ADD: 'Doda{{j|wanie|}}',
|
||||
ACCESS_TOKEN_FOR: 'Token dostępu dla użytkownika',
|
||||
ACCESS_TOKEN_TEXT: 'Token jest używany w wywołaniach REST API wymagających autoryzacji. Można go przekazywać bezpośrednio lub przez URL.',
|
||||
GENERATING_TOKEN: 'Generowanie tokenu',
|
||||
USER: '{{Użytkownik|użytkownika|}}',
|
||||
MODIFY: 'Edycja',
|
||||
SU_TEXT: 'Hasło "su" (super-użytkownika) służy do podpisywania tokenów autoryzujących oraz włączania uprawnień administratora w konsoli.',
|
||||
NOT_ENABLED: 'nie aktywowano',
|
||||
ERRORS_OF: 'Błędy {0}',
|
||||
DISCONNECT_REASON: 'Przyczyna braku połączenia',
|
||||
ENABLE_MQTT: 'Aktywuj MQTT',
|
||||
BROKER: 'brokera',
|
||||
CLIENT: 'klienta',
|
||||
BASE_TOPIC: 'Prefiks bazowy (unikalny!)',
|
||||
OPTIONAL: 'opcjonalny',
|
||||
FORMATTING: 'Formatowanie',
|
||||
MQTT_FORMAT: 'Sposób publikowania danych',
|
||||
MQTT_NEST_1: 'zagnieżdżone w jednym temacie',
|
||||
MQTT_NEST_2: 'jako oddzielne tematy',
|
||||
MQTT_RESPONSE: 'Rezultat wykonania komendy publikuj w temacie "response"',
|
||||
MQTT_PUBLISH_TEXT_1: 'Tematy z pojedynczą wartością publikuj po jej zmianie',
|
||||
MQTT_PUBLISH_TEXT_2: 'Publikuj w tematach "command" (ioBroker)',
|
||||
MQTT_PUBLISH_TEXT_3: 'Włącz opcję "MQTT discovery" (Home Assistant, Domoticz)',
|
||||
MQTT_PUBLISH_TEXT_4: 'Prefiks dla "MQTT discovery"',
|
||||
MQTT_PUBLISH_INTERVALS: 'Interwały publikowania',
|
||||
MQTT_INT_BOILER: 'Kotły i pompy ciepła',
|
||||
MQTT_INT_THERMOSTATS: 'Termostaty',
|
||||
MQTT_INT_SOLAR: 'Panele solarne',
|
||||
MQTT_INT_MIXER: 'Mieszacze',
|
||||
MQTT_INT_HEARTBEAT: '"Heartbeat" (aktywność)',
|
||||
MQTT_QUEUE: 'Kolejka MQTT',
|
||||
DEFAULT: '{{Pozostałe|Domyślna|}}',
|
||||
MQTT_ENTITY_FORMAT: 'Format "Entity ID"',
|
||||
MQTT_ENTITY_FORMAT_0: 'długa nazwa (jak w v3.4)',
|
||||
MQTT_ENTITY_FORMAT_1: 'krótka nazwa',
|
||||
MQTT_ENTITY_FORMAT_2: 'prefiks bazowy + krótka nazwa',
|
||||
MQTT_CLEAN_SESSION: 'Ustawiaj flagę "Clean session"',
|
||||
MQTT_RETAIN_FLAG: 'Ustawiaj flagę "Retain"',
|
||||
INACTIVE: 'nieaktywn{{y|a|}}',
|
||||
ACTIVE: 'aktywny',
|
||||
UNKNOWN: 'nieznany',
|
||||
SET_TIME: '{{Ustaw zegar|Ustawianie zegara|}}',
|
||||
SET_TIME_TEXT: 'Wprowadź aktualną datę i godzinę',
|
||||
LOCAL_TIME: 'Czas lokalny',
|
||||
UTC_TIME: 'Czas UTC',
|
||||
ENABLE_NTP: 'Aktywuj NTP (data i godzina będą automatycznie synchronizowane z poniższym serwerem czasu)',
|
||||
NTP_SERVER: 'Serwer NTP',
|
||||
TIME_ZONE: 'Strefa czasowa',
|
||||
ACCESS_POINT: '{{Punkt|punktu|}} {{dostępowy|dostępowego|}}',
|
||||
AP_PROVIDE: 'Punkt dostępowy',
|
||||
AP_PROVIDE_TEXT_1: 'zawsze aktywny',
|
||||
AP_PROVIDE_TEXT_2: 'aktywny jeśli brak połączenia z siecią',
|
||||
AP_PROVIDE_TEXT_3: 'nieaktywny',
|
||||
AP_PREFERRED_CHANNEL: 'Preferowany kanał',
|
||||
AP_HIDE_SSID: 'Ukryj SSID',
|
||||
AP_CLIENTS: 'Liczba klientów',
|
||||
AP_MAX_CLIENTS: 'Maksymalna liczba klientów',
|
||||
AP_LOCAL_IP: 'Lokalny adres IP',
|
||||
NETWORK_SCAN: 'Skanowanie sieci WiFi',
|
||||
IDLE: 'bezczynna',
|
||||
LOST: 'zostało utracone.',
|
||||
SCANNING: 'Skanuję',
|
||||
SCAN_AGAIN: 'Skanuj ponownie',
|
||||
NETWORK_SCANNER: 'Skaner sieci WiFi',
|
||||
NETWORK_NO_WIFI: 'Brak sieci WiFi w zasięgu',
|
||||
NETWORK_BLANK_SSID: 'pozostaw puste aby wyłączyć WiFi',
|
||||
TX_POWER: 'Moc nadawania',
|
||||
HOSTNAME: 'Nazwa w sieci',
|
||||
NETWORK_DISABLE_SLEEP: 'Wyłącz tryb usypiania WiFi',
|
||||
NETWORK_LOW_BAND: 'Używaj mniejszej szerokości pasma WiFi (20MHz)',
|
||||
NETWORK_USE_DNS: 'Włącz wsparcie dla mDNS',
|
||||
NETWORK_ENABLE_CORS: 'Włącz wsparcie dla CORS',
|
||||
NETWORK_CORS_ORIGIN: 'CORS origin',
|
||||
NETWORK_ENABLE_IPV6: 'Włącz wsparcie dla IPv6',
|
||||
NETWORK_FIXED_IP: 'Użyj stałego adresu IP',
|
||||
NETWORK_GATEWAY: 'Brama',
|
||||
NETWORK_SUBNET: 'Maska podsieci',
|
||||
NETWORK_DNS: 'Serwery DNS',
|
||||
ADDRESS_OF: 'Adres {0}',
|
||||
ADMIN: 'Użytkownik "administrator".',
|
||||
GUEST: 'Użytkownik "gość".',
|
||||
NEW: 'Nowy',
|
||||
NEW_NAME_OF: 'Nowa nazwa {0}',
|
||||
ENTITY: 'encji',
|
||||
MIN: 'Min.',
|
||||
MAX: 'Maks.'
|
||||
};
|
||||
|
||||
export default pl;
|
||||
309
interface/src/i18n/sv/index.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
import type { Translation } from '../i18n-types';
|
||||
/* prettier-ignore */
|
||||
/* eslint-disable */
|
||||
|
||||
const sv: Translation = {
|
||||
LANGUAGE: 'Språk',
|
||||
RETRY: 'Försök igen',
|
||||
LOADING: 'Laddar',
|
||||
IS_REQUIRED: '{0} Krävs',
|
||||
SIGN_IN: 'Logga In',
|
||||
SIGN_OUT: 'Logga Ut',
|
||||
USERNAME: 'Användarnamn',
|
||||
PASSWORD: 'Lösenord',
|
||||
SU_PASSWORD: 'su Lösenord',
|
||||
DASHBOARD: 'Kontrollpanel',
|
||||
SETTINGS_OF: '{0} Inställningar',
|
||||
SAVED: 'Sparat',
|
||||
HELP_OF: '{0} Hjälp',
|
||||
LOGGED_IN: 'Inloggad som {name}',
|
||||
PLEASE_SIGNIN: 'Vänligen logga in för att fortsätta',
|
||||
UPLOAD_SUCCESSFUL: 'Uppladdning lyckades',
|
||||
DOWNLOAD_SUCCESSFUL: 'Nedladdning lyckades',
|
||||
INVALID_LOGIN: 'Ogiltig login',
|
||||
NETWORK: 'Nätverk',
|
||||
SECURITY: 'Säkerhet',
|
||||
ONOFF_CAP: 'PÅ/AV',
|
||||
ONOFF: 'på/av',
|
||||
TYPE: 'Typ',
|
||||
DESCRIPTION: 'Beskrivning',
|
||||
ENTITIES: 'Entiteter',
|
||||
REFRESH: 'Uppdatera',
|
||||
EXPORT: 'Exportera',
|
||||
DEVICE_DETAILS: 'Enhetsdetaljer',
|
||||
ID_OF: '{0}-ID',
|
||||
DEVICE: 'Enhets',
|
||||
PRODUCT: 'Produkt',
|
||||
VERSION: 'Version',
|
||||
BRAND: 'Fabrikat',
|
||||
ENTITY_NAME: 'Entitetsnamn',
|
||||
VALUE: '{{Värde|värde}}',
|
||||
SHOW_FAV: 'Visa enbart favoriter',
|
||||
DEVICE_SENSOR_DATA: 'Enhets och Sensor-data',
|
||||
DEVICES_SENSORS: 'Enheter & Sensorer',
|
||||
ATTACHED_SENSORS: 'Anslutna EMS-ESP Sensorer',
|
||||
RUN_COMMAND: 'Kör Kommando',
|
||||
CHANGE_VALUE: 'Ändra Värde',
|
||||
CANCEL: 'Avbryt',
|
||||
RESET: 'Nollställ',
|
||||
SEND: 'Skicka',
|
||||
SAVE: 'Spara',
|
||||
REMOVE: 'Ta bort',
|
||||
PROBLEM_UPDATING: 'Problem vid uppdatering',
|
||||
PROBLEM_LOADING: 'Problem vid hämtning',
|
||||
ACCESS_DENIED: 'Åtkomst Nekad',
|
||||
ANALOG_SENSOR: 'Analog Sensor',
|
||||
ANALOG_SENSORS: 'Analoga Sensorer',
|
||||
UPDATED_OF: '{0} Uppdaterad',
|
||||
UPDATE_OF: '{0} Uppdatera',
|
||||
REMOVED_OF: '{0} Raderad',
|
||||
DELETION_OF: '{0} Radering',
|
||||
OFFSET: 'Kompensering',
|
||||
FACTOR: 'Faktor',
|
||||
FREQ: 'Frekvens',
|
||||
DUTY_CYCLE: 'Duty Cycle',
|
||||
UNIT: 'UoM',
|
||||
STARTVALUE: 'Startvärde',
|
||||
WARN_GPIO: 'Varning: Var försiktig vid aktivering av GPIO!',
|
||||
EDIT: 'Ändra',
|
||||
SENSOR: 'Sensor',
|
||||
TEMP_SENSOR: 'Temperatursensor',
|
||||
TEMP_SENSORS: 'Temperatursensorer',
|
||||
WRITE_CMD_SENT: 'Skrivkommandon skickade',
|
||||
WRITE_CMD_FAILED: 'Skrivkommandon misslyckade',
|
||||
EMS_BUS_WARNING: 'EMS-buss nedkopplad. Om denna varning kvarstår efter några sekunder, kontrollera inställningar och enhets-profil.',
|
||||
EMS_BUS_SCANNING: 'Söker efter EMS-enheter...',
|
||||
CONNECTED: 'Ansluten',
|
||||
TX_ISSUES: 'Sändfel - Prova ett annat TX-läge',
|
||||
DISCONNECTED: 'Nedkopplad',
|
||||
EMS_SCAN: 'Är du säker att du vill initiera en full genomsökning av EMS-bussen?',
|
||||
EMS_BUS_STATUS: 'Status',
|
||||
ACTIVE_DEVICES: 'Aktiva Enheter',
|
||||
EMS_DEVICE: 'EMS Enhet',
|
||||
SUCCESS: 'Lyckades',
|
||||
FAIL: 'Misslyckades',
|
||||
QUALITY: 'Kvalitet',
|
||||
SCAN_DEVICES: 'Sök efter nya enheter',
|
||||
EMS_BUS_STATUS_TITLE: 'EMS-buss & aktivitetsstatus',
|
||||
SCAN: 'Sök',
|
||||
STATUS_NAMES: [
|
||||
'EMS-telegram (Rx)',
|
||||
'EMS-läsningar (Tx)',
|
||||
'EMS-skrivningar (Tx)',
|
||||
'Temperatursensor-läsningar',
|
||||
'Analog Sensor-läsningar',
|
||||
'MQTT-publiceringar',
|
||||
'API-anrop',
|
||||
'Syslog-meddelanden'
|
||||
],
|
||||
NUM_DEVICES: '{num} Enhet{{er}}',
|
||||
NUM_TEMP_SENSORS: '{num} Temperatur-sensor{{er}}',
|
||||
NUM_ANALOG_SENSORS: '{num} Analoga Sensor{{er}}',
|
||||
NUM_DAYS: '{num} dag{{ar}}',
|
||||
NUM_SECONDS: '{num} sekund{{er}}',
|
||||
NUM_HOURS: '{num} timmar',
|
||||
NUM_MINUTES: '{num} minut{{er}}',
|
||||
APPLICATION_SETTINGS: 'Inställningar',
|
||||
CUSTOMIZATION: 'Anpassa',
|
||||
APPLICATION_RESTARTING: 'EMS-ESP startar om',
|
||||
INTERFACE_BOARD_PROFILE: 'Interface Hårdvaruprofil',
|
||||
BOARD_PROFILE_TEXT: 'Välj en förkonfigurerad hårdvaruprofil från listan nedan eller välj Anpassad för att konfigurera dina egna hårdvaruinställningar',
|
||||
BOARD_PROFILE: 'Hårdvarutyp',
|
||||
CUSTOM: 'Anpassa',
|
||||
GPIO_OF: '{0} GPIO',
|
||||
BUTTON: 'Knapp',
|
||||
TEMPERATURE: 'Temperatur',
|
||||
PHY_TYPE: 'Eth PHY-typ',
|
||||
DISABLED: 'inaktiverad',
|
||||
TX_MODE: 'Tx-läge',
|
||||
HARDWARE: 'Hårdvara',
|
||||
EMS_BUS: '{{BUSS|EMS-BUSS}}',
|
||||
GENERAL_OPTIONS: 'Allmänna Inställningar',
|
||||
LANGUAGE_ENTITIES: 'Språk (för entiteter)',
|
||||
HIDE_LED: 'Inaktivera LED',
|
||||
ENABLE_TELNET: 'Aktivera Telnet',
|
||||
ENABLE_ANALOG: 'Aktivera Analoga Sensorer',
|
||||
CONVERT_FAHRENHEIT: 'Konvertera temperaturer till Fahrenheit',
|
||||
BYPASS_TOKEN: 'Inaktivera Token-autensiering för API-anrop',
|
||||
READONLY: 'Aktivera read-only (blockerar alla utgående skrivkommandon mot EMS-bussen)',
|
||||
UNDERCLOCK_CPU: 'Nedklocka Processorhastighet',
|
||||
ENABLE_SHOWER_TIMER: 'Aktivera Dusch-timer',
|
||||
ENABLE_SHOWER_ALERT: 'Aktivera Dusch-varning',
|
||||
TRIGGER_TIME: 'Aktiveringstid',
|
||||
COLD_SHOT_DURATION: 'Längd på kalldusch',
|
||||
FORMATTING_OPTIONS: 'Formatteringsalternativ',
|
||||
BOOLEAN_FORMAT_DASHBOARD: 'Bool-format Kontrollpanel',
|
||||
BOOLEAN_FORMAT_API: 'Bool-format API/MQTT',
|
||||
ENUM_FORMAT: 'Enum-format API/MQTT',
|
||||
INDEX: 'Index',
|
||||
ENABLE_PARASITE: 'Aktivera parasitström',
|
||||
LOGGING: 'Loggning',
|
||||
LOG_HEX: 'Logga EMS-telegram i hexadecimal',
|
||||
ENABLE_SYSLOG: 'Aktivera Syslog',
|
||||
LOG_LEVEL: 'Loggnivå',
|
||||
MARK_INTERVAL: 'Markerings-interval',
|
||||
SECONDS: 'sekunder',
|
||||
MINUTES: 'minuter',
|
||||
HOURS: 'timmar',
|
||||
RESTART: 'Starta om',
|
||||
RESTART_TEXT: 'EMS-ESP kräver en omstart för att applicera förändrade systeminställningar',
|
||||
RESTART_CONFIRM: 'Är du säker på att du vill starta om EMS-ESP?',
|
||||
COMMAND: 'Kommando',
|
||||
CUSTOMIZATIONS_RESTART: 'Alla anpassningr har raderats. Startar om...',
|
||||
CUSTOMIZATIONS_FULL: 'Antal valda enheter för högt. Vänligen spara i mindre antal åt gången.',
|
||||
CUSTOMIZATIONS_SAVED: 'Anpassningar sparade',
|
||||
CUSTOMIZATIONS_HELP_1: 'Välj en enhet och anpassa underenheter med hjälp av alternativen',
|
||||
CUSTOMIZATIONS_HELP_2: 'Favorit',
|
||||
CUSTOMIZATIONS_HELP_3: 'Inaktivera skrivningar',
|
||||
CUSTOMIZATIONS_HELP_4: 'Exkludera från MQTT & API',
|
||||
CUSTOMIZATIONS_HELP_5: 'Göm från Kontrollpanel',
|
||||
CUSTOMIZATIONS_HELP_6: 'remove from memory',
|
||||
SELECT_DEVICE: 'Välj en enhet',
|
||||
SET_ALL: 'ställ in alla',
|
||||
OPTIONS: 'Alternativ',
|
||||
NAME: 'Namn',
|
||||
CUSTOMIZATIONS_RESET: 'Är du säker på att du vill ta bort alla anpassningar inklusive inställningar för Temperatur och Analoga sensorer?',
|
||||
DEVICE_ENTITIES: 'Enhets-entiteter',
|
||||
USER_CUSTOMIZATION: 'Användaranpassningar',
|
||||
SUPPORT_INFORMATION: 'Supportinformation',
|
||||
CLICK_HERE: 'Klicka Här',
|
||||
HELP_INFORMATION_1: 'Besök Wikin för instruktioner för hur du kan konfigurera EMS-ESP',
|
||||
HELP_INFORMATION_2: 'För community-support besök vår Discord-server',
|
||||
HELP_INFORMATION_3: 'Önska en ny funktion eller rapportera en bugg',
|
||||
HELP_INFORMATION_4: 'Bifoga din systeminformation för snabbare hantering när du rapporterar ett problem',
|
||||
HELP_INFORMATION_5: 'EMS-ESP är gratis och är öppen källkod. Bidra till utvecklingen genom att ge oss en stjärna på GitHub!',
|
||||
SUPPORT_INFO: 'Supportinfo',
|
||||
UPLOAD_OF: '{0} Uppladdning',
|
||||
UPLOAD: 'Uppladdning',
|
||||
DOWNLOAD: 'Nedladdning',
|
||||
ABORTED: 'Avbruten',
|
||||
FAILED: 'Misslyckades',
|
||||
SUCCESSFUL: 'Lyckades',
|
||||
SYSTEM: 'System',
|
||||
LOG_OF: '{0} Logg',
|
||||
STATUS_OF: '{0} Status',
|
||||
UPLOAD_DOWNLOAD: 'Upp/Nedladdning',
|
||||
SYSTEM_VERSION_RUNNING: 'Du använder version',
|
||||
SYSTEM_APPLY_FIRMWARE: 'för att aktivera ny firmware',
|
||||
CLOSE: 'Stäng',
|
||||
USE: 'Använd',
|
||||
FACTORY_RESET: 'Fabriksåterställning',
|
||||
SYSTEM_FACTORY_TEXT: 'Enheten har blivit fabriksåterställd och startar nu om',
|
||||
SYSTEM_FACTORY_TEXT_DIALOG: 'Är du säker att du vill fabriksåterställa enheten?',
|
||||
VERSION_CHECK: 'Senaste versioner',
|
||||
THE_LATEST: 'Den senaste',
|
||||
OFFICIAL: 'officiell',
|
||||
DEVELOPMENT: 'utveckling',
|
||||
VERSION_IS: 'version är',
|
||||
RELEASE_NOTES: 'release-logg',
|
||||
EMS_ESP_VER: 'EMS-ESP Version',
|
||||
PLATFORM: 'Enhet (Plattform / SDK)',
|
||||
UPTIME: 'Systemets Upptid',
|
||||
CPU_FREQ: 'CPU-frekvens',
|
||||
HEAP: 'Heap (Ledigt / Max allokerat)',
|
||||
PSRAM: 'PSRAM (Storlek / Ledigt)',
|
||||
FLASH: 'Flashminne (Storlek / Hastighet)',
|
||||
APPSIZE: 'Applikationer (Använt / Ledigt)',
|
||||
FILESYSTEM: 'Filsystem (Använt / Ledigt)',
|
||||
BUFFER_SIZE: 'Max Bufferstorlek',
|
||||
COMPACT: 'Komprimera',
|
||||
ENABLE_OTA: 'Aktivera OTA-uppdateringar',
|
||||
DOWNLOAD_CUSTOMIZATION_TEXT: 'Ladda ner entitetsanpassningar',
|
||||
DOWNLOAD_SETTINGS_TEXT: 'Ladda ner applikationsinställningar. Var försiktig om du delar dina iställlningar då de innehåller lösenord och annan känslig systeminformation',
|
||||
UPLOAD_TEXT: 'Ladda upp ett nytt firmware (.bin), inställningar eller anpassningar (.json) nedan',
|
||||
UPLOADING: 'Laddar upp',
|
||||
UPLOAD_DROP_TEXT: 'Släpp fil eller klicka här',
|
||||
ERROR: 'Okänt Fel, var god försök igen',
|
||||
TIME_SET: 'Ställ in tid',
|
||||
MANAGE_USERS: 'Användare',
|
||||
IS_ADMIN: 'Admin',
|
||||
USER_WARNING: 'Du måste ha minst en admin konfigurerad',
|
||||
ADD: 'Lägg till',
|
||||
ACCESS_TOKEN_FOR: 'Access Token för',
|
||||
ACCESS_TOKEN_TEXT: 'Nedan Token används med REST API-anrop som kräver auktorisering. Den kan skickas med antingen som en Bearer token i Authorization-headern eller i access_token URL query-parametern.',
|
||||
GENERATING_TOKEN: 'Genererar token',
|
||||
USER: 'Användare',
|
||||
MODIFY: 'Ändra',
|
||||
SU_TEXT: 'SU-användarens (super user) lösenord används för att signera autensierings-tokens samt för att aktivera administratörsprivilegier i Console-läge',
|
||||
NOT_ENABLED: 'Ej aktiv',
|
||||
ERRORS_OF: '{0} fel',
|
||||
DISCONNECT_REASON: 'Anledning till nedkoppling',
|
||||
ENABLE_MQTT: 'Aktivera MQTT',
|
||||
BROKER: 'Broker',
|
||||
CLIENT: 'Client',
|
||||
BASE_TOPIC: 'Base',
|
||||
OPTIONAL: 'Valfritt',
|
||||
FORMATTING: 'Formatering',
|
||||
MQTT_FORMAT: 'Topic/Payload Format',
|
||||
MQTT_NEST_1: 'Nestlat i en topic.',
|
||||
MQTT_NEST_2: 'Som individuella topics',
|
||||
MQTT_RESPONSE: 'Publish-kommando som ett `response` topic',
|
||||
MQTT_PUBLISH_TEXT_1: 'Publicera single value topics vid värdeförändring',
|
||||
MQTT_PUBLISH_TEXT_2: 'Publicera till kommando-topics (ioBroker)',
|
||||
MQTT_PUBLISH_TEXT_3: 'Aktivera MQTT Discovery (Home Assistant, Domoticz)',
|
||||
MQTT_PUBLISH_TEXT_4: 'Prefix för Discovery topics',
|
||||
MQTT_PUBLISH_INTERVALS: 'Publiceringsintervall',
|
||||
MQTT_INT_BOILER: 'Värmepump/panna',
|
||||
MQTT_INT_THERMOSTATS: 'Termostater',
|
||||
MQTT_INT_SOLAR: 'Solpaneler',
|
||||
MQTT_INT_MIXER: 'Blandningsventiler',
|
||||
MQTT_INT_HEARTBEAT: 'Heartbeat',
|
||||
MQTT_QUEUE: 'MQTT-kö',
|
||||
DEFAULT: 'Standard',
|
||||
MQTT_ENTITY_FORMAT: 'Entitets-ID format',
|
||||
MQTT_ENTITY_FORMAT_0: 'Singel-instans, långt namn(v3.4)',
|
||||
MQTT_ENTITY_FORMAT_1: 'Singel-instans, kort name',
|
||||
MQTT_ENTITY_FORMAT_2: 'Multi-instans, kort name',
|
||||
MQTT_CLEAN_SESSION: 'Använd "Clean Session"-flaggan',
|
||||
MQTT_RETAIN_FLAG: 'Använd "Always Retain"-flaggan',
|
||||
INACTIVE: 'Inaktiv',
|
||||
ACTIVE: 'Aktiv',
|
||||
UNKNOWN: 'Okänt',
|
||||
SET_TIME: 'Ställ in klockan',
|
||||
SET_TIME_TEXT: 'Ange lokal datum och tid nedan för att ställa in klockan',
|
||||
LOCAL_TIME: 'Tid (lokal)',
|
||||
UTC_TIME: 'Tid (UTC)',
|
||||
ENABLE_NTP: 'Aktivera NTP',
|
||||
NTP_SERVER: 'NTP-server',
|
||||
TIME_ZONE: 'Tidszon',
|
||||
ACCESS_POINT: 'Accesspunkt',
|
||||
AP_PROVIDE: 'Aktivera Accesspunkt',
|
||||
AP_PROVIDE_TEXT_1: 'alltid',
|
||||
AP_PROVIDE_TEXT_2: 'när WiFi är nedkopplat',
|
||||
AP_PROVIDE_TEXT_3: 'aldrig',
|
||||
AP_PREFERRED_CHANNEL: 'Kanal',
|
||||
AP_HIDE_SSID: 'Göm SSID',
|
||||
AP_CLIENTS: 'AP-klienter',
|
||||
AP_MAX_CLIENTS: 'Max Klienter',
|
||||
AP_LOCAL_IP: 'Lokalt IP',
|
||||
NETWORK_SCAN: 'Sök efter WiFi-nätverk',
|
||||
IDLE: 'Vilande',
|
||||
LOST: 'Förlorad',
|
||||
SCANNING: 'Söker',
|
||||
SCAN_AGAIN: 'Sök igen',
|
||||
NETWORK_SCANNER: 'Hittade nätverk',
|
||||
NETWORK_NO_WIFI: 'Inga WiFi-nätverk hittades',
|
||||
NETWORK_BLANK_SSID: 'lämna blankt för att inaktivera WiFi',
|
||||
TX_POWER: 'Tx Effekt',
|
||||
HOSTNAME: 'Värdnamn',
|
||||
NETWORK_DISABLE_SLEEP: 'Inaktivera sömnläge',
|
||||
NETWORK_LOW_BAND: 'Använd lägre bandbredd',
|
||||
NETWORK_USE_DNS: 'Aktivera mDNS-tjänsten',
|
||||
NETWORK_ENABLE_CORS: 'Aktivera CORS',
|
||||
NETWORK_CORS_ORIGIN: 'CORS origin',
|
||||
NETWORK_ENABLE_IPV6: 'Aktivera IPv6-support',
|
||||
NETWORK_FIXED_IP: 'Använd statiskt IP',
|
||||
NETWORK_GATEWAY: 'Gateway',
|
||||
NETWORK_SUBNET: 'Subnätmask',
|
||||
NETWORK_DNS: 'DNS-Server',
|
||||
ADDRESS_OF: '{0} Adress',
|
||||
ADMIN: 'Admin',
|
||||
GUEST: 'Gäst',
|
||||
NEW: 'Ny',
|
||||
NEW_NAME_OF: 'Byt namn {0}',
|
||||
ENTITY: 'Entitet',
|
||||
MIN: 'min',
|
||||
MAX: 'max'
|
||||
};
|
||||
|
||||
export default sv;
|
||||
@@ -5,17 +5,21 @@ import { Tab } from '@mui/material';
|
||||
|
||||
import { RouterTabs, useRouterTab, useLayoutTitle } from '../components';
|
||||
|
||||
import { useI18nContext } from '../i18n/i18n-react';
|
||||
|
||||
import DashboardStatus from './DashboardStatus';
|
||||
import DashboardData from './DashboardData';
|
||||
|
||||
const Dashboard: FC = () => {
|
||||
useLayoutTitle('Dashboard');
|
||||
const { routerTab } = useRouterTab();
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
useLayoutTitle(LL.DASHBOARD());
|
||||
|
||||
return (
|
||||
<>
|
||||
<RouterTabs value={routerTab}>
|
||||
<Tab value="data" label="Devices & Sensors" />
|
||||
<Tab value="data" label={LL.DEVICES_SENSORS()} />
|
||||
<Tab value="status" label="Status" />
|
||||
</RouterTabs>
|
||||
<Routes>
|
||||
|
||||
@@ -49,8 +49,6 @@ import DeviceIcon from './DeviceIcon';
|
||||
|
||||
import { IconContext } from 'react-icons';
|
||||
|
||||
import { formatDurationMin, pluralize } from '../utils';
|
||||
|
||||
import { AuthenticatedContext } from '../contexts/authentication';
|
||||
|
||||
import { ButtonRow, ValidatedTextField, SectionContent, MessageBox } from '../components';
|
||||
@@ -74,12 +72,23 @@ import {
|
||||
DeviceEntityMask
|
||||
} from './types';
|
||||
|
||||
import { useI18nContext } from '../i18n/i18n-react';
|
||||
|
||||
const DashboardData: FC = () => {
|
||||
const { me } = useContext(AuthenticatedContext);
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const [coreData, setCoreData] = useState<CoreData>({ connected: true, devices: [], active_sensors: 0, analog_enabled: false });
|
||||
const [coreData, setCoreData] = useState<CoreData>({
|
||||
connected: true,
|
||||
devices: [],
|
||||
s_n: '',
|
||||
active_sensors: 0,
|
||||
analog_enabled: false
|
||||
});
|
||||
|
||||
const [deviceData, setDeviceData] = useState<DeviceData>({ label: '', data: [] });
|
||||
const [sensorData, setSensorData] = useState<SensorData>({ sensors: [], analogs: [] });
|
||||
const [deviceValue, setDeviceValue] = useState<DeviceValue>();
|
||||
@@ -134,7 +143,7 @@ const DashboardData: FC = () => {
|
||||
common_theme,
|
||||
{
|
||||
Table: `
|
||||
--data-table-library_grid-template-columns: 40px 100px repeat(1, minmax(0, 1fr)) 80px 40px;
|
||||
--data-table-library_grid-template-columns: 40px 160px repeat(1, minmax(0, 1fr)) 100px 40px;
|
||||
`,
|
||||
BaseRow: `
|
||||
.td {
|
||||
@@ -162,7 +171,7 @@ const DashboardData: FC = () => {
|
||||
common_theme,
|
||||
{
|
||||
Table: `
|
||||
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) 140px 40px;
|
||||
--data-table-library_grid-template-columns: minmax(0, 1fr) 35% 40px;
|
||||
`,
|
||||
BaseRow: `
|
||||
.td {
|
||||
@@ -319,10 +328,10 @@ const DashboardData: FC = () => {
|
||||
|
||||
const handleDownloadCsv = () => {
|
||||
const columns = [
|
||||
{ accessor: (dv: any) => dv.id.slice(2), name: 'Entity' },
|
||||
{ accessor: (dv: any) => dv.id.slice(2), name: LL.ENTITY_NAME() },
|
||||
{
|
||||
accessor: (dv: any) => (typeof dv.v === 'number' ? new Intl.NumberFormat().format(dv.v) : dv.v),
|
||||
name: 'Value'
|
||||
name: LL.VALUE(0)
|
||||
},
|
||||
{ accessor: (dv: any) => DeviceValueUOM_s[dv.u], name: 'UoM' }
|
||||
];
|
||||
@@ -354,10 +363,10 @@ const DashboardData: FC = () => {
|
||||
const fetchCoreData = useCallback(async () => {
|
||||
try {
|
||||
setCoreData((await EMSESP.readCoreData()).data);
|
||||
} catch (error: unknown) {
|
||||
enqueueSnackbar(extractErrorMessage(error, 'Failed to fetch core data'), { variant: 'error' });
|
||||
} catch (error) {
|
||||
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_LOADING()), { variant: 'error' });
|
||||
}
|
||||
}, [enqueueSnackbar]);
|
||||
}, [enqueueSnackbar, LL]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCoreData();
|
||||
@@ -375,30 +384,50 @@ const DashboardData: FC = () => {
|
||||
const unique_id = parseInt(id);
|
||||
try {
|
||||
setDeviceData((await EMSESP.readDeviceData({ id: unique_id })).data);
|
||||
} catch (error: unknown) {
|
||||
enqueueSnackbar(extractErrorMessage(error, 'Problem fetching device data'), { variant: 'error' });
|
||||
} catch (error) {
|
||||
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_LOADING()), { variant: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
const fetchSensorData = async () => {
|
||||
try {
|
||||
setSensorData((await EMSESP.readSensorData()).data);
|
||||
} catch (error: unknown) {
|
||||
enqueueSnackbar(extractErrorMessage(error, 'Problem fetching sensor data'), { variant: 'error' });
|
||||
} catch (error) {
|
||||
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_LOADING()), { variant: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
const isCmdOnly = (dv: DeviceValue) => dv.v === '' && dv.c;
|
||||
|
||||
const formatDurationMin = (duration_min: number) => {
|
||||
const days = Math.trunc((duration_min * 60000) / 86400000);
|
||||
const hours = Math.trunc((duration_min * 60000) / 3600000) % 24;
|
||||
const minutes = Math.trunc((duration_min * 60000) / 60000) % 60;
|
||||
|
||||
let formatted = '';
|
||||
if (days) {
|
||||
formatted += LL.NUM_DAYS({ num: days }) + ' ';
|
||||
}
|
||||
if (hours) {
|
||||
formatted += LL.NUM_HOURS({ num: hours }) + ' ';
|
||||
}
|
||||
if (minutes) {
|
||||
formatted += LL.NUM_MINUTES({ num: minutes });
|
||||
}
|
||||
return formatted;
|
||||
};
|
||||
|
||||
function formatValue(value: any, uom: number) {
|
||||
if (value === undefined) {
|
||||
return '';
|
||||
}
|
||||
switch (uom) {
|
||||
case DeviceValueUOM.HOURS:
|
||||
return value ? formatDurationMin(value * 60) : '0 hours';
|
||||
return value ? formatDurationMin(value * 60) : LL.NUM_HOURS({ num: 0 });
|
||||
case DeviceValueUOM.MINUTES:
|
||||
return value ? formatDurationMin(value) : '0 minutes';
|
||||
return value ? formatDurationMin(value) : LL.NUM_MINUTES({ num: 0 });
|
||||
case DeviceValueUOM.SECONDS:
|
||||
return LL.NUM_SECONDS({ num: value });
|
||||
case DeviceValueUOM.NONE:
|
||||
if (typeof value === 'number') {
|
||||
return new Intl.NumberFormat().format(value);
|
||||
@@ -414,13 +443,24 @@ const DashboardData: FC = () => {
|
||||
' ' +
|
||||
DeviceValueUOM_s[uom]
|
||||
);
|
||||
case DeviceValueUOM.SECONDS:
|
||||
return pluralize(value, DeviceValueUOM_s[uom]);
|
||||
default:
|
||||
return new Intl.NumberFormat().format(value) + ' ' + DeviceValueUOM_s[uom];
|
||||
}
|
||||
}
|
||||
|
||||
const setUom = (uom: number) => {
|
||||
switch (uom) {
|
||||
case DeviceValueUOM.HOURS:
|
||||
return LL.HOURS();
|
||||
case DeviceValueUOM.MINUTES:
|
||||
return LL.MINUTES();
|
||||
case DeviceValueUOM.SECONDS:
|
||||
return LL.SECONDS();
|
||||
default:
|
||||
return DeviceValueUOM_s[uom];
|
||||
}
|
||||
};
|
||||
|
||||
const sendDeviceValue = async () => {
|
||||
if (deviceValue) {
|
||||
try {
|
||||
@@ -429,15 +469,15 @@ const DashboardData: FC = () => {
|
||||
devicevalue: deviceValue
|
||||
});
|
||||
if (response.status === 204) {
|
||||
enqueueSnackbar('Write command failed', { variant: 'error' });
|
||||
enqueueSnackbar(LL.WRITE_CMD_FAILED(), { variant: 'error' });
|
||||
} else if (response.status === 403) {
|
||||
enqueueSnackbar('Write access denied', { variant: 'error' });
|
||||
enqueueSnackbar(LL.ACCESS_DENIED(), { variant: 'error' });
|
||||
} else {
|
||||
enqueueSnackbar('Write command sent', { variant: 'success' });
|
||||
enqueueSnackbar(LL.WRITE_CMD_SENT(), { variant: 'success' });
|
||||
}
|
||||
setDeviceValue(undefined);
|
||||
} catch (error: unknown) {
|
||||
enqueueSnackbar(extractErrorMessage(error, 'Problem writing value'), { variant: 'error' });
|
||||
} catch (error) {
|
||||
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
|
||||
} finally {
|
||||
refreshData();
|
||||
setDeviceValue(undefined);
|
||||
@@ -449,7 +489,7 @@ const DashboardData: FC = () => {
|
||||
if (deviceValue) {
|
||||
return (
|
||||
<Dialog open={deviceValue !== undefined} onClose={() => setDeviceValue(undefined)}>
|
||||
<DialogTitle>{isCmdOnly(deviceValue) ? 'Run Command' : 'Change Value'}</DialogTitle>
|
||||
<DialogTitle>{isCmdOnly(deviceValue) ? LL.RUN_COMMAND() : LL.CHANGE_VALUE()}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
{deviceValue.l && (
|
||||
<ValidatedTextField
|
||||
@@ -470,7 +510,7 @@ const DashboardData: FC = () => {
|
||||
<ValidatedTextField
|
||||
name="v"
|
||||
label={deviceValue.id.slice(2)}
|
||||
value={deviceValue.u ? numberValue(deviceValue.v) : deviceValue.v}
|
||||
value={typeof deviceValue.v === 'number' ? Math.round(deviceValue.v * 10) / 10 : deviceValue.v}
|
||||
autoFocus
|
||||
multiline={deviceValue.u ? false : true}
|
||||
sx={{ width: '30ch' }}
|
||||
@@ -478,7 +518,7 @@ const DashboardData: FC = () => {
|
||||
onChange={updateValue(setDeviceValue)}
|
||||
inputProps={deviceValue.u ? { min: deviceValue.m, max: deviceValue.x, step: deviceValue.s } : {}}
|
||||
InputProps={{
|
||||
startAdornment: <InputAdornment position="start">{DeviceValueUOM_s[deviceValue.u]}</InputAdornment>
|
||||
startAdornment: <InputAdornment position="start">{setUom(deviceValue.u)}</InputAdornment>
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -491,7 +531,7 @@ const DashboardData: FC = () => {
|
||||
onClick={() => setDeviceValue(undefined)}
|
||||
color="secondary"
|
||||
>
|
||||
Cancel
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<SendIcon />}
|
||||
@@ -500,7 +540,7 @@ const DashboardData: FC = () => {
|
||||
onClick={() => sendDeviceValue()}
|
||||
color="warning"
|
||||
>
|
||||
Send
|
||||
{LL.SEND()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
@@ -521,15 +561,15 @@ const DashboardData: FC = () => {
|
||||
offset: sensor.o
|
||||
});
|
||||
if (response.status === 204) {
|
||||
enqueueSnackbar('Sensor change failed', { variant: 'error' });
|
||||
enqueueSnackbar(LL.UPLOAD_OF(LL.SENSOR()) + ' ' + LL.FAILED(), { variant: 'error' });
|
||||
} else if (response.status === 403) {
|
||||
enqueueSnackbar('Access denied', { variant: 'error' });
|
||||
enqueueSnackbar(LL.ACCESS_DENIED(), { variant: 'error' });
|
||||
} else {
|
||||
enqueueSnackbar('Sensor updated', { variant: 'success' });
|
||||
enqueueSnackbar(LL.UPDATED_OF(LL.SENSOR()), { variant: 'success' });
|
||||
}
|
||||
setSensor(undefined);
|
||||
} catch (error: unknown) {
|
||||
enqueueSnackbar(extractErrorMessage(error, 'Problem updating sensor'), { variant: 'error' });
|
||||
} catch (error) {
|
||||
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
|
||||
} finally {
|
||||
setSensor(undefined);
|
||||
fetchSensorData();
|
||||
@@ -541,16 +581,20 @@ const DashboardData: FC = () => {
|
||||
if (sensor) {
|
||||
return (
|
||||
<Dialog open={sensor !== undefined} onClose={() => setSensor(undefined)}>
|
||||
<DialogTitle>Edit Temperature Sensor</DialogTitle>
|
||||
<DialogTitle>
|
||||
{LL.EDIT()} {LL.TEMP_SENSOR()}
|
||||
</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Box color="warning.main" p={0} pl={0} pr={0} mt={0} mb={2}>
|
||||
<Typography variant="body2">Sensor ID {sensor.id}</Typography>
|
||||
<Typography variant="body2">
|
||||
{LL.ID_OF(LL.SENSOR())}: {sensor.id}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Grid container spacing={1}>
|
||||
<Grid item>
|
||||
<ValidatedTextField
|
||||
name="n"
|
||||
label="Name"
|
||||
label={LL.ENTITY_NAME()}
|
||||
value={sensor.n}
|
||||
autoFocus
|
||||
sx={{ width: '30ch' }}
|
||||
@@ -560,7 +604,7 @@ const DashboardData: FC = () => {
|
||||
<Grid item>
|
||||
<ValidatedTextField
|
||||
name="o"
|
||||
label="Offset"
|
||||
label={LL.OFFSET()}
|
||||
value={numberValue(sensor.o)}
|
||||
sx={{ width: '12ch' }}
|
||||
type="number"
|
||||
@@ -581,7 +625,7 @@ const DashboardData: FC = () => {
|
||||
onClick={() => setSensor(undefined)}
|
||||
color="secondary"
|
||||
>
|
||||
Cancel
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<SaveIcon />}
|
||||
@@ -590,7 +634,7 @@ const DashboardData: FC = () => {
|
||||
onClick={() => sendSensor()}
|
||||
color="warning"
|
||||
>
|
||||
Save
|
||||
{LL.SAVE()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
@@ -602,35 +646,35 @@ const DashboardData: FC = () => {
|
||||
if (coreData && coreData.devices.length > 0 && deviceDialog !== -1) {
|
||||
return (
|
||||
<Dialog open={deviceDialog !== -1} onClose={() => setDeviceDialog(-1)}>
|
||||
<DialogTitle>Device Details</DialogTitle>
|
||||
<DialogTitle>{LL.DEVICE_DETAILS()}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<List dense={true}>
|
||||
<ListItem>
|
||||
<ListItemText primary="Type" secondary={coreData.devices[deviceDialog].t} />
|
||||
<ListItemText primary={LL.TYPE()} secondary={coreData.devices[deviceDialog].t} />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText primary="Name" secondary={coreData.devices[deviceDialog].n} />
|
||||
<ListItemText primary={LL.NAME(0)} secondary={coreData.devices[deviceDialog].n} />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText primary="Brand" secondary={coreData.devices[deviceDialog].b} />
|
||||
<ListItemText primary={LL.BRAND()} secondary={coreData.devices[deviceDialog].b} />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary="Device ID"
|
||||
primary={LL.ID_OF(LL.DEVICE())}
|
||||
secondary={'0x' + ('00' + coreData.devices[deviceDialog].d.toString(16).toUpperCase()).slice(-2)}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText primary="Product ID" secondary={coreData.devices[deviceDialog].p} />
|
||||
<ListItemText primary={LL.ID_OF(LL.PRODUCT())} secondary={coreData.devices[deviceDialog].p} />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText primary="Version" secondary={coreData.devices[deviceDialog].v} />
|
||||
<ListItemText primary={LL.VERSION()} secondary={coreData.devices[deviceDialog].v} />
|
||||
</ListItem>
|
||||
</List>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="outlined" onClick={() => setDeviceDialog(-1)} color="secondary">
|
||||
Close
|
||||
{LL.CLOSE()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
@@ -640,17 +684,20 @@ const DashboardData: FC = () => {
|
||||
|
||||
const renderCoreData = () => (
|
||||
<IconContext.Provider value={{ color: 'lightblue', size: '24', style: { verticalAlign: 'middle' } }}>
|
||||
{!coreData.connected && <MessageBox my={2} level="error" message="EMSbus disconnected, check settings and board profile" />}
|
||||
{coreData.connected && coreData.devices.length === 0 && <MessageBox my={2} level="warning" message="Scanning for EMS devices..." />}
|
||||
{!coreData.connected && <MessageBox my={2} level="error" message={LL.EMS_BUS_WARNING()} />}
|
||||
{coreData.connected && coreData.devices.length === 0 && (
|
||||
<MessageBox my={2} level="warning" message={LL.EMS_BUS_SCANNING()} />
|
||||
)}
|
||||
|
||||
<Table data={{ nodes: coreData.devices }} select={device_select} theme={device_theme} layout={{ custom: true }}>
|
||||
{(tableList: any) => (
|
||||
<>
|
||||
<Header>
|
||||
<HeaderRow>
|
||||
<HeaderCell stiff />
|
||||
<HeaderCell stiff>TYPE</HeaderCell>
|
||||
<HeaderCell resize>DESCRIPTION</HeaderCell>
|
||||
<HeaderCell stiff>ENTITIES</HeaderCell>
|
||||
<HeaderCell stiff>{LL.TYPE()}</HeaderCell>
|
||||
<HeaderCell resize>{LL.DESCRIPTION()}</HeaderCell>
|
||||
<HeaderCell stiff>{LL.ENTITIES()}</HeaderCell>
|
||||
<HeaderCell stiff />
|
||||
</HeaderRow>
|
||||
</Header>
|
||||
@@ -658,9 +705,9 @@ const DashboardData: FC = () => {
|
||||
{tableList.map((device: Device, index: number) => (
|
||||
<Row key={device.id} item={device}>
|
||||
<Cell stiff>
|
||||
<DeviceIcon type={device.t} />
|
||||
<DeviceIcon type_id={device.t} />
|
||||
</Cell>
|
||||
<Cell stiff>{device.t}</Cell>
|
||||
<Cell stiff>{device.tn}</Cell>
|
||||
<Cell>{device.n}</Cell>
|
||||
<Cell stiff>{device.e}</Cell>
|
||||
<Cell stiff>
|
||||
@@ -673,10 +720,10 @@ const DashboardData: FC = () => {
|
||||
{(coreData.active_sensors > 0 || coreData.analog_enabled) && (
|
||||
<Row key="sensor" item={{ id: 'sensor' }}>
|
||||
<Cell>
|
||||
<DeviceIcon type="Sensor" />
|
||||
<DeviceIcon type_id={1} />
|
||||
</Cell>
|
||||
<Cell>Sensors</Cell>
|
||||
<Cell>Attached EMS-ESP Sensors</Cell>
|
||||
<Cell>{coreData.s_n}</Cell>
|
||||
<Cell>{LL.ATTACHED_SENSORS()}</Cell>
|
||||
<Cell>{coreData.active_sensors}</Cell>
|
||||
<Cell>
|
||||
<IconButton size="small" onClick={() => addAnalogSensor()}>
|
||||
@@ -723,7 +770,7 @@ const DashboardData: FC = () => {
|
||||
control={<Checkbox size="small" name="onlyFav" checked={onlyFav} onChange={() => setOnlyFav(!onlyFav)} />}
|
||||
label={
|
||||
<span style={{ fontSize: '12px' }}>
|
||||
only show favorites
|
||||
{LL.SHOW_FAV()}
|
||||
<StarIcon color="primary" sx={{ fontSize: 12 }} />
|
||||
</span>
|
||||
}
|
||||
@@ -749,7 +796,7 @@ const DashboardData: FC = () => {
|
||||
endIcon={getSortIcon(dv_sort.state, 'NAME')}
|
||||
onClick={() => dv_sort.fns.onToggleSort({ sortKey: 'NAME' })}
|
||||
>
|
||||
ENTITY NAME
|
||||
{LL.ENTITY_NAME()}
|
||||
</Button>
|
||||
</HeaderCell>
|
||||
<HeaderCell resize>
|
||||
@@ -759,7 +806,7 @@ const DashboardData: FC = () => {
|
||||
endIcon={getSortIcon(dv_sort.state, 'VALUE')}
|
||||
onClick={() => dv_sort.fns.onToggleSort({ sortKey: 'VALUE' })}
|
||||
>
|
||||
VALUE
|
||||
{LL.VALUE(0)}
|
||||
</Button>
|
||||
</HeaderCell>
|
||||
<HeaderCell stiff />
|
||||
@@ -806,7 +853,7 @@ const DashboardData: FC = () => {
|
||||
const renderDallasData = () => (
|
||||
<>
|
||||
<Typography sx={{ pt: 2, pb: 1 }} variant="h6" color="secondary">
|
||||
Temperature Sensors
|
||||
{LL.TEMP_SENSORS()}
|
||||
</Typography>
|
||||
<Table
|
||||
data={{ nodes: sensorData.sensors }}
|
||||
@@ -825,7 +872,7 @@ const DashboardData: FC = () => {
|
||||
endIcon={getSortIcon(sensor_sort.state, 'NAME')}
|
||||
onClick={() => sensor_sort.fns.onToggleSort({ sortKey: 'NAME' })}
|
||||
>
|
||||
NAME
|
||||
{LL.ENTITY_NAME()}
|
||||
</Button>
|
||||
</HeaderCell>
|
||||
<HeaderCell stiff>
|
||||
@@ -835,7 +882,7 @@ const DashboardData: FC = () => {
|
||||
endIcon={getSortIcon(sensor_sort.state, 'TEMPERATURE')}
|
||||
onClick={() => sensor_sort.fns.onToggleSort({ sortKey: 'TEMPERATURE' })}
|
||||
>
|
||||
TEMPERATURE
|
||||
{LL.VALUE(0)}
|
||||
</Button>
|
||||
</HeaderCell>
|
||||
<HeaderCell stiff />
|
||||
@@ -865,7 +912,7 @@ const DashboardData: FC = () => {
|
||||
const renderAnalogData = () => (
|
||||
<>
|
||||
<Typography sx={{ pt: 2, pb: 1 }} variant="h6" color="secondary">
|
||||
Analog Sensors
|
||||
{LL.ANALOG_SENSORS()}
|
||||
</Typography>
|
||||
|
||||
<Table data={{ nodes: sensorData.analogs }} theme={analog_theme} sort={analog_sort} layout={{ custom: true }}>
|
||||
@@ -890,7 +937,7 @@ const DashboardData: FC = () => {
|
||||
endIcon={getSortIcon(analog_sort.state, 'NAME')}
|
||||
onClick={() => analog_sort.fns.onToggleSort({ sortKey: 'NAME' })}
|
||||
>
|
||||
NAME
|
||||
{LL.ENTITY_NAME()}
|
||||
</Button>
|
||||
</HeaderCell>
|
||||
<HeaderCell stiff>
|
||||
@@ -900,10 +947,10 @@ const DashboardData: FC = () => {
|
||||
endIcon={getSortIcon(analog_sort.state, 'TYPE')}
|
||||
onClick={() => analog_sort.fns.onToggleSort({ sortKey: 'TYPE' })}
|
||||
>
|
||||
TYPE
|
||||
{LL.TYPE()}
|
||||
</Button>
|
||||
</HeaderCell>
|
||||
<HeaderCell stiff>VALUE</HeaderCell>
|
||||
<HeaderCell stiff>{LL.VALUE(0)}</HeaderCell>
|
||||
<HeaderCell stiff />
|
||||
</HeaderRow>
|
||||
</Header>
|
||||
@@ -943,14 +990,14 @@ const DashboardData: FC = () => {
|
||||
});
|
||||
|
||||
if (response.status === 204) {
|
||||
enqueueSnackbar('Analog deletion failed', { variant: 'error' });
|
||||
enqueueSnackbar(LL.DELETION_OF(LL.ANALOG_SENSOR()) + ' ' + LL.FAILED(), { variant: 'error' });
|
||||
} else if (response.status === 403) {
|
||||
enqueueSnackbar('Access denied', { variant: 'error' });
|
||||
enqueueSnackbar(LL.ACCESS_DENIED(), { variant: 'error' });
|
||||
} else {
|
||||
enqueueSnackbar('Analog sensor removed', { variant: 'success' });
|
||||
enqueueSnackbar(LL.REMOVED_OF(LL.ANALOG_SENSOR()), { variant: 'success' });
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
enqueueSnackbar(extractErrorMessage(error, 'Problem updating analog sensor'), { variant: 'error' });
|
||||
} catch (error) {
|
||||
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
|
||||
} finally {
|
||||
setAnalog(undefined);
|
||||
fetchSensorData();
|
||||
@@ -971,14 +1018,14 @@ const DashboardData: FC = () => {
|
||||
});
|
||||
|
||||
if (response.status === 204) {
|
||||
enqueueSnackbar('Analog sensor update failed', { variant: 'error' });
|
||||
enqueueSnackbar(LL.UPDATE_OF(LL.ANALOG_SENSOR()) + ' ' + LL.FAILED(), { variant: 'error' });
|
||||
} else if (response.status === 403) {
|
||||
enqueueSnackbar('Access denied', { variant: 'error' });
|
||||
enqueueSnackbar(LL.ACCESS_DENIED(), { variant: 'error' });
|
||||
} else {
|
||||
enqueueSnackbar('Analog sensor updated', { variant: 'success' });
|
||||
enqueueSnackbar(LL.UPDATED_OF(LL.ANALOG_SENSOR()), { variant: 'success' });
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
enqueueSnackbar(extractErrorMessage(error, 'Problem updating analog'), { variant: 'error' });
|
||||
} catch (error) {
|
||||
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
|
||||
} finally {
|
||||
setAnalog(undefined);
|
||||
fetchSensorData();
|
||||
@@ -990,32 +1037,42 @@ const DashboardData: FC = () => {
|
||||
if (analog) {
|
||||
return (
|
||||
<Dialog open={analog !== undefined} onClose={() => setAnalog(undefined)}>
|
||||
<DialogTitle>Edit Analog Sensor</DialogTitle>
|
||||
<DialogTitle>
|
||||
{LL.EDIT()} {LL.ANALOG_SENSOR()}
|
||||
</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item>
|
||||
<Grid item xs={12}>
|
||||
<ValidatedTextField
|
||||
name="n"
|
||||
label={LL.ENTITY_NAME()}
|
||||
value={analog.n}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={updateValue(setAnalog)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<ValidatedTextField
|
||||
name="g"
|
||||
label="GPIO"
|
||||
value={analog.g}
|
||||
fullWidth
|
||||
type="number"
|
||||
variant="outlined"
|
||||
autoFocus
|
||||
onChange={updateValue(setAnalog)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Grid item xs={8}>
|
||||
<ValidatedTextField
|
||||
name="n"
|
||||
label="Name"
|
||||
value={analog.n}
|
||||
sx={{ width: '20ch' }}
|
||||
variant="outlined"
|
||||
name="t"
|
||||
label={LL.TYPE()}
|
||||
value={analog.t}
|
||||
fullWidth
|
||||
select
|
||||
onChange={updateValue(setAnalog)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<ValidatedTextField name="t" label="Type" value={analog.t} select onChange={updateValue(setAnalog)}>
|
||||
>
|
||||
{AnalogTypeNames.map((val, i) => (
|
||||
<MenuItem key={i} value={i}>
|
||||
{val}
|
||||
@@ -1025,8 +1082,15 @@ const DashboardData: FC = () => {
|
||||
</Grid>
|
||||
{analog.t >= AnalogType.COUNTER && analog.t <= AnalogType.RATE && (
|
||||
<>
|
||||
<Grid item>
|
||||
<ValidatedTextField name="u" label="UoM" value={analog.u} select onChange={updateValue(setAnalog)}>
|
||||
<Grid item xs={4}>
|
||||
<ValidatedTextField
|
||||
name="u"
|
||||
label={LL.UNIT()}
|
||||
value={analog.u}
|
||||
fullWidth
|
||||
select
|
||||
onChange={updateValue(setAnalog)}
|
||||
>
|
||||
{DeviceValueUOM_s.map((val, i) => (
|
||||
<MenuItem key={i} value={i}>
|
||||
{val}
|
||||
@@ -1035,12 +1099,12 @@ const DashboardData: FC = () => {
|
||||
</ValidatedTextField>
|
||||
</Grid>
|
||||
{analog.t === AnalogType.ADC && (
|
||||
<Grid item>
|
||||
<Grid item xs={4}>
|
||||
<ValidatedTextField
|
||||
name="o"
|
||||
label="Offset"
|
||||
label={LL.OFFSET()}
|
||||
value={numberValue(analog.o)}
|
||||
sx={{ width: '20ch' }}
|
||||
fullWidth
|
||||
type="number"
|
||||
variant="outlined"
|
||||
onChange={updateValue(setAnalog)}
|
||||
@@ -1052,41 +1116,41 @@ const DashboardData: FC = () => {
|
||||
</Grid>
|
||||
)}
|
||||
{analog.t === AnalogType.COUNTER && (
|
||||
<Grid item>
|
||||
<Grid item xs={4}>
|
||||
<ValidatedTextField
|
||||
name="o"
|
||||
label="Start Value"
|
||||
label={LL.STARTVALUE()}
|
||||
value={numberValue(analog.o)}
|
||||
sx={{ width: '20ch' }}
|
||||
fullWidth
|
||||
type="number"
|
||||
variant="outlined"
|
||||
onChange={updateValue(setAnalog)}
|
||||
inputProps={{ min: '0', step: '1' }}
|
||||
inputProps={{ step: '0.001' }}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item>
|
||||
<Grid item xs={4}>
|
||||
<ValidatedTextField
|
||||
name="f"
|
||||
label="Factor"
|
||||
label={LL.FACTOR()}
|
||||
value={numberValue(analog.f)}
|
||||
sx={{ width: '20ch' }}
|
||||
fullWidth
|
||||
type="number"
|
||||
variant="outlined"
|
||||
onChange={updateValue(setAnalog)}
|
||||
inputProps={{ min: '-100', max: '100', step: '0.1' }}
|
||||
inputProps={{ step: '0.001' }}
|
||||
/>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
{analog.t === AnalogType.DIGITAL_OUT && (analog.id === '25' || analog.id === '26') && (
|
||||
{analog.t === AnalogType.DIGITAL_OUT && (analog.g === 25 || analog.g === 26) && (
|
||||
<>
|
||||
<Grid item>
|
||||
<Grid item xs={4}>
|
||||
<ValidatedTextField
|
||||
name="o"
|
||||
label="DAC Value"
|
||||
label={LL.VALUE(0)}
|
||||
value={numberValue(analog.o)}
|
||||
sx={{ width: '20ch' }}
|
||||
fullWidth
|
||||
type="number"
|
||||
variant="outlined"
|
||||
onChange={updateValue(setAnalog)}
|
||||
@@ -1095,14 +1159,14 @@ const DashboardData: FC = () => {
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
{analog.t === AnalogType.DIGITAL_OUT && analog.id !== '25' && analog.id !== '26' && (
|
||||
{analog.t === AnalogType.DIGITAL_OUT && analog.g !== 25 && analog.g !== 26 && (
|
||||
<>
|
||||
<Grid item>
|
||||
<Grid item xs={4}>
|
||||
<ValidatedTextField
|
||||
name="o"
|
||||
label="Value"
|
||||
label={LL.VALUE(0)}
|
||||
value={numberValue(analog.o)}
|
||||
sx={{ width: '20ch' }}
|
||||
fullWidth
|
||||
type="number"
|
||||
variant="outlined"
|
||||
onChange={updateValue(setAnalog)}
|
||||
@@ -1113,12 +1177,12 @@ const DashboardData: FC = () => {
|
||||
)}
|
||||
{analog.t >= AnalogType.PWM_0 && (
|
||||
<>
|
||||
<Grid item>
|
||||
<Grid item xs={4}>
|
||||
<ValidatedTextField
|
||||
name="f"
|
||||
label="Frequency"
|
||||
label={LL.FREQ()}
|
||||
value={numberValue(analog.f)}
|
||||
sx={{ width: '20ch' }}
|
||||
fullWidth
|
||||
type="number"
|
||||
variant="outlined"
|
||||
onChange={updateValue(setAnalog)}
|
||||
@@ -1128,12 +1192,12 @@ const DashboardData: FC = () => {
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Grid item xs={4}>
|
||||
<ValidatedTextField
|
||||
name="o"
|
||||
label="Dutycycle"
|
||||
label={LL.DUTY_CYCLE()}
|
||||
value={numberValue(analog.o)}
|
||||
sx={{ width: '20ch' }}
|
||||
fullWidth
|
||||
type="number"
|
||||
variant="outlined"
|
||||
onChange={updateValue(setAnalog)}
|
||||
@@ -1147,13 +1211,13 @@ const DashboardData: FC = () => {
|
||||
)}
|
||||
</Grid>
|
||||
<Box color="warning.main" mt={2}>
|
||||
<Typography variant="body2">Warning: be careful when assigning a GPIO!</Typography>
|
||||
<Typography variant="body2">{LL.WARN_GPIO()}</Typography>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Box flexGrow={1} sx={{ '& button': { mt: 0 } }}>
|
||||
<Button startIcon={<RemoveIcon />} variant="outlined" color="error" onClick={() => sendRemoveAnalog()}>
|
||||
Remove
|
||||
{LL.REMOVE()}
|
||||
</Button>
|
||||
</Box>
|
||||
<Button
|
||||
@@ -1162,7 +1226,7 @@ const DashboardData: FC = () => {
|
||||
onClick={() => setAnalog(undefined)}
|
||||
color="secondary"
|
||||
>
|
||||
Cancel
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<SaveIcon />}
|
||||
@@ -1171,7 +1235,7 @@ const DashboardData: FC = () => {
|
||||
onClick={() => sendAnalog()}
|
||||
color="warning"
|
||||
>
|
||||
Save
|
||||
{LL.SAVE()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
@@ -1180,7 +1244,7 @@ const DashboardData: FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title="Device and Sensor Data" titleGutter>
|
||||
<SectionContent title={LL.DEVICE_SENSOR_DATA()} titleGutter>
|
||||
{renderCoreData()}
|
||||
{renderDeviceData()}
|
||||
{renderDeviceDialog()}
|
||||
@@ -1191,11 +1255,11 @@ const DashboardData: FC = () => {
|
||||
{renderAnalogDialog()}
|
||||
<ButtonRow>
|
||||
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={refreshData}>
|
||||
Refresh
|
||||
{LL.REFRESH()}
|
||||
</Button>
|
||||
{device_select.state.id && device_select.state.id !== 'sensor' && (
|
||||
<Button startIcon={<DownloadIcon />} variant="outlined" onClick={handleDownloadCsv}>
|
||||
Export
|
||||
{LL.EXPORT()}
|
||||
</Button>
|
||||
)}
|
||||
</ButtonRow>
|
||||
|
||||
@@ -32,10 +32,13 @@ import { ButtonRow, FormLoader, SectionContent } from '../components';
|
||||
|
||||
import { Status, busConnectionStatus, Stat } from './types';
|
||||
|
||||
import { formatDurationSec, pluralize, extractErrorMessage, useRest } from '../utils';
|
||||
import { extractErrorMessage, useRest } from '../utils';
|
||||
|
||||
import * as EMSESP from './api';
|
||||
|
||||
import type { Translation } from '../i18n/i18n-types';
|
||||
import { useI18nContext } from '../i18n/i18n-react';
|
||||
|
||||
export const isConnected = ({ status }: Status) => status !== busConnectionStatus.BUS_STATUS_OFFLINE;
|
||||
|
||||
const busStatusHighlight = ({ status }: Status, theme: Theme) => {
|
||||
@@ -51,19 +54,6 @@ const busStatusHighlight = ({ status }: Status, theme: Theme) => {
|
||||
}
|
||||
};
|
||||
|
||||
const busStatus = ({ status }: Status) => {
|
||||
switch (status) {
|
||||
case busConnectionStatus.BUS_STATUS_CONNECTED:
|
||||
return 'Connected';
|
||||
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
|
||||
return 'Tx issues - try a different Tx Mode';
|
||||
case busConnectionStatus.BUS_STATUS_OFFLINE:
|
||||
return 'Disconnected';
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
};
|
||||
|
||||
const showQuality = (stat: Stat) => {
|
||||
if (stat.q === 0 || stat.s + stat.f === 0) {
|
||||
return;
|
||||
@@ -81,12 +71,32 @@ const showQuality = (stat: Stat) => {
|
||||
const DashboardStatus: FC = () => {
|
||||
const { loadData, data, errorMessage } = useRest<Status>({ read: EMSESP.readStatus });
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const theme = useTheme();
|
||||
const [confirmScan, setConfirmScan] = useState<boolean>(false);
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const { me } = useContext(AuthenticatedContext);
|
||||
|
||||
const showName = (id: any) => {
|
||||
let name: keyof Translation['STATUS_NAMES'] = id;
|
||||
return LL.STATUS_NAMES[name]();
|
||||
};
|
||||
|
||||
const busStatus = ({ status }: Status) => {
|
||||
switch (status) {
|
||||
case busConnectionStatus.BUS_STATUS_CONNECTED:
|
||||
return LL.CONNECTED(0);
|
||||
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
|
||||
return LL.TX_ISSUES();
|
||||
case busConnectionStatus.BUS_STATUS_OFFLINE:
|
||||
return LL.DISCONNECTED();
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
};
|
||||
|
||||
const stats_theme = tableTheme({
|
||||
Table: `
|
||||
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) 90px 90px 80px;
|
||||
@@ -137,24 +147,44 @@ const DashboardStatus: FC = () => {
|
||||
const scan = async () => {
|
||||
try {
|
||||
await EMSESP.scanDevices();
|
||||
enqueueSnackbar('Scanning for devices...', { variant: 'info' });
|
||||
} catch (error: unknown) {
|
||||
enqueueSnackbar(extractErrorMessage(error, 'Problem initiating scan'), { variant: 'error' });
|
||||
enqueueSnackbar(LL.SCANNING() + '...', { variant: 'info' });
|
||||
} catch (error) {
|
||||
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
|
||||
} finally {
|
||||
setConfirmScan(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatDurationSec = (duration_sec: number) => {
|
||||
const days = Math.trunc((duration_sec * 1000) / 86400000);
|
||||
const hours = Math.trunc((duration_sec * 1000) / 3600000) % 24;
|
||||
const minutes = Math.trunc((duration_sec * 1000) / 60000) % 60;
|
||||
const seconds = Math.trunc((duration_sec * 1000) / 1000) % 60;
|
||||
|
||||
let formatted = '';
|
||||
if (days) {
|
||||
formatted += LL.NUM_DAYS({ num: days }) + ' ';
|
||||
}
|
||||
if (hours) {
|
||||
formatted += LL.NUM_HOURS({ num: hours }) + ' ';
|
||||
}
|
||||
if (minutes) {
|
||||
formatted += LL.NUM_MINUTES({ num: minutes }) + ' ';
|
||||
}
|
||||
formatted += LL.NUM_SECONDS({ num: seconds });
|
||||
return formatted;
|
||||
};
|
||||
|
||||
const renderScanDialog = () => (
|
||||
<Dialog open={confirmScan} onClose={() => setConfirmScan(false)}>
|
||||
<DialogTitle>EMS Device Scan</DialogTitle>
|
||||
<DialogContent dividers>Are you sure you want to initiate a full device scan of the EMS bus?</DialogContent>
|
||||
<DialogTitle>{LL.SCAN_DEVICES()}</DialogTitle>
|
||||
<DialogContent dividers>{LL.EMS_SCAN()}</DialogContent>
|
||||
<DialogActions>
|
||||
<Button startIcon={<CancelIcon />} variant="outlined" onClick={() => setConfirmScan(false)} color="secondary">
|
||||
Cancel
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button startIcon={<PermScanWifiIcon />} variant="outlined" onClick={scan} color="primary" autoFocus>
|
||||
Scan
|
||||
{LL.SCAN()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
@@ -174,7 +204,10 @@ const DashboardStatus: FC = () => {
|
||||
<DirectionsBusIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="EMS Bus Status" secondary={busStatus(data) + formatDurationSec(data.uptime)} />
|
||||
<ListItemText
|
||||
primary={LL.EMS_BUS_STATUS()}
|
||||
secondary={busStatus(data) + ' (' + formatDurationSec(data.uptime) + ')'}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
@@ -183,13 +216,13 @@ const DashboardStatus: FC = () => {
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary="Active Devices & Sensors"
|
||||
primary={LL.ACTIVE_DEVICES()}
|
||||
secondary={
|
||||
pluralize(data.num_devices, 'EMS Device') +
|
||||
LL.NUM_DEVICES({ num: data.num_devices }) +
|
||||
', ' +
|
||||
pluralize(data.num_sensors, 'Temperature Sensor') +
|
||||
LL.NUM_TEMP_SENSORS({ num: data.num_sensors }) +
|
||||
', ' +
|
||||
pluralize(data.num_analogs, 'Analog Sensor')
|
||||
LL.NUM_ANALOG_SENSORS({ num: data.num_analogs })
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
@@ -200,15 +233,15 @@ const DashboardStatus: FC = () => {
|
||||
<Header>
|
||||
<HeaderRow>
|
||||
<HeaderCell resize></HeaderCell>
|
||||
<HeaderCell stiff>SUCCESS</HeaderCell>
|
||||
<HeaderCell stiff>FAIL</HeaderCell>
|
||||
<HeaderCell stiff>QUALITY</HeaderCell>
|
||||
<HeaderCell stiff>{LL.SUCCESS()}</HeaderCell>
|
||||
<HeaderCell stiff>{LL.FAIL()}</HeaderCell>
|
||||
<HeaderCell stiff>{LL.QUALITY()}</HeaderCell>
|
||||
</HeaderRow>
|
||||
</Header>
|
||||
<Body>
|
||||
{tableList.map((stat: Stat) => (
|
||||
<Row key={stat.id} item={stat}>
|
||||
<Cell>{stat.id}</Cell>
|
||||
<Cell>{showName(stat.id)}</Cell>
|
||||
<Cell stiff>{Intl.NumberFormat().format(stat.s)}</Cell>
|
||||
<Cell stiff>{Intl.NumberFormat().format(stat.f)}</Cell>
|
||||
<Cell stiff>{showQuality(stat)}</Cell>
|
||||
@@ -223,7 +256,7 @@ const DashboardStatus: FC = () => {
|
||||
<Box display="flex" flexWrap="wrap">
|
||||
<Box flexGrow={1} sx={{ '& button': { mt: 2 } }}>
|
||||
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
|
||||
Refresh
|
||||
{LL.REFRESH()}
|
||||
</Button>
|
||||
</Box>
|
||||
<Box flexWrap="nowrap" whiteSpace="nowrap">
|
||||
@@ -235,7 +268,7 @@ const DashboardStatus: FC = () => {
|
||||
disabled={!me.admin}
|
||||
onClick={() => setConfirmScan(true)}
|
||||
>
|
||||
Scan for new devices
|
||||
{LL.SCAN_DEVICES()}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
</Box>
|
||||
@@ -245,7 +278,7 @@ const DashboardStatus: FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title="EMS Bus & Activity Status" titleGutter>
|
||||
<SectionContent title={LL.EMS_BUS_STATUS_TITLE()} titleGutter>
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
|
||||
@@ -9,31 +9,61 @@ import { GiHeatHaze } from 'react-icons/gi';
|
||||
import { TiFlowSwitch } from 'react-icons/ti';
|
||||
import { VscVmConnect } from 'react-icons/vsc';
|
||||
import { AiOutlineGateway } from 'react-icons/ai';
|
||||
import { AiOutlineAlert } from 'react-icons/ai';
|
||||
import { AiOutlineChrome } from 'react-icons/ai';
|
||||
|
||||
interface DeviceIconProps {
|
||||
type: string;
|
||||
type_id: number;
|
||||
}
|
||||
|
||||
const DeviceIcon: FC<DeviceIconProps> = ({ type }) => {
|
||||
switch (type) {
|
||||
case 'Boiler':
|
||||
return <CgSmartHomeBoiler />;
|
||||
case 'Sensor':
|
||||
// matches emsdevice.h DeviceType
|
||||
const enum DeviceType {
|
||||
SYSTEM = 0,
|
||||
DALLASSENSOR,
|
||||
ANALOGSENSOR,
|
||||
BOILER,
|
||||
THERMOSTAT,
|
||||
MIXER,
|
||||
SOLAR,
|
||||
HEATPUMP,
|
||||
GATEWAY,
|
||||
SWITCH,
|
||||
CONTROLLER,
|
||||
CONNECT,
|
||||
ALERT,
|
||||
PUMP,
|
||||
GENERIC,
|
||||
HEATSOURCE,
|
||||
UNKNOWN
|
||||
}
|
||||
|
||||
const DeviceIcon: FC<DeviceIconProps> = ({ type_id }) => {
|
||||
switch (type_id) {
|
||||
case DeviceType.DALLASSENSOR:
|
||||
case DeviceType.ANALOGSENSOR:
|
||||
return <MdOutlineSensors />;
|
||||
case 'Solar':
|
||||
return <FaSolarPanel />;
|
||||
case 'Thermostat':
|
||||
case DeviceType.BOILER:
|
||||
case DeviceType.HEATSOURCE:
|
||||
return <CgSmartHomeBoiler />;
|
||||
case DeviceType.THERMOSTAT:
|
||||
return <MdThermostatAuto />;
|
||||
case 'Mixer':
|
||||
case DeviceType.MIXER:
|
||||
return <AiOutlineControl />;
|
||||
case 'Heatpump':
|
||||
case DeviceType.SOLAR:
|
||||
return <FaSolarPanel />;
|
||||
case DeviceType.HEATPUMP:
|
||||
return <GiHeatHaze />;
|
||||
case 'Switch':
|
||||
return <TiFlowSwitch />;
|
||||
case 'Connect':
|
||||
return <VscVmConnect />;
|
||||
case 'Gateway':
|
||||
case DeviceType.GATEWAY:
|
||||
return <AiOutlineGateway />;
|
||||
case DeviceType.SWITCH:
|
||||
return <TiFlowSwitch />;
|
||||
case DeviceType.CONTROLLER:
|
||||
case DeviceType.CONNECT:
|
||||
return <VscVmConnect />;
|
||||
case DeviceType.ALERT:
|
||||
return <AiOutlineAlert />;
|
||||
case DeviceType.PUMP:
|
||||
return <AiOutlineChrome />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -5,16 +5,20 @@ import { Tab } from '@mui/material';
|
||||
|
||||
import { RouterTabs, useRouterTab, useLayoutTitle } from '../components';
|
||||
|
||||
import { useI18nContext } from '../i18n/i18n-react';
|
||||
|
||||
import HelpInformation from './HelpInformation';
|
||||
|
||||
const Help: FC = () => {
|
||||
useLayoutTitle('Help');
|
||||
const { LL } = useI18nContext();
|
||||
const { routerTab } = useRouterTab();
|
||||
|
||||
useLayoutTitle(LL.HELP_OF(''));
|
||||
|
||||
return (
|
||||
<>
|
||||
<RouterTabs value={routerTab}>
|
||||
<Tab value="information" label="EMS-ESP Help" />
|
||||
<Tab value="information" label={LL.HELP_OF('EMS-ESP')} />
|
||||
</RouterTabs>
|
||||
<Routes>
|
||||
<Route path="information" element={<HelpInformation />} />
|
||||
|
||||
@@ -9,14 +9,18 @@ import { useSnackbar } from 'notistack';
|
||||
import CommentIcon from '@mui/icons-material/CommentTwoTone';
|
||||
import MenuBookIcon from '@mui/icons-material/MenuBookTwoTone';
|
||||
import GitHubIcon from '@mui/icons-material/GitHub';
|
||||
import StarIcon from '@mui/icons-material/Star';
|
||||
import DownloadIcon from '@mui/icons-material/GetApp';
|
||||
import EastIcon from '@mui/icons-material/East';
|
||||
|
||||
import { extractErrorMessage } from '../utils';
|
||||
|
||||
import { useI18nContext } from '../i18n/i18n-react';
|
||||
|
||||
import * as EMSESP from './api';
|
||||
|
||||
const HelpInformation: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const saveFile = (json: any, endpoint: string) => {
|
||||
@@ -31,7 +35,7 @@ const HelpInformation: FC = () => {
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
enqueueSnackbar('System information downloaded', { variant: 'info' });
|
||||
enqueueSnackbar(LL.DOWNLOAD_SUCCESSFUL(), { variant: 'info' });
|
||||
};
|
||||
|
||||
const callAPI = async (endpoint: string) => {
|
||||
@@ -42,83 +46,84 @@ const HelpInformation: FC = () => {
|
||||
id: 0
|
||||
});
|
||||
if (response.status !== 200) {
|
||||
enqueueSnackbar('API call failed', { variant: 'error' });
|
||||
enqueueSnackbar(LL.PROBLEM_LOADING(), { variant: 'error' });
|
||||
} else {
|
||||
saveFile(response.data, endpoint);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
enqueueSnackbar(extractErrorMessage(error, 'Problem with downloading'), { variant: 'error' });
|
||||
} catch (error) {
|
||||
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_LOADING()), { variant: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title="Support Information" titleGutter>
|
||||
<SectionContent title={LL.SUPPORT_INFORMATION()} titleGutter>
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<MenuBookIcon />
|
||||
<MenuBookIcon style={{ fontSize: 24, color: 'lightblue', verticalAlign: 'middle' }} />
|
||||
</ListItemAvatar>
|
||||
<ListItemText>
|
||||
Visit the online
|
||||
{LL.HELP_INFORMATION_1()}
|
||||
<EastIcon style={{ fontSize: 24, color: 'lightblue', verticalAlign: 'middle' }} />
|
||||
|
||||
<Link target="_blank" href="https://emsesp.github.io/docs" color="primary">
|
||||
{'Wiki'}
|
||||
{LL.CLICK_HERE()}
|
||||
</Link>
|
||||
to get instructions on how to
|
||||
<Link
|
||||
target="_blank"
|
||||
href="https://emsesp.github.io/docs/#/Configure-firmware?id=ems-esp-settings"
|
||||
color="primary"
|
||||
>
|
||||
{'configure'}
|
||||
</Link>
|
||||
EMS-ESP and access other information.
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<CommentIcon />
|
||||
<CommentIcon style={{ fontSize: 24, color: 'lightblue', verticalAlign: 'middle' }} />
|
||||
</ListItemAvatar>
|
||||
<ListItemText>
|
||||
For live community chat join our
|
||||
{LL.HELP_INFORMATION_2()}
|
||||
<EastIcon style={{ fontSize: 24, color: 'lightblue', verticalAlign: 'middle' }} />
|
||||
|
||||
<Link target="_blank" href="https://discord.gg/3J3GgnzpyT" color="primary">
|
||||
{'Discord'}
|
||||
{LL.CLICK_HERE()}
|
||||
</Link>
|
||||
server.
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<GitHubIcon />
|
||||
<GitHubIcon style={{ fontSize: 24, color: 'lightblue', verticalAlign: 'middle' }} />
|
||||
</ListItemAvatar>
|
||||
|
||||
<ListItemText>
|
||||
Submit a
|
||||
{LL.HELP_INFORMATION_3()}
|
||||
<EastIcon style={{ fontSize: 24, color: 'lightblue', verticalAlign: 'middle' }} />
|
||||
<Link target="_blank" href="https://github.com/emsesp/EMS-ESP32/issues/new/choose" color="primary">
|
||||
support issue
|
||||
{LL.CLICK_HERE()}
|
||||
</Link>
|
||||
for requesting a new feature or reporting a bug.
|
||||
<br />
|
||||
Make sure you also
|
||||
<Button startIcon={<DownloadIcon />} variant="outlined" color="primary" onClick={() => callAPI('info')}>
|
||||
download
|
||||
<i>({LL.HELP_INFORMATION_4()}</i>
|
||||
<Button
|
||||
startIcon={<DownloadIcon />}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => callAPI('info')}
|
||||
>
|
||||
{LL.SUPPORT_INFO()}
|
||||
</Button>
|
||||
and attach your system details for a faster response.
|
||||
)
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
<Box border={1} p={1} mt={4}>
|
||||
<Typography align="center" variant="h6" color="orange">
|
||||
EMS-ESP will always be a free and open-source project
|
||||
<br></br>Please consider supporting it with a
|
||||
<StarIcon style={{ fontSize: 16, color: 'yellow', verticalAlign: 'middle' }} /> on
|
||||
<Link href="https://github.com/emsesp/EMS-ESP32" color="primary">
|
||||
{'GitHub'}
|
||||
<Box border={1} p={1} mt={4} color="orange">
|
||||
<Typography align="center" variant="subtitle1" color="orange">
|
||||
<b>{LL.HELP_INFORMATION_5()}</b>
|
||||
</Typography>
|
||||
<Typography align="center">
|
||||
<Link target="_blank" href="https://github.com/emsesp/EMS-ESP32" color="primary">
|
||||
{'github.com/emsesp/EMS-ESP32'}
|
||||
</Link>
|
||||
</Typography>
|
||||
<Typography align="center">@proddy @MichaelDvP</Typography>
|
||||
<Typography color="white" align="center">
|
||||
@proddy @MichaelDvP
|
||||
</Typography>
|
||||
</Box>
|
||||
</SectionContent>
|
||||
);
|
||||
|
||||
@@ -13,9 +13,13 @@ import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
|
||||
import CommentsDisabledOutlinedIcon from '@mui/icons-material/CommentsDisabledOutlined';
|
||||
import InsertCommentOutlinedIcon from '@mui/icons-material/InsertCommentOutlined';
|
||||
|
||||
type OptionType = 'readonly' | 'web_exclude' | 'api_mqtt_exclude' | 'favorite';
|
||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||
import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
|
||||
|
||||
type OptionType = 'deleted' | 'readonly' | 'web_exclude' | 'api_mqtt_exclude' | 'favorite';
|
||||
|
||||
const OPTION_ICONS: { [type in OptionType]: [React.ComponentType<SvgIconProps>, React.ComponentType<SvgIconProps>] } = {
|
||||
deleted: [DeleteForeverIcon, DeleteOutlineIcon],
|
||||
readonly: [EditOffOutlinedIcon, EditOutlinedIcon],
|
||||
web_exclude: [VisibilityOffOutlinedIcon, VisibilityOutlinedIcon],
|
||||
api_mqtt_exclude: [CommentsDisabledOutlinedIcon, InsertCommentOutlinedIcon],
|
||||
|
||||
@@ -6,6 +6,8 @@ import { AuthenticatedContext } from '../contexts/authentication';
|
||||
|
||||
import { PROJECT_PATH } from '../api/env';
|
||||
|
||||
import { useI18nContext } from '../i18n/i18n-react';
|
||||
|
||||
import TuneIcon from '@mui/icons-material/Tune';
|
||||
import DashboardIcon from '@mui/icons-material/Dashboard';
|
||||
import LayoutMenuItem from '../components/layout/LayoutMenuItem';
|
||||
@@ -13,17 +15,18 @@ import InfoIcon from '@mui/icons-material/Info';
|
||||
|
||||
const ProjectMenu: FC = () => {
|
||||
const authenticatedContext = useContext(AuthenticatedContext);
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
return (
|
||||
<List>
|
||||
<LayoutMenuItem icon={DashboardIcon} label="Dashboard" to={`/${PROJECT_PATH}/dashboard`} />
|
||||
<LayoutMenuItem icon={DashboardIcon} label={LL.DASHBOARD()} to={`/${PROJECT_PATH}/dashboard`} />
|
||||
<LayoutMenuItem
|
||||
icon={TuneIcon}
|
||||
label="Settings"
|
||||
label={LL.SETTINGS_OF('')}
|
||||
to={`/${PROJECT_PATH}/settings`}
|
||||
disabled={!authenticatedContext.me.admin}
|
||||
/>
|
||||
<LayoutMenuItem icon={InfoIcon} label="Help" to={`/${PROJECT_PATH}/help`} />
|
||||
<LayoutMenuItem icon={InfoIcon} label={LL.HELP_OF('')} to={`/${PROJECT_PATH}/help`} />
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,18 +5,22 @@ import { Tab } from '@mui/material';
|
||||
|
||||
import { RouterTabs, useRouterTab, useLayoutTitle } from '../components';
|
||||
|
||||
import { useI18nContext } from '../i18n/i18n-react';
|
||||
|
||||
import SettingsApplication from './SettingsApplication';
|
||||
import SettingsCustomization from './SettingsCustomization';
|
||||
|
||||
const Settings: FC = () => {
|
||||
useLayoutTitle('Settings');
|
||||
const { LL } = useI18nContext();
|
||||
const { routerTab } = useRouterTab();
|
||||
|
||||
useLayoutTitle(LL.SETTINGS_OF(''));
|
||||
|
||||
return (
|
||||
<>
|
||||
<RouterTabs value={routerTab}>
|
||||
<Tab value="application" label="Application Settings" />
|
||||
<Tab value="customization" label="Customization" />
|
||||
<Tab value="application" label={LL.APPLICATION_SETTINGS()} />
|
||||
<Tab value="customization" label={LL.CUSTOMIZATION()} />
|
||||
</RouterTabs>
|
||||
<Routes>
|
||||
<Route path="application" element={<SettingsApplication />} />
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ValidateFieldsError } from 'async-validator';
|
||||
|
||||
import { useSnackbar } from 'notistack';
|
||||
|
||||
import { Box, Button, Checkbox, MenuItem, Grid, Typography, Divider } from '@mui/material';
|
||||
import { Box, Button, Checkbox, MenuItem, Grid, Typography, Divider, InputAdornment } from '@mui/material';
|
||||
|
||||
import SaveIcon from '@mui/icons-material/Save';
|
||||
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
|
||||
@@ -24,6 +24,9 @@ import { numberValue, extractErrorMessage, updateValue, useRest } from '../utils
|
||||
import * as EMSESP from './api';
|
||||
import { Settings, BOARD_PROFILES } from './types';
|
||||
|
||||
import { useI18nContext } from '../i18n/i18n-react';
|
||||
import RestartMonitor from '../framework/system/RestartMonitor';
|
||||
|
||||
export function boardProfileSelectItems() {
|
||||
return Object.keys(BOARD_PROFILES).map((code) => (
|
||||
<MenuItem key={code} value={code}>
|
||||
@@ -37,6 +40,9 @@ const SettingsApplication: FC = () => {
|
||||
read: EMSESP.readSettings,
|
||||
update: EMSESP.writeSettings
|
||||
});
|
||||
const [restarting, setRestarting] = useState<boolean>();
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
@@ -64,8 +70,8 @@ const SettingsApplication: FC = () => {
|
||||
eth_clock_mode: response.data.eth_clock_mode
|
||||
});
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
enqueueSnackbar(extractErrorMessage(error, 'Problem fetching board profile'), { variant: 'error' });
|
||||
} catch (error) {
|
||||
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
|
||||
} finally {
|
||||
setProcessingBoard(false);
|
||||
}
|
||||
@@ -102,28 +108,26 @@ const SettingsApplication: FC = () => {
|
||||
validateAndSubmit();
|
||||
try {
|
||||
await EMSESP.restart();
|
||||
enqueueSnackbar('EMS-ESP is restarting...', { variant: 'info' });
|
||||
} catch (error: unknown) {
|
||||
enqueueSnackbar(extractErrorMessage(error, 'Problem restarting device'), { variant: 'error' });
|
||||
setRestarting(true);
|
||||
} catch (error) {
|
||||
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
||||
Interface Board Profile
|
||||
{LL.INTERFACE_BOARD_PROFILE()}
|
||||
</Typography>
|
||||
<Box color="warning.main">
|
||||
<Typography variant="body2">
|
||||
Select a pre-configured interface board profile from the list below or choose "Custom" to configure your own
|
||||
hardware settings.
|
||||
</Typography>
|
||||
<Typography variant="body2">{LL.BOARD_PROFILE_TEXT()}</Typography>
|
||||
</Box>
|
||||
<ValidatedTextField
|
||||
name="board_profile"
|
||||
label="Board Profile"
|
||||
label={LL.BOARD_PROFILE()}
|
||||
value={data.board_profile}
|
||||
disabled={processingBoard}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={changeBoardProfile}
|
||||
margin="normal"
|
||||
@@ -132,17 +136,24 @@ const SettingsApplication: FC = () => {
|
||||
{boardProfileSelectItems()}
|
||||
<Divider />
|
||||
<MenuItem key={'CUSTOM'} value={'CUSTOM'}>
|
||||
Custom…
|
||||
{LL.CUSTOM()}…
|
||||
</MenuItem>
|
||||
</ValidatedTextField>
|
||||
{data.board_profile === 'CUSTOM' && (
|
||||
<>
|
||||
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
||||
<Grid item xs={4}>
|
||||
<Grid
|
||||
container
|
||||
spacing={1}
|
||||
sx={{ pt: 1 }}
|
||||
direction="row"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
<Grid item xs={6} sm={4}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="rx_gpio"
|
||||
label="Rx GPIO"
|
||||
label={LL.GPIO_OF('Rx')}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.rx_gpio)}
|
||||
@@ -152,11 +163,11 @@ const SettingsApplication: FC = () => {
|
||||
disabled={saving}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<Grid item xs={6} sm={4}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="tx_gpio"
|
||||
label="Tx GPIO"
|
||||
label={LL.GPIO_OF('Tx')}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.tx_gpio)}
|
||||
@@ -166,11 +177,11 @@ const SettingsApplication: FC = () => {
|
||||
disabled={saving}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<Grid item xs={6} sm={4}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="pbutton_gpio"
|
||||
label="Button GPIO"
|
||||
label={LL.GPIO_OF(LL.BUTTON())}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.pbutton_gpio)}
|
||||
@@ -180,11 +191,11 @@ const SettingsApplication: FC = () => {
|
||||
disabled={saving}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Grid item xs={6} sm={4}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="dallas_gpio"
|
||||
label="Temperature GPIO (0=disabled)"
|
||||
label={LL.GPIO_OF(LL.TEMPERATURE()) + ' (0=' + LL.DISABLED(1) + ')'}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.dallas_gpio)}
|
||||
@@ -194,11 +205,11 @@ const SettingsApplication: FC = () => {
|
||||
disabled={saving}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Grid item xs={6} sm={4}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="led_gpio"
|
||||
label="LED GPIO (0=disabled)"
|
||||
label={LL.GPIO_OF('LED') + ' (0=' + LL.DISABLED(1) + ')'}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.led_gpio)}
|
||||
@@ -208,30 +219,37 @@ const SettingsApplication: FC = () => {
|
||||
disabled={saving}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<ValidatedTextField
|
||||
name="phy_type"
|
||||
label="Eth PHY Type"
|
||||
disabled={saving}
|
||||
value={data.phy_type}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
select
|
||||
>
|
||||
<MenuItem value={0}>No Ethernet Module</MenuItem>
|
||||
<MenuItem value={1}>LAN8720</MenuItem>
|
||||
<MenuItem value={2}>TLK110</MenuItem>
|
||||
</ValidatedTextField>
|
||||
<Grid item xs={6} sm={4}>
|
||||
<ValidatedTextField
|
||||
name="phy_type"
|
||||
label={LL.PHY_TYPE()}
|
||||
disabled={saving}
|
||||
value={data.phy_type}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
select
|
||||
>
|
||||
<MenuItem value={0}>{LL.DISABLED(1)}</MenuItem>
|
||||
<MenuItem value={1}>LAN8720</MenuItem>
|
||||
<MenuItem value={2}>TLK110</MenuItem>
|
||||
</ValidatedTextField>
|
||||
</Grid>
|
||||
</Grid>
|
||||
{data.phy_type !== 0 && (
|
||||
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
||||
<Grid item>
|
||||
<Grid
|
||||
container
|
||||
spacing={1}
|
||||
sx={{ pt: 1 }}
|
||||
direction="row"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
<Grid item xs={6} sm={4}>
|
||||
<ValidatedTextField
|
||||
name="eth_power"
|
||||
label="Eth Power GPIO (-1=disabled)"
|
||||
label={LL.GPIO_OF('PHY Power') + ' (-1=' + LL.DISABLED(1) + ')'}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.eth_power)}
|
||||
@@ -241,10 +259,10 @@ const SettingsApplication: FC = () => {
|
||||
disabled={saving}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Grid item xs={6} sm={4}>
|
||||
<ValidatedTextField
|
||||
name="eth_phy_addr"
|
||||
label="Eth I²C-address"
|
||||
label={LL.ADDRESS_OF('PHY I²C')}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.eth_phy_addr)}
|
||||
@@ -254,10 +272,10 @@ const SettingsApplication: FC = () => {
|
||||
disabled={saving}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Grid item xs={6} sm={4}>
|
||||
<ValidatedTextField
|
||||
name="eth_clock_mode"
|
||||
label="Eth Clock Mode"
|
||||
label="PHY Clk"
|
||||
disabled={saving}
|
||||
value={data.eth_clock_mode}
|
||||
fullWidth
|
||||
@@ -276,14 +294,14 @@ const SettingsApplication: FC = () => {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Typography variant="h6" color="primary">
|
||||
EMS Bus Settings
|
||||
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
||||
{LL.SETTINGS_OF(LL.EMS_BUS(0))}
|
||||
</Typography>
|
||||
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
||||
<Grid item xs={6}>
|
||||
<ValidatedTextField
|
||||
name="tx_mode"
|
||||
label="Tx Mode"
|
||||
label={LL.TX_MODE()}
|
||||
disabled={saving}
|
||||
value={data.tx_mode}
|
||||
fullWidth
|
||||
@@ -295,13 +313,13 @@ const SettingsApplication: FC = () => {
|
||||
<MenuItem value={1}>EMS</MenuItem>
|
||||
<MenuItem value={2}>EMS+</MenuItem>
|
||||
<MenuItem value={3}>HT3</MenuItem>
|
||||
<MenuItem value={4}>Hardware</MenuItem>
|
||||
<MenuItem value={4}>{LL.HARDWARE()}</MenuItem>
|
||||
</ValidatedTextField>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<ValidatedTextField
|
||||
name="ems_bus_id"
|
||||
label="Bus ID"
|
||||
label={LL.ID_OF(LL.EMS_BUS(1))}
|
||||
disabled={saving}
|
||||
value={data.ems_bus_id}
|
||||
fullWidth
|
||||
@@ -310,102 +328,143 @@ const SettingsApplication: FC = () => {
|
||||
margin="normal"
|
||||
select
|
||||
>
|
||||
<MenuItem value={0x0a}>Terminal (0x0A)</MenuItem>
|
||||
<MenuItem value={0x0b}>Service Key (0x0B)</MenuItem>
|
||||
<MenuItem value={0x0d}>Modem (0x0D)</MenuItem>
|
||||
<MenuItem value={0x0a}>Terminal (0x0A)</MenuItem>
|
||||
<MenuItem value={0x0e}>Converter (0x0E)</MenuItem>
|
||||
<MenuItem value={0x0f}>Time Module (0x0F)</MenuItem>
|
||||
<MenuItem value={0x12}>Alarm Module (0x12)</MenuItem>
|
||||
<MenuItem value={0x48}>Gateway 1 (0x48)</MenuItem>
|
||||
<MenuItem value={0x49}>Gateway 2 (0x49)</MenuItem>
|
||||
<MenuItem value={0x4a}>Gateway 3 (0x4A)</MenuItem>
|
||||
<MenuItem value={0x4b}>Gateway 4 (0x4B)</MenuItem>
|
||||
<MenuItem value={0x4c}>Gateway 5 (0x4C)</MenuItem>
|
||||
<MenuItem value={0x4d}>Gateway 7 (0x4D)</MenuItem>
|
||||
</ValidatedTextField>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
||||
General Options
|
||||
{LL.GENERAL_OPTIONS()}
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
'& .MuiTextField-root': { width: '25ch' }
|
||||
}}
|
||||
>
|
||||
<ValidatedTextField
|
||||
name="locale"
|
||||
label={LL.LANGUAGE_ENTITIES()}
|
||||
disabled={saving}
|
||||
value={data.locale}
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
size="small"
|
||||
select
|
||||
>
|
||||
<MenuItem value="en">English (EN)</MenuItem>
|
||||
<Divider />
|
||||
<MenuItem value="de">Deutsch (DE)</MenuItem>
|
||||
<MenuItem value="fr">Français (FR)</MenuItem>
|
||||
<MenuItem value="nl">Nederlands (NL)</MenuItem>
|
||||
<MenuItem value="no">Norsk (NO)</MenuItem>
|
||||
<MenuItem value="pl">Polski (PL)</MenuItem>
|
||||
<MenuItem value="sv">Svenska (SV)</MenuItem>
|
||||
</ValidatedTextField>
|
||||
</Box>
|
||||
{data.led_gpio !== 0 && (
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox checked={data.hide_led} onChange={updateFormValue} name="hide_led" />}
|
||||
label="Hide LED"
|
||||
label={LL.HIDE_LED()}
|
||||
disabled={saving}
|
||||
/>
|
||||
)}
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox checked={data.telnet_enabled} onChange={updateFormValue} name="telnet_enabled" />}
|
||||
label="Enable Telnet Console"
|
||||
label={LL.ENABLE_TELNET()}
|
||||
disabled={saving}
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox checked={data.analog_enabled} onChange={updateFormValue} name="analog_enabled" />}
|
||||
label="Enable Analog Sensors"
|
||||
label={LL.ENABLE_ANALOG()}
|
||||
disabled={saving}
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox checked={data.fahrenheit} onChange={updateFormValue} name="fahrenheit" />}
|
||||
label="Convert temperature values to Fahrenheit"
|
||||
label={LL.CONVERT_FAHRENHEIT()}
|
||||
disabled={saving}
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox checked={data.notoken_api} onChange={updateFormValue} name="notoken_api" />}
|
||||
label="Bypass Access Token authorization on API calls"
|
||||
label={LL.BYPASS_TOKEN()}
|
||||
disabled={saving}
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox checked={data.readonly_mode} onChange={updateFormValue} name="readonly_mode" />}
|
||||
label="Enable Read only mode (blocks all outgoing EMS Tx write commands)"
|
||||
label={LL.READONLY()}
|
||||
disabled={saving}
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox checked={data.low_clock} onChange={updateFormValue} name="low_clock" />}
|
||||
label="Underclock CPU speed"
|
||||
label={LL.UNDERCLOCK_CPU()}
|
||||
disabled={saving}
|
||||
/>
|
||||
<Grid container spacing={0} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox checked={data.shower_timer} onChange={updateFormValue} name="shower_timer" />}
|
||||
label="Enable Shower Timer"
|
||||
label={LL.ENABLE_SHOWER_TIMER()}
|
||||
disabled={saving}
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
sx={{ pb: 2 }}
|
||||
control={<Checkbox checked={data.shower_alert} onChange={updateFormValue} name="shower_alert" />}
|
||||
label="Enable Shower Alert"
|
||||
label={LL.ENABLE_SHOWER_ALERT()}
|
||||
disabled={!data.shower_timer}
|
||||
/>
|
||||
{data.shower_alert && (
|
||||
<>
|
||||
<Grid item xs={2}>
|
||||
<Grid item sx={{ pr: 1, pb: 2 }}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="shower_alert_trigger"
|
||||
label="Trigger Time (minutes)"
|
||||
label={LL.TRIGGER_TIME()}
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">{LL.MINUTES()}</InputAdornment>
|
||||
}}
|
||||
variant="outlined"
|
||||
value={data.shower_alert_trigger}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
size="small"
|
||||
disabled={!data.shower_timer}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={2}>
|
||||
<Grid item sx={{ pb: 3 }}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="shower_alert_coldshot"
|
||||
label="Cold Shot Time (seconds)"
|
||||
label={LL.COLD_SHOT_DURATION()}
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||
}}
|
||||
variant="outlined"
|
||||
value={data.shower_alert_coldshot}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
size="small"
|
||||
disabled={!data.shower_timer}
|
||||
/>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
</Grid>
|
||||
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
||||
Formatting Options
|
||||
<Typography variant="h6" color="primary">
|
||||
{LL.FORMATTING_OPTIONS()}
|
||||
</Typography>
|
||||
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
||||
<Grid item xs={4}>
|
||||
<Grid item xs={6} sm={4}>
|
||||
<ValidatedTextField
|
||||
name="bool_dashboard"
|
||||
label="Boolean Format Dashboard"
|
||||
label={LL.BOOLEAN_FORMAT_DASHBOARD()}
|
||||
value={data.bool_dashboard}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
@@ -413,16 +472,16 @@ const SettingsApplication: FC = () => {
|
||||
margin="normal"
|
||||
select
|
||||
>
|
||||
<MenuItem value={1}>on/off</MenuItem>
|
||||
<MenuItem value={2}>ON/OFF</MenuItem>
|
||||
<MenuItem value={1}>{LL.ONOFF()}</MenuItem>
|
||||
<MenuItem value={2}>{LL.ONOFF_CAP()}</MenuItem>
|
||||
<MenuItem value={3}>true/false</MenuItem>
|
||||
<MenuItem value={5}>1/0</MenuItem>
|
||||
</ValidatedTextField>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<Grid item xs={6} sm={4}>
|
||||
<ValidatedTextField
|
||||
name="bool_format"
|
||||
label="Boolean Format API/MQTT"
|
||||
label={LL.BOOLEAN_FORMAT_API()}
|
||||
value={data.bool_format}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
@@ -430,18 +489,18 @@ const SettingsApplication: FC = () => {
|
||||
margin="normal"
|
||||
select
|
||||
>
|
||||
<MenuItem value={1}>"on"/"off"</MenuItem>
|
||||
<MenuItem value={2}>"ON"/"OFF"</MenuItem>
|
||||
<MenuItem value={1}>{LL.ONOFF()}</MenuItem>
|
||||
<MenuItem value={2}>{LL.ONOFF_CAP()}</MenuItem>
|
||||
<MenuItem value={3}>"true"/"false"</MenuItem>
|
||||
<MenuItem value={4}>true/false</MenuItem>
|
||||
<MenuItem value={5}>"1"/"0"</MenuItem>
|
||||
<MenuItem value={6}>1/0</MenuItem>
|
||||
</ValidatedTextField>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<Grid item xs={6} sm={4}>
|
||||
<ValidatedTextField
|
||||
name="enum_format"
|
||||
label="Enum Format API/MQTT"
|
||||
label={LL.ENUM_FORMAT()}
|
||||
value={data.enum_format}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
@@ -449,29 +508,29 @@ const SettingsApplication: FC = () => {
|
||||
margin="normal"
|
||||
select
|
||||
>
|
||||
<MenuItem value={1}>Value</MenuItem>
|
||||
<MenuItem value={2}>Index</MenuItem>
|
||||
<MenuItem value={1}>{LL.VALUE(1)}</MenuItem>
|
||||
<MenuItem value={2}>{LL.INDEX()}</MenuItem>
|
||||
</ValidatedTextField>
|
||||
</Grid>
|
||||
</Grid>
|
||||
{data.dallas_gpio !== 0 && (
|
||||
<>
|
||||
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
||||
Temperature Sensors
|
||||
{LL.TEMP_SENSORS()}
|
||||
</Typography>
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox checked={data.dallas_parasite} onChange={updateFormValue} name="dallas_parasite" />}
|
||||
label="Enable parasite power"
|
||||
label={LL.ENABLE_PARASITE()}
|
||||
disabled={saving}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
||||
Logging
|
||||
{LL.LOGGING()}
|
||||
</Typography>
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox checked={data.trace_raw} onChange={updateFormValue} name="trace_raw" />}
|
||||
label="Log EMS telegrams in hexadecimal"
|
||||
label={LL.LOG_HEX()}
|
||||
disabled={saving}
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
@@ -483,11 +542,11 @@ const SettingsApplication: FC = () => {
|
||||
disabled={saving}
|
||||
/>
|
||||
}
|
||||
label="Enable Syslog"
|
||||
label={LL.ENABLE_SYSLOG()}
|
||||
/>
|
||||
{data.syslog_enabled && (
|
||||
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
||||
<Grid item xs={5}>
|
||||
<Grid item xs={4}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="syslog_host"
|
||||
@@ -500,7 +559,7 @@ const SettingsApplication: FC = () => {
|
||||
disabled={saving}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Grid item xs={4}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="syslog_port"
|
||||
@@ -514,10 +573,10 @@ const SettingsApplication: FC = () => {
|
||||
disabled={saving}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={5}>
|
||||
<Grid item xs={4}>
|
||||
<ValidatedTextField
|
||||
name="syslog_level"
|
||||
label="Log Level"
|
||||
label={LL.LOG_LEVEL()}
|
||||
value={data.syslog_level}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
@@ -534,11 +593,14 @@ const SettingsApplication: FC = () => {
|
||||
<MenuItem value={9}>ALL</MenuItem>
|
||||
</ValidatedTextField>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Grid item xs={4}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="syslog_mark_interval"
|
||||
label="Mark Interval (seconds, 0=off)"
|
||||
label={LL.MARK_INTERVAL()}
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||
}}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.syslog_mark_interval}
|
||||
@@ -551,9 +613,9 @@ const SettingsApplication: FC = () => {
|
||||
</Grid>
|
||||
)}
|
||||
{restartNeeded && (
|
||||
<MessageBox my={2} level="warning" message="EMS-ESP needs to be restarted to apply changed system settings">
|
||||
<MessageBox my={2} level="warning" message={LL.RESTART_TEXT()}>
|
||||
<Button startIcon={<PowerSettingsNewIcon />} variant="contained" color="error" onClick={restart}>
|
||||
Restart
|
||||
{LL.RESTART()}
|
||||
</Button>
|
||||
</MessageBox>
|
||||
)}
|
||||
@@ -567,7 +629,7 @@ const SettingsApplication: FC = () => {
|
||||
type="submit"
|
||||
onClick={validateAndSubmit}
|
||||
>
|
||||
Save
|
||||
{LL.SAVE()}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
)}
|
||||
@@ -576,8 +638,8 @@ const SettingsApplication: FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title="Application Settings" titleGutter>
|
||||
{content()}
|
||||
<SectionContent title={LL.APPLICATION_SETTINGS()} titleGutter>
|
||||
{restarting ? <RestartMonitor /> : content()}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,9 +17,11 @@ import {
|
||||
Link
|
||||
} from '@mui/material';
|
||||
|
||||
import { MessageBox } from '../components';
|
||||
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
|
||||
|
||||
import { Table } from '@table-library/react-table-library/table';
|
||||
import { useTheme } from '@table-library/react-table-library/theme';
|
||||
import { useSort, SortToggleType } from '@table-library/react-table-library/sort';
|
||||
import { Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table';
|
||||
|
||||
import { useSnackbar } from 'notistack';
|
||||
@@ -28,9 +30,6 @@ import SaveIcon from '@mui/icons-material/Save';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
|
||||
import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore';
|
||||
import KeyboardArrowUpOutlinedIcon from '@mui/icons-material/KeyboardArrowUpOutlined';
|
||||
import KeyboardArrowDownOutlinedIcon from '@mui/icons-material/KeyboardArrowDownOutlined';
|
||||
import UnfoldMoreOutlinedIcon from '@mui/icons-material/UnfoldMoreOutlined';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import FilterListIcon from '@mui/icons-material/FilterList';
|
||||
|
||||
@@ -40,16 +39,25 @@ import { ButtonRow, FormLoader, ValidatedTextField, SectionContent } from '../co
|
||||
|
||||
import * as EMSESP from './api';
|
||||
|
||||
import { extractErrorMessage } from '../utils';
|
||||
import { extractErrorMessage, updateValue } from '../utils';
|
||||
|
||||
import { DeviceShort, Devices, DeviceEntity, DeviceEntityMask } from './types';
|
||||
|
||||
import { useI18nContext } from '../i18n/i18n-react';
|
||||
import RestartMonitor from '../framework/system/RestartMonitor';
|
||||
|
||||
export const APIURL = window.location.origin + '/api/';
|
||||
|
||||
const SettingsCustomization: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
const [restarting, setRestarting] = useState<boolean>(false);
|
||||
const [restartNeeded, setRestartNeeded] = useState<boolean>(false);
|
||||
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const [deviceEntities, setDeviceEntities] = useState<DeviceEntity[]>([{ id: '', v: 0, n: '', m: 0, w: false }]);
|
||||
const emptyDeviceEntity = { id: '', v: 0, n: '', cn: '', m: 0, w: false };
|
||||
|
||||
const [deviceEntities, setDeviceEntities] = useState<DeviceEntity[]>([emptyDeviceEntity]);
|
||||
const [devices, setDevices] = useState<Devices>();
|
||||
const [errorMessage, setErrorMessage] = useState<string>();
|
||||
const [selectedDevice, setSelectedDevice] = useState<number>(-1);
|
||||
@@ -57,12 +65,14 @@ const SettingsCustomization: FC = () => {
|
||||
const [selectedFilters, setSelectedFilters] = useState<number>(0);
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
const [deviceEntity, setDeviceEntity] = useState<DeviceEntity>();
|
||||
|
||||
// eslint-disable-next-line
|
||||
const [masks, setMasks] = useState(() => ['']);
|
||||
|
||||
const entities_theme = useTheme({
|
||||
Table: `
|
||||
--data-table-library_grid-template-columns: 120px repeat(1, minmax(0, 1fr)) 120px;
|
||||
--data-table-library_grid-template-columns: 150px repeat(1, minmax(80px, 1fr)) 45px 45px 120px;
|
||||
`,
|
||||
BaseRow: `
|
||||
font-size: 14px;
|
||||
@@ -71,6 +81,12 @@ const SettingsCustomization: FC = () => {
|
||||
}
|
||||
`,
|
||||
BaseCell: `
|
||||
&:nth-of-type(3) {
|
||||
text-align: right;
|
||||
}
|
||||
&:nth-of-type(4) {
|
||||
text-align: right;
|
||||
}
|
||||
&:last-of-type {
|
||||
text-align: right;
|
||||
}
|
||||
@@ -92,6 +108,7 @@ const SettingsCustomization: FC = () => {
|
||||
Row: `
|
||||
background-color: #1e1e1e;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
.td {
|
||||
border-top: 1px solid #565656;
|
||||
@@ -104,6 +121,11 @@ const SettingsCustomization: FC = () => {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
&:hover .td {
|
||||
border-top: 1px solid #177ac9;
|
||||
border-bottom: 1px solid #177ac9;
|
||||
}
|
||||
|
||||
&:nth-of-type(odd) .td {
|
||||
background-color: #303030;
|
||||
}
|
||||
@@ -112,56 +134,36 @@ const SettingsCustomization: FC = () => {
|
||||
&:nth-of-type(2) {
|
||||
padding: 8px;
|
||||
}
|
||||
&:nth-of-type(3) {
|
||||
padding-right: 4px;
|
||||
}
|
||||
&:nth-of-type(4) {
|
||||
padding-right: 4px;
|
||||
}
|
||||
&:last-of-type {
|
||||
padding-right: 8px;
|
||||
}
|
||||
`
|
||||
});
|
||||
|
||||
const getSortIcon = (state: any, sortKey: any) => {
|
||||
if (state.sortKey === sortKey && state.reverse) {
|
||||
return <KeyboardArrowDownOutlinedIcon />;
|
||||
}
|
||||
if (state.sortKey === sortKey && !state.reverse) {
|
||||
return <KeyboardArrowUpOutlinedIcon />;
|
||||
}
|
||||
return <UnfoldMoreOutlinedIcon />;
|
||||
};
|
||||
|
||||
const entity_sort = useSort(
|
||||
{ nodes: deviceEntities },
|
||||
{},
|
||||
{
|
||||
sortIcon: {
|
||||
iconDefault: <UnfoldMoreOutlinedIcon />,
|
||||
iconUp: <KeyboardArrowUpOutlinedIcon />,
|
||||
iconDown: <KeyboardArrowDownOutlinedIcon />
|
||||
},
|
||||
sortToggleType: SortToggleType.AlternateWithReset,
|
||||
sortFns: {
|
||||
NAME: (array) => array.sort((a, b) => a.id.localeCompare(b.id))
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const fetchDevices = useCallback(async () => {
|
||||
try {
|
||||
setDevices((await EMSESP.readDevices()).data);
|
||||
} catch (error: unknown) {
|
||||
setErrorMessage(extractErrorMessage(error, 'Failed to fetch device list'));
|
||||
} catch (error) {
|
||||
setErrorMessage(extractErrorMessage(error, LL.PROBLEM_LOADING()));
|
||||
}
|
||||
}, []);
|
||||
}, [LL]);
|
||||
|
||||
const setInitialMask = (data: DeviceEntity[]) => {
|
||||
setDeviceEntities(data.map((de) => ({ ...de, om: de.m })));
|
||||
setDeviceEntities(data.map((de) => ({ ...de, o_m: de.m, o_cn: de.cn, o_mi: de.mi, o_ma: de.ma })));
|
||||
};
|
||||
|
||||
const fetchDeviceEntities = async (unique_id: number) => {
|
||||
try {
|
||||
const data = (await EMSESP.readDeviceEntities({ id: unique_id })).data;
|
||||
setInitialMask(data);
|
||||
} catch (error: unknown) {
|
||||
setErrorMessage(extractErrorMessage(error, 'Problem fetching device entities'));
|
||||
const new_deviceEntities = (await EMSESP.readDeviceEntities({ id: unique_id })).data;
|
||||
setInitialMask(new_deviceEntities);
|
||||
} catch (error) {
|
||||
setErrorMessage(extractErrorMessage(error, LL.PROBLEM_LOADING()));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -181,15 +183,10 @@ const SettingsCustomization: FC = () => {
|
||||
}
|
||||
|
||||
function formatName(de: DeviceEntity) {
|
||||
if (de.n === undefined || de.n === de.id) {
|
||||
return de.id;
|
||||
} else if (de.n === '') {
|
||||
return 'Command: ' + de.id;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{de.n} (
|
||||
<Link target="_blank" href={APIURL + devices?.devices[selectedDevice].t + '/' + de.id}>
|
||||
{de.n && (de.n[0] === '!' ? LL.COMMAND() + ': ' + de.n.slice(1) : de.cn && de.cn !== '' ? de.cn : de.n) + ' '}(
|
||||
<Link target="_blank" href={APIURL + devices?.devices[selectedDevice].tn + '/' + de.id}>
|
||||
{de.id}
|
||||
</Link>
|
||||
)
|
||||
@@ -219,6 +216,9 @@ const SettingsCustomization: FC = () => {
|
||||
if ((m & 8) === 8) {
|
||||
new_masks.push('8');
|
||||
}
|
||||
if ((m & 128) === 128) {
|
||||
new_masks.push('128');
|
||||
}
|
||||
return new_masks;
|
||||
};
|
||||
|
||||
@@ -244,43 +244,65 @@ const SettingsCustomization: FC = () => {
|
||||
const selected_device = parseInt(event.target.value, 10);
|
||||
setSelectedDevice(selected_device);
|
||||
fetchDeviceEntities(devices?.devices[selected_device].i);
|
||||
setRestartNeeded(false);
|
||||
}
|
||||
};
|
||||
|
||||
const resetCustomization = async () => {
|
||||
try {
|
||||
await EMSESP.resetCustomizations();
|
||||
enqueueSnackbar('All customizations have been removed. Restarting...', { variant: 'info' });
|
||||
} catch (error: unknown) {
|
||||
enqueueSnackbar(extractErrorMessage(error, 'Problem resetting customizations'), { variant: 'error' });
|
||||
enqueueSnackbar(LL.CUSTOMIZATIONS_RESTART(), { variant: 'info' });
|
||||
} catch (error) {
|
||||
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
|
||||
} finally {
|
||||
setConfirmReset(false);
|
||||
}
|
||||
};
|
||||
|
||||
const restart = async () => {
|
||||
try {
|
||||
await EMSESP.restart();
|
||||
setRestarting(true);
|
||||
} catch (error) {
|
||||
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
const saveCustomization = async () => {
|
||||
if (devices && deviceEntities && selectedDevice !== -1) {
|
||||
const masked_entities = deviceEntities
|
||||
.filter((de) => de.m !== de.om)
|
||||
.map((new_de) => new_de.m.toString(16).padStart(2, '0') + new_de.id);
|
||||
.filter((de) => de.m !== de.o_m || de.cn !== de.o_cn || de.ma !== de.o_ma || de.mi !== de.o_mi)
|
||||
.map(
|
||||
(new_de) =>
|
||||
new_de.m.toString(16).padStart(2, '0') +
|
||||
new_de.id +
|
||||
(new_de.cn || new_de.mi || new_de.ma ? '|' : '') +
|
||||
(new_de.cn ? new_de.cn : '') +
|
||||
(new_de.mi ? '>' + new_de.mi : '') +
|
||||
(new_de.ma ? '<' + new_de.ma : '')
|
||||
);
|
||||
|
||||
if (masked_entities.length > 60) {
|
||||
enqueueSnackbar('Selected entities exceeded limit of 60. Please Save in batches', { variant: 'warning' });
|
||||
// check size in bytes to match buffer in CPP, which is 2048
|
||||
const bytes = new TextEncoder().encode(JSON.stringify(masked_entities)).length;
|
||||
if (bytes > 2000) {
|
||||
enqueueSnackbar(LL.CUSTOMIZATIONS_FULL(), { variant: 'warning' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await EMSESP.writeMaskedEntities({
|
||||
const response = await EMSESP.writeCustomEntities({
|
||||
id: devices?.devices[selectedDevice].i,
|
||||
entity_ids: masked_entities
|
||||
});
|
||||
if (response.status === 200) {
|
||||
enqueueSnackbar('Customization saved', { variant: 'success' });
|
||||
enqueueSnackbar(LL.CUSTOMIZATIONS_SAVED(), { variant: 'success' });
|
||||
} else if (response.status === 201) {
|
||||
setRestartNeeded(true);
|
||||
} else {
|
||||
enqueueSnackbar('Customization save failed', { variant: 'error' });
|
||||
enqueueSnackbar(LL.PROBLEM_UPDATING(), { variant: 'error' });
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
enqueueSnackbar(extractErrorMessage(error, 'Problem sending entity list'), { variant: 'error' });
|
||||
} catch (error) {
|
||||
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
|
||||
}
|
||||
setInitialMask(deviceEntities);
|
||||
}
|
||||
@@ -294,21 +316,18 @@ const SettingsCustomization: FC = () => {
|
||||
return (
|
||||
<>
|
||||
<Box mb={2} color="warning.main">
|
||||
<Typography variant="body2">Select a device and customize each of its entities using the options:</Typography>
|
||||
<Typography variant="body2">{LL.CUSTOMIZATIONS_HELP_1()}</Typography>
|
||||
<Typography variant="body2">
|
||||
<OptionIcon type="favorite" isSet={true} />
|
||||
=mark as favorite
|
||||
<OptionIcon type="readonly" isSet={true} />
|
||||
=disable write action
|
||||
<OptionIcon type="api_mqtt_exclude" isSet={true} />
|
||||
=exclude from MQTT and API
|
||||
<OptionIcon type="web_exclude" isSet={true} />
|
||||
=hide from Dashboard
|
||||
<OptionIcon type="favorite" isSet={true} />={LL.CUSTOMIZATIONS_HELP_2()}
|
||||
<OptionIcon type="readonly" isSet={true} />={LL.CUSTOMIZATIONS_HELP_3()}
|
||||
<OptionIcon type="api_mqtt_exclude" isSet={true} />={LL.CUSTOMIZATIONS_HELP_4()}
|
||||
<OptionIcon type="web_exclude" isSet={true} />={LL.CUSTOMIZATIONS_HELP_5()}
|
||||
<OptionIcon type="deleted" isSet={true} />={LL.CUSTOMIZATIONS_HELP_6()}
|
||||
</Typography>
|
||||
</Box>
|
||||
<ValidatedTextField
|
||||
name="device"
|
||||
label="EMS Device"
|
||||
label={LL.EMS_DEVICE()}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={selectedDevice}
|
||||
@@ -317,7 +336,7 @@ const SettingsCustomization: FC = () => {
|
||||
select
|
||||
>
|
||||
<MenuItem disabled key={0} value={-1}>
|
||||
Select a device...
|
||||
{LL.SELECT_DEVICE()}...
|
||||
</MenuItem>
|
||||
{devices.devices.map((device: DeviceShort, index) => (
|
||||
<MenuItem key={index} value={index}>
|
||||
@@ -329,6 +348,33 @@ const SettingsCustomization: FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const editEntity = (de: DeviceEntity) => {
|
||||
if (de.n === undefined || (de.n && de.n[0] === '!')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (de.cn === undefined) {
|
||||
de.cn = '';
|
||||
}
|
||||
setDeviceEntity(de);
|
||||
};
|
||||
|
||||
const updateEntity = () => {
|
||||
if (deviceEntity) {
|
||||
setDeviceEntities((prevState) => {
|
||||
const newState = prevState.map((obj) => {
|
||||
if (obj.id === deviceEntity.id) {
|
||||
return { ...obj, cn: deviceEntity.cn, mi: deviceEntity.mi, ma: deviceEntity.ma };
|
||||
}
|
||||
return obj;
|
||||
});
|
||||
return newState;
|
||||
});
|
||||
}
|
||||
|
||||
setDeviceEntity(undefined);
|
||||
};
|
||||
|
||||
const renderDeviceData = () => {
|
||||
if (devices?.devices.length === 0 || deviceEntities[0].id === '') {
|
||||
return;
|
||||
@@ -389,6 +435,9 @@ const SettingsCustomization: FC = () => {
|
||||
<ToggleButton value="1">
|
||||
<OptionIcon type="web_exclude" isSet={true} />
|
||||
</ToggleButton>
|
||||
<ToggleButton value="128">
|
||||
<OptionIcon type="deleted" isSet={true} />
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Grid>
|
||||
|
||||
@@ -401,7 +450,7 @@ const SettingsCustomization: FC = () => {
|
||||
color="inherit"
|
||||
onClick={() => maskDisabled(false)}
|
||||
>
|
||||
set all
|
||||
{LL.SET_ALL()}
|
||||
<OptionIcon type="api_mqtt_exclude" isSet={false} />
|
||||
<OptionIcon type="web_exclude" isSet={false} />
|
||||
</Button>
|
||||
@@ -416,81 +465,88 @@ const SettingsCustomization: FC = () => {
|
||||
color="inherit"
|
||||
onClick={() => maskDisabled(true)}
|
||||
>
|
||||
set all
|
||||
{LL.SET_ALL()}
|
||||
<OptionIcon type="api_mqtt_exclude" isSet={true} />
|
||||
<OptionIcon type="web_exclude" isSet={true} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Table data={{ nodes: shown_data }} theme={entities_theme} sort={entity_sort} layout={{ custom: true }}>
|
||||
<Table data={{ nodes: shown_data }} theme={entities_theme} layout={{ custom: true }}>
|
||||
{(tableList: any) => (
|
||||
<>
|
||||
<Header>
|
||||
<HeaderRow>
|
||||
<HeaderCell stiff>OPTIONS</HeaderCell>
|
||||
<HeaderCell stiff>{LL.OPTIONS()}</HeaderCell>
|
||||
<HeaderCell resize>
|
||||
<Button
|
||||
fullWidth
|
||||
style={{ fontSize: '14px', justifyContent: 'flex-start' }}
|
||||
endIcon={getSortIcon(entity_sort.state, 'NAME')}
|
||||
onClick={() => entity_sort.fns.onToggleSort({ sortKey: 'NAME' })}
|
||||
>
|
||||
NAME
|
||||
<Button fullWidth style={{ fontSize: '14px', justifyContent: 'flex-start' }}>
|
||||
{LL.NAME(1)}
|
||||
</Button>
|
||||
</HeaderCell>
|
||||
<HeaderCell resize>VALUE</HeaderCell>
|
||||
<HeaderCell stiff>{LL.MIN()}</HeaderCell>
|
||||
<HeaderCell stiff>{LL.MAX()}</HeaderCell>
|
||||
<HeaderCell resize>{LL.VALUE(0)}</HeaderCell>
|
||||
</HeaderRow>
|
||||
</Header>
|
||||
<Body>
|
||||
{tableList.map((de: DeviceEntity) => (
|
||||
<Row key={de.id} item={de}>
|
||||
<Row key={de.id} item={de} onClick={() => editEntity(de)}>
|
||||
<Cell stiff>
|
||||
<ToggleButtonGroup
|
||||
size="small"
|
||||
color="secondary"
|
||||
value={getMaskString(de.m)}
|
||||
onChange={(event, mask) => {
|
||||
de.m = getMaskNumber(mask);
|
||||
if (de.n === '' && de.m & DeviceEntityMask.DV_READONLY) {
|
||||
de.m = de.m | DeviceEntityMask.DV_WEB_EXCLUDE;
|
||||
}
|
||||
if (de.m & DeviceEntityMask.DV_WEB_EXCLUDE) {
|
||||
de.m = de.m & ~DeviceEntityMask.DV_FAVORITE;
|
||||
}
|
||||
setMasks(['']);
|
||||
}}
|
||||
>
|
||||
<ToggleButton value="8" disabled={(de.m & 1) !== 0 || de.n === undefined}>
|
||||
<OptionIcon
|
||||
type="favorite"
|
||||
isSet={(de.m & DeviceEntityMask.DV_FAVORITE) === DeviceEntityMask.DV_FAVORITE}
|
||||
/>
|
||||
</ToggleButton>
|
||||
<ToggleButton value="4" disabled={!de.w || (de.m & 3) === 3}>
|
||||
<OptionIcon
|
||||
type="readonly"
|
||||
isSet={(de.m & DeviceEntityMask.DV_READONLY) === DeviceEntityMask.DV_READONLY}
|
||||
/>
|
||||
</ToggleButton>
|
||||
<ToggleButton value="2" disabled={de.n === ''}>
|
||||
<OptionIcon
|
||||
type="api_mqtt_exclude"
|
||||
isSet={
|
||||
(de.m & DeviceEntityMask.DV_API_MQTT_EXCLUDE) === DeviceEntityMask.DV_API_MQTT_EXCLUDE
|
||||
{!deviceEntity && (
|
||||
<ToggleButtonGroup
|
||||
size="small"
|
||||
color="secondary"
|
||||
value={getMaskString(de.m)}
|
||||
onChange={(event, mask) => {
|
||||
de.m = getMaskNumber(mask);
|
||||
if (de.n === '' && de.m & DeviceEntityMask.DV_READONLY) {
|
||||
de.m = de.m | DeviceEntityMask.DV_WEB_EXCLUDE;
|
||||
}
|
||||
/>
|
||||
</ToggleButton>
|
||||
<ToggleButton value="1" disabled={de.n === undefined}>
|
||||
<OptionIcon
|
||||
type="web_exclude"
|
||||
isSet={(de.m & DeviceEntityMask.DV_WEB_EXCLUDE) === DeviceEntityMask.DV_WEB_EXCLUDE}
|
||||
/>
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
if (de.m & DeviceEntityMask.DV_WEB_EXCLUDE) {
|
||||
de.m = de.m & ~DeviceEntityMask.DV_FAVORITE;
|
||||
}
|
||||
setMasks(['']);
|
||||
}}
|
||||
>
|
||||
<ToggleButton value="8" disabled={(de.m & 0x81) !== 0 || de.n === undefined}>
|
||||
<OptionIcon
|
||||
type="favorite"
|
||||
isSet={(de.m & DeviceEntityMask.DV_FAVORITE) === DeviceEntityMask.DV_FAVORITE}
|
||||
/>
|
||||
</ToggleButton>
|
||||
<ToggleButton value="4" disabled={!de.w || (de.m & 0x83) >= 3}>
|
||||
<OptionIcon
|
||||
type="readonly"
|
||||
isSet={(de.m & DeviceEntityMask.DV_READONLY) === DeviceEntityMask.DV_READONLY}
|
||||
/>
|
||||
</ToggleButton>
|
||||
<ToggleButton value="2" disabled={de.n === '' || (de.m & 0x80) !== 0}>
|
||||
<OptionIcon
|
||||
type="api_mqtt_exclude"
|
||||
isSet={
|
||||
(de.m & DeviceEntityMask.DV_API_MQTT_EXCLUDE) === DeviceEntityMask.DV_API_MQTT_EXCLUDE
|
||||
}
|
||||
/>
|
||||
</ToggleButton>
|
||||
<ToggleButton value="1" disabled={de.n === undefined || (de.m & 0x80) !== 0}>
|
||||
<OptionIcon
|
||||
type="web_exclude"
|
||||
isSet={(de.m & DeviceEntityMask.DV_WEB_EXCLUDE) === DeviceEntityMask.DV_WEB_EXCLUDE}
|
||||
/>
|
||||
</ToggleButton>
|
||||
<ToggleButton value="128">
|
||||
<OptionIcon
|
||||
type="deleted"
|
||||
isSet={(de.m & DeviceEntityMask.DV_DELETED) === DeviceEntityMask.DV_DELETED}
|
||||
/>
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
)}
|
||||
</Cell>
|
||||
<Cell>{formatName(de)}</Cell>
|
||||
<Cell>{formatValue(de.v)}</Cell>
|
||||
<Cell>{!deviceEntity && formatName(de)}</Cell>
|
||||
<Cell>{!deviceEntity && !(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.mi)}</Cell>
|
||||
<Cell>{!deviceEntity && !(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.ma)}</Cell>
|
||||
<Cell>{!deviceEntity && formatValue(de.v)}</Cell>
|
||||
</Row>
|
||||
))}
|
||||
</Body>
|
||||
@@ -503,14 +559,11 @@ const SettingsCustomization: FC = () => {
|
||||
|
||||
const renderResetDialog = () => (
|
||||
<Dialog open={confirmReset} onClose={() => setConfirmReset(false)}>
|
||||
<DialogTitle>Reset</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
Are you sure you want remove all customizations including the custom settings of the Temperature and Analog
|
||||
sensors?
|
||||
</DialogContent>
|
||||
<DialogTitle>{LL.RESET(1)}</DialogTitle>
|
||||
<DialogContent dividers>{LL.CUSTOMIZATIONS_RESET()}</DialogContent>
|
||||
<DialogActions>
|
||||
<Button startIcon={<CancelIcon />} variant="outlined" onClick={() => setConfirmReset(false)} color="secondary">
|
||||
Cancel
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<SettingsBackupRestoreIcon />}
|
||||
@@ -519,25 +572,32 @@ const SettingsCustomization: FC = () => {
|
||||
autoFocus
|
||||
color="error"
|
||||
>
|
||||
Reset
|
||||
{LL.RESET(0)}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
const content = () => {
|
||||
return (
|
||||
<>
|
||||
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
|
||||
Device Entities
|
||||
</Typography>
|
||||
{renderDeviceList()}
|
||||
{renderDeviceData()}
|
||||
const renderContent = () => (
|
||||
<>
|
||||
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
|
||||
{LL.DEVICE_ENTITIES()}
|
||||
</Typography>
|
||||
{renderDeviceList()}
|
||||
{renderDeviceData()}
|
||||
{restartNeeded && (
|
||||
<MessageBox my={2} level="warning" message={LL.RESTART_TEXT()}>
|
||||
<Button startIcon={<PowerSettingsNewIcon />} variant="contained" color="error" onClick={restart}>
|
||||
{LL.RESTART()}
|
||||
</Button>
|
||||
</MessageBox>
|
||||
)}
|
||||
{!restartNeeded && (
|
||||
<Box display="flex" flexWrap="wrap">
|
||||
<Box flexGrow={1}>
|
||||
<ButtonRow>
|
||||
<Button startIcon={<SaveIcon />} variant="outlined" color="primary" onClick={() => saveCustomization()}>
|
||||
Save
|
||||
{LL.SAVE()}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
</Box>
|
||||
@@ -548,18 +608,90 @@ const SettingsCustomization: FC = () => {
|
||||
color="error"
|
||||
onClick={() => setConfirmReset(true)}
|
||||
>
|
||||
Reset
|
||||
{LL.RESET(0)}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
</Box>
|
||||
{renderResetDialog()}
|
||||
</>
|
||||
);
|
||||
)}
|
||||
{renderResetDialog()}
|
||||
</>
|
||||
);
|
||||
|
||||
const renderEditDialog = () => {
|
||||
if (deviceEntity) {
|
||||
const de = deviceEntity;
|
||||
return (
|
||||
<Dialog open={!!deviceEntity} onClose={() => setDeviceEntity(undefined)}>
|
||||
<DialogTitle>{LL.EDIT() + ' ' + LL.ENTITY() + ' "' + de.id + '"'}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Box color="warning.main" mb={2}>
|
||||
<Typography variant="body2">
|
||||
{LL.DEFAULT(1) + ' ' + LL.NAME(1)}: {deviceEntity.n}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Grid container spacing={1}>
|
||||
<Grid item>
|
||||
<TextField
|
||||
name="cn"
|
||||
label={LL.NEW_NAME_OF(LL.ENTITY())}
|
||||
value={deviceEntity.cn}
|
||||
autoFocus
|
||||
sx={{ width: '30ch' }}
|
||||
onChange={updateValue(setDeviceEntity)}
|
||||
/>
|
||||
</Grid>
|
||||
{typeof de.v === 'number' && de.w && !(de.m & DeviceEntityMask.DV_READONLY) && (
|
||||
<>
|
||||
<Grid item>
|
||||
<TextField
|
||||
name="mi"
|
||||
label={LL.MIN()}
|
||||
value={deviceEntity.mi}
|
||||
sx={{ width: '8ch' }}
|
||||
onChange={updateValue(setDeviceEntity)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<TextField
|
||||
name="ma"
|
||||
label={LL.MAX()}
|
||||
value={deviceEntity.ma}
|
||||
sx={{ width: '8ch' }}
|
||||
onChange={updateValue(setDeviceEntity)}
|
||||
/>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
onClick={() => setDeviceEntity(undefined)}
|
||||
color="secondary"
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<SaveIcon />}
|
||||
variant="outlined"
|
||||
type="submit"
|
||||
onClick={() => updateEntity()}
|
||||
color="warning"
|
||||
>
|
||||
{LL.SAVE()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title="User Customization" titleGutter>
|
||||
{content()}
|
||||
<SectionContent title={LL.USER_CUSTOMIZATION()} titleGutter>
|
||||
{restarting ? <RestartMonitor /> : renderContent()}
|
||||
{renderEditDialog()}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
DeviceData,
|
||||
DeviceEntity,
|
||||
UniqueID,
|
||||
MaskedEntities,
|
||||
CustomEntities,
|
||||
WriteValue,
|
||||
WriteSensor,
|
||||
WriteAnalog,
|
||||
@@ -63,8 +63,8 @@ export function readDeviceEntities(unique_id: UniqueID): AxiosPromise<DeviceEnti
|
||||
return AXIOS_BIN.post('/deviceEntities', unique_id);
|
||||
}
|
||||
|
||||
export function writeMaskedEntities(maskedEntities: MaskedEntities): AxiosPromise<void> {
|
||||
return AXIOS.post('/maskedEntities', maskedEntities);
|
||||
export function writeCustomEntities(customEntities: CustomEntities): AxiosPromise<void> {
|
||||
return AXIOS.post('/customEntities', customEntities);
|
||||
}
|
||||
|
||||
export function writeValue(writevalue: WriteValue): AxiosPromise<void> {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export interface Settings {
|
||||
locale: string;
|
||||
tx_mode: number;
|
||||
ems_bus_id: number;
|
||||
syslog_enabled: boolean;
|
||||
@@ -32,6 +33,7 @@ export interface Settings {
|
||||
eth_power: number;
|
||||
eth_phy_addr: number;
|
||||
eth_clock_mode: number;
|
||||
platform: string;
|
||||
}
|
||||
|
||||
export enum busConnectionStatus {
|
||||
@@ -41,7 +43,7 @@ export enum busConnectionStatus {
|
||||
}
|
||||
|
||||
export interface Stat {
|
||||
id: string; // name
|
||||
id: string; // id - needs to be a string
|
||||
s: number; // success
|
||||
f: number; // fail
|
||||
q: number; // quality
|
||||
@@ -57,7 +59,8 @@ export interface Status {
|
||||
}
|
||||
export interface Device {
|
||||
id: string; // id index
|
||||
t: string; // type
|
||||
tn: string; // device type translated name
|
||||
t: number; // device type id
|
||||
b: string; // brand
|
||||
n: string; // name
|
||||
d: number; // deviceid
|
||||
@@ -99,6 +102,7 @@ export interface SensorData {
|
||||
export interface CoreData {
|
||||
connected: boolean;
|
||||
devices: Device[];
|
||||
s_n: string;
|
||||
active_sensors: number;
|
||||
analog_enabled: boolean;
|
||||
}
|
||||
@@ -108,7 +112,8 @@ export interface DeviceShort {
|
||||
d?: number; // deviceid
|
||||
p?: number; // productid
|
||||
s: string; // shortname
|
||||
t?: string; // device type name
|
||||
t?: number; // device type id
|
||||
tn?: string; // device type internal name
|
||||
}
|
||||
|
||||
export interface Devices {
|
||||
@@ -136,12 +141,18 @@ export interface DeviceEntity {
|
||||
id: string; // shortname
|
||||
v?: any; // value, in any format, optional
|
||||
n?: string; // fullname, optional
|
||||
cn?: string; // custom fullname, optional
|
||||
m: number; // mask
|
||||
om?: number; // original mask before edits
|
||||
o_m?: number; // original mask before edits
|
||||
o_cn?: string; // original cn before edits
|
||||
w: boolean; // writeable
|
||||
mi?: string; // min value
|
||||
ma?: string; // max value
|
||||
o_mi?: string;
|
||||
o_ma?: string;
|
||||
}
|
||||
|
||||
export interface MaskedEntities {
|
||||
export interface CustomEntities {
|
||||
id: number;
|
||||
entity_ids: string[];
|
||||
}
|
||||
@@ -171,7 +182,9 @@ export enum DeviceValueUOM {
|
||||
MV,
|
||||
SQM,
|
||||
M3,
|
||||
L
|
||||
L,
|
||||
KMIN,
|
||||
K
|
||||
}
|
||||
|
||||
export const DeviceValueUOM_s = [
|
||||
@@ -184,18 +197,20 @@ export const DeviceValueUOM_s = [
|
||||
'Wh',
|
||||
'hours',
|
||||
'minutes',
|
||||
'uA',
|
||||
'µA',
|
||||
'bar',
|
||||
'kW',
|
||||
'W',
|
||||
'KB',
|
||||
'second',
|
||||
'seconds',
|
||||
'dBm',
|
||||
'°F',
|
||||
'mV',
|
||||
'sqm',
|
||||
'm3',
|
||||
'l'
|
||||
'm²',
|
||||
'm³',
|
||||
'l',
|
||||
'K*min',
|
||||
'K'
|
||||
];
|
||||
|
||||
export enum AnalogType {
|
||||
@@ -235,7 +250,10 @@ export const BOARD_PROFILES: BoardProfiles = {
|
||||
'MH-ET': 'MH-ET Live D1 Mini',
|
||||
LOLIN: 'Lolin D32',
|
||||
OLIMEX: 'Olimex ESP32-EVB',
|
||||
OLIMEXPOE: 'Olimex ESP32-POE'
|
||||
OLIMEXPOE: 'Olimex ESP32-POE',
|
||||
C3MINI: 'Wemos C3 Mini',
|
||||
S2MINI: 'Wemos S2 Mini',
|
||||
S3MINI: 'Liligo S3'
|
||||
};
|
||||
|
||||
export interface BoardProfileName {
|
||||
@@ -280,5 +298,6 @@ export enum DeviceEntityMask {
|
||||
DV_WEB_EXCLUDE = 1,
|
||||
DV_API_MQTT_EXCLUDE = 2,
|
||||
DV_READONLY = 4,
|
||||
DV_FAVORITE = 8
|
||||
DV_FAVORITE = 8,
|
||||
DV_DELETED = 128
|
||||
}
|
||||
|
||||
@@ -7,12 +7,13 @@ export const GPIO_VALIDATOR = {
|
||||
if (
|
||||
value &&
|
||||
(value === 1 ||
|
||||
(value >= 6 && value <= 12) ||
|
||||
(value >= 10 && value <= 12) ||
|
||||
(value >= 14 && value <= 15) ||
|
||||
value === 20 ||
|
||||
value === 24 ||
|
||||
(value >= 28 && value <= 31) ||
|
||||
value > 40)
|
||||
value > 40 ||
|
||||
value < 0)
|
||||
) {
|
||||
callback('Must be an valid GPIO port');
|
||||
} else {
|
||||
@@ -21,24 +22,61 @@ export const GPIO_VALIDATOR = {
|
||||
}
|
||||
};
|
||||
|
||||
export const GPIO_VALIDATORC3 = {
|
||||
validator(rule: InternalRuleItem, value: number, callback: (error?: string) => void) {
|
||||
if (value && ((value >= 11 && value <= 19) || value > 21 || value < 0)) {
|
||||
callback('Must be an valid GPIO port');
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const GPIO_VALIDATORS2 = {
|
||||
validator(rule: InternalRuleItem, value: number, callback: (error?: string) => void) {
|
||||
if (value && ((value >= 19 && value <= 20) || (value >= 22 && value <= 32) || value > 40 || value < 0)) {
|
||||
callback('Must be an valid GPIO port');
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const createSettingsValidator = (settings: Settings) =>
|
||||
new Schema({
|
||||
...(settings.board_profile === 'CUSTOM' && {
|
||||
led_gpio: [{ required: true, message: 'LED GPIO is required' }, GPIO_VALIDATOR],
|
||||
dallas_gpio: [{ required: true, message: 'GPIO is required' }, GPIO_VALIDATOR],
|
||||
pbutton_gpio: [{ required: true, message: 'Button GPIO is required' }, GPIO_VALIDATOR],
|
||||
tx_gpio: [{ required: true, message: 'Tx GPIO is required' }, GPIO_VALIDATOR],
|
||||
rx_gpio: [{ required: true, message: 'Rx GPIO is required' }, GPIO_VALIDATOR]
|
||||
}),
|
||||
...(settings.board_profile === 'CUSTOM' &&
|
||||
settings.platform === 'ESP32' && {
|
||||
led_gpio: [{ required: true, message: 'LED GPIO is required' }, GPIO_VALIDATOR],
|
||||
dallas_gpio: [{ required: true, message: 'GPIO is required' }, GPIO_VALIDATOR],
|
||||
pbutton_gpio: [{ required: true, message: 'Button GPIO is required' }, GPIO_VALIDATOR],
|
||||
tx_gpio: [{ required: true, message: 'Tx GPIO is required' }, GPIO_VALIDATOR],
|
||||
rx_gpio: [{ required: true, message: 'Rx GPIO is required' }, GPIO_VALIDATOR]
|
||||
}),
|
||||
...(settings.board_profile === 'CUSTOM' &&
|
||||
settings.platform === 'ESP32-C3' && {
|
||||
led_gpio: [{ required: true, message: 'LED GPIO is required' }, GPIO_VALIDATORC3],
|
||||
dallas_gpio: [{ required: true, message: 'GPIO is required' }, GPIO_VALIDATORC3],
|
||||
pbutton_gpio: [{ required: true, message: 'Button GPIO is required' }, GPIO_VALIDATORC3],
|
||||
tx_gpio: [{ required: true, message: 'Tx GPIO is required' }, GPIO_VALIDATORC3],
|
||||
rx_gpio: [{ required: true, message: 'Rx GPIO is required' }, GPIO_VALIDATORC3]
|
||||
}),
|
||||
...(settings.board_profile === 'CUSTOM' &&
|
||||
settings.platform === 'ESP32-S2' && {
|
||||
led_gpio: [{ required: true, message: 'LED GPIO is required' }, GPIO_VALIDATORS2],
|
||||
dallas_gpio: [{ required: true, message: 'GPIO is required' }, GPIO_VALIDATORS2],
|
||||
pbutton_gpio: [{ required: true, message: 'Button GPIO is required' }, GPIO_VALIDATORS2],
|
||||
tx_gpio: [{ required: true, message: 'Tx GPIO is required' }, GPIO_VALIDATORS2],
|
||||
rx_gpio: [{ required: true, message: 'Rx GPIO is required' }, GPIO_VALIDATORS2]
|
||||
}),
|
||||
...(settings.syslog_enabled && {
|
||||
syslog_host: [{ required: true, message: 'Host is required' }, IP_OR_HOSTNAME_VALIDATOR],
|
||||
syslog_port: [
|
||||
{ required: true, message: 'Port is required' },
|
||||
{ type: 'number', min: 0, max: 65535, message: 'Port must be between 0 and 65535' }
|
||||
{ type: 'number', min: 0, max: 65535, message: 'Invalid Port' }
|
||||
],
|
||||
syslog_mark_interval: [
|
||||
{ required: true, message: 'Mark interval is required' },
|
||||
{ type: 'number', min: 0, max: 10, message: 'Port must be between 0 and 10' }
|
||||
{ type: 'number', min: 0, max: 10, message: ' must be between 0 and 10' }
|
||||
]
|
||||
}),
|
||||
...(settings.shower_alert && {
|
||||
|
||||
@@ -15,6 +15,8 @@ export interface MqttStatus {
|
||||
client_id: string;
|
||||
disconnect_reason: MqttDisconnectReason;
|
||||
mqtt_fails: number;
|
||||
mqtt_queued: number;
|
||||
connect_count: number;
|
||||
}
|
||||
|
||||
export interface MqttSettings {
|
||||
@@ -27,13 +29,14 @@ export interface MqttSettings {
|
||||
client_id: string;
|
||||
keep_alive: number;
|
||||
clean_session: boolean;
|
||||
max_topic_length: number;
|
||||
entity_format: number;
|
||||
publish_time_boiler: number;
|
||||
publish_time_thermostat: number;
|
||||
publish_time_solar: number;
|
||||
publish_time_mixer: number;
|
||||
publish_time_other: number;
|
||||
publish_time_sensor: number;
|
||||
publish_time_heartbeat: number;
|
||||
mqtt_qos: number;
|
||||
mqtt_retain: boolean;
|
||||
ha_enabled: boolean;
|
||||
|
||||
@@ -48,6 +48,8 @@ export interface NetworkSettings {
|
||||
dns_ip_1?: string;
|
||||
dns_ip_2?: string;
|
||||
enableMDNS: boolean;
|
||||
enableCORS: boolean;
|
||||
CORSOrigin: string;
|
||||
}
|
||||
|
||||
export interface WiFiNetworkList {
|
||||
|
||||
@@ -1,36 +1,23 @@
|
||||
export enum EspPlatform {
|
||||
ESP8266 = 'esp8266',
|
||||
ESP32 = 'esp32'
|
||||
}
|
||||
|
||||
interface ESPSystemStatus {
|
||||
export interface SystemStatus {
|
||||
emsesp_version: string;
|
||||
esp_platform: EspPlatform;
|
||||
esp_platform: string;
|
||||
max_alloc_heap: number;
|
||||
cpu_freq_mhz: number;
|
||||
free_heap: number;
|
||||
sdk_version: string;
|
||||
flash_chip_size: number;
|
||||
flash_chip_speed: number;
|
||||
app_used: number;
|
||||
app_free: number;
|
||||
fs_used: number;
|
||||
fs_total: number;
|
||||
fs_free: number;
|
||||
uptime: string;
|
||||
free_mem: number;
|
||||
psram_size?: number;
|
||||
free_psram?: number;
|
||||
has_loader: boolean;
|
||||
}
|
||||
|
||||
export interface ESP32SystemStatus extends ESPSystemStatus {
|
||||
esp_platform: EspPlatform.ESP32;
|
||||
psram_size: number;
|
||||
free_psram: number;
|
||||
}
|
||||
|
||||
export interface ESP8266SystemStatus extends ESPSystemStatus {
|
||||
esp_platform: EspPlatform.ESP8266;
|
||||
heap_fragmentation: number;
|
||||
}
|
||||
|
||||
export type SystemStatus = ESP8266SystemStatus | ESP32SystemStatus;
|
||||
|
||||
export interface OTASettings {
|
||||
enabled: boolean;
|
||||
port: number;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { AxiosError } from 'axios';
|
||||
|
||||
export const extractErrorMessage = (error: unknown, defaultMessage: string) => {
|
||||
if (error instanceof AxiosError) {
|
||||
return defaultMessage + ' (' + error.request.statusText + ')';
|
||||
export const extractErrorMessage = (error: any, defaultMessage: string) => {
|
||||
if (error.request) {
|
||||
return defaultMessage + ' (' + error.request.status + ': ' + error.request.statusText + ')';
|
||||
} else if (error instanceof Error) {
|
||||
return defaultMessage + ' (' + error.message + ')';
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import parseMilliseconds from 'parse-ms';
|
||||
|
||||
const LOCALE_FORMAT = new Intl.DateTimeFormat([...window.navigator.languages], {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
@@ -21,21 +19,6 @@ export const formatLocalDateTime = (date: Date) => {
|
||||
export const pluralize = (count: number, noun: string) =>
|
||||
`${Intl.NumberFormat().format(count)} ${noun}${count !== 1 ? 's' : ''}`;
|
||||
|
||||
export const formatDurationMin = (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;
|
||||
};
|
||||
|
||||
export const formatDurationSec = (duration_sec: number) => {
|
||||
if (duration_sec === 0) {
|
||||
return ' ';
|
||||
|
||||
@@ -4,12 +4,16 @@ import { AxiosPromise } from 'axios';
|
||||
|
||||
import { extractErrorMessage } from '.';
|
||||
|
||||
import { useI18nContext } from '../i18n/i18n-react';
|
||||
|
||||
export interface RestRequestOptions<D> {
|
||||
read: () => AxiosPromise<D>;
|
||||
update?: (value: D) => AxiosPromise<D>;
|
||||
}
|
||||
|
||||
export const useRest = <D>({ read, update }: RestRequestOptions<D>) => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const [saving, setSaving] = useState<boolean>(false);
|
||||
@@ -22,12 +26,12 @@ export const useRest = <D>({ read, update }: RestRequestOptions<D>) => {
|
||||
setErrorMessage(undefined);
|
||||
try {
|
||||
setData((await read()).data);
|
||||
} catch (error: unknown) {
|
||||
const message = extractErrorMessage(error, 'Problem loading data');
|
||||
} catch (error) {
|
||||
const message = extractErrorMessage(error, LL.PROBLEM_LOADING());
|
||||
enqueueSnackbar(message, { variant: 'error' });
|
||||
setErrorMessage(message);
|
||||
}
|
||||
}, [read, enqueueSnackbar]);
|
||||
}, [read, enqueueSnackbar, LL]);
|
||||
|
||||
const save = useCallback(
|
||||
async (toSave: D) => {
|
||||
@@ -43,17 +47,17 @@ export const useRest = <D>({ read, update }: RestRequestOptions<D>) => {
|
||||
if (response.status === 202) {
|
||||
setRestartNeeded(true);
|
||||
} else {
|
||||
enqueueSnackbar('Settings saved', { variant: 'success' });
|
||||
enqueueSnackbar(LL.SETTINGS_OF('') + ' ' + LL.SAVED(), { variant: 'success' });
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const message = extractErrorMessage(error, 'Problem saving data');
|
||||
} catch (error) {
|
||||
const message = extractErrorMessage(error, LL.PROBLEM_UPDATING());
|
||||
enqueueSnackbar(message, { variant: 'error' });
|
||||
setErrorMessage(message);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
},
|
||||
[update, enqueueSnackbar]
|
||||
[update, enqueueSnackbar, LL]
|
||||
);
|
||||
|
||||
const saveData = () => data && save(data);
|
||||
|
||||
@@ -2,11 +2,9 @@ import Schema from 'async-validator';
|
||||
|
||||
export const SIGN_IN_REQUEST_VALIDATOR = new Schema({
|
||||
username: {
|
||||
required: true,
|
||||
message: 'Please provide a username'
|
||||
required: true
|
||||
},
|
||||
password: {
|
||||
required: true,
|
||||
message: 'Please provide a password'
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
import Schema from 'async-validator';
|
||||
import { MqttSettings } from '../types';
|
||||
import { IP_OR_HOSTNAME_VALIDATOR } from './shared';
|
||||
|
||||
export const MQTT_SETTINGS_VALIDATOR = new Schema({
|
||||
host: [{ required: true, message: 'Host is required' }, IP_OR_HOSTNAME_VALIDATOR],
|
||||
port: [
|
||||
{ required: true, message: 'Port is required' },
|
||||
{ type: 'number', min: 0, max: 65535, message: 'Port must be between 0 and 65535' }
|
||||
],
|
||||
keep_alive: [
|
||||
{ required: true, message: 'Keep alive is required' },
|
||||
{ type: 'number', min: 1, max: 86400, message: 'Keep alive must be between 1 and 86400' }
|
||||
],
|
||||
max_topic_length: [
|
||||
{ required: true, message: 'Max topic length is required' },
|
||||
{ type: 'number', min: 16, max: 1024, message: 'Max topic length must be between 16 and 1024' }
|
||||
]
|
||||
});
|
||||
export const createMqttSettingsValidator = (mqttSettings: MqttSettings) =>
|
||||
new Schema({
|
||||
...(mqttSettings.enabled && {
|
||||
host: [{ required: true, message: 'Host is required' }, IP_OR_HOSTNAME_VALIDATOR],
|
||||
base: { required: true, message: 'Base is required' },
|
||||
port: [
|
||||
{ required: true, message: 'Port is required' },
|
||||
{ type: 'number', min: 0, max: 65535, message: 'Invalid Port' }
|
||||
],
|
||||
keep_alive: [
|
||||
{ required: true, message: 'Keep alive is required' },
|
||||
{ type: 'number', min: 1, max: 86400, message: 'Keep alive must be between 1 and 86400' }
|
||||
],
|
||||
publish_time_heartbeat: [
|
||||
{ required: true, message: 'Heartbeat is required' },
|
||||
{ type: 'number', min: 10, max: 86400, message: 'Heartbeat must be between 10 and 86400' }
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"useUnknownInCatchVariables": false,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src"]
|
||||
|
||||
3440
lib/Adafruit_NeoPixel/Adafruit_NeoPixel.cpp
Normal file
410
lib/Adafruit_NeoPixel/Adafruit_NeoPixel.h
Normal file
@@ -0,0 +1,410 @@
|
||||
/*!
|
||||
* @file Adafruit_NeoPixel.h
|
||||
*
|
||||
* This is part of Adafruit's NeoPixel library for the Arduino platform,
|
||||
* allowing a broad range of microcontroller boards (most AVR boards,
|
||||
* many ARM devices, ESP8266 and ESP32, among others) to control Adafruit
|
||||
* NeoPixels, FLORA RGB Smart Pixels and compatible devices -- WS2811,
|
||||
* WS2812, WS2812B, SK6812, etc.
|
||||
*
|
||||
* Adafruit invests time and resources providing this open source code,
|
||||
* please support Adafruit and open-source hardware by purchasing products
|
||||
* from Adafruit!
|
||||
*
|
||||
* Written by Phil "Paint Your Dragon" Burgess for Adafruit Industries,
|
||||
* with contributions by PJRC, Michael Miller and other members of the
|
||||
* open source community.
|
||||
*
|
||||
* This file is part of the Adafruit_NeoPixel library.
|
||||
*
|
||||
* Adafruit_NeoPixel is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Adafruit_NeoPixel is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with NeoPixel. If not, see
|
||||
* <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef ADAFRUIT_NEOPIXEL_H
|
||||
#define ADAFRUIT_NEOPIXEL_H
|
||||
|
||||
#ifdef ARDUINO
|
||||
#if (ARDUINO >= 100)
|
||||
#include <Arduino.h>
|
||||
#else
|
||||
#include <WProgram.h>
|
||||
#include <pins_arduino.h>
|
||||
#endif
|
||||
|
||||
#ifdef USE_TINYUSB // For Serial when selecting TinyUSB
|
||||
#include <Adafruit_TinyUSB.h>
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
#ifdef TARGET_LPC1768
|
||||
#include <Arduino.h>
|
||||
#endif
|
||||
|
||||
#if defined(ARDUINO_ARCH_RP2040)
|
||||
#include <stdlib.h>
|
||||
#include "hardware/pio.h"
|
||||
#include "hardware/clocks.h"
|
||||
#include "rp2040_pio.h"
|
||||
#endif
|
||||
|
||||
// The order of primary colors in the NeoPixel data stream can vary among
|
||||
// device types, manufacturers and even different revisions of the same
|
||||
// item. The third parameter to the Adafruit_NeoPixel constructor encodes
|
||||
// the per-pixel byte offsets of the red, green and blue primaries (plus
|
||||
// white, if present) in the data stream -- the following #defines provide
|
||||
// an easier-to-use named version for each permutation. e.g. NEO_GRB
|
||||
// indicates a NeoPixel-compatible device expecting three bytes per pixel,
|
||||
// with the first byte transmitted containing the green value, second
|
||||
// containing red and third containing blue. The in-memory representation
|
||||
// of a chain of NeoPixels is the same as the data-stream order; no
|
||||
// re-ordering of bytes is required when issuing data to the chain.
|
||||
// Most of these values won't exist in real-world devices, but it's done
|
||||
// this way so we're ready for it (also, if using the WS2811 driver IC,
|
||||
// one might have their pixels set up in any weird permutation).
|
||||
|
||||
// Bits 5,4 of this value are the offset (0-3) from the first byte of a
|
||||
// pixel to the location of the red color byte. Bits 3,2 are the green
|
||||
// offset and 1,0 are the blue offset. If it is an RGBW-type device
|
||||
// (supporting a white primary in addition to R,G,B), bits 7,6 are the
|
||||
// offset to the white byte...otherwise, bits 7,6 are set to the same value
|
||||
// as 5,4 (red) to indicate an RGB (not RGBW) device.
|
||||
// i.e. binary representation:
|
||||
// 0bWWRRGGBB for RGBW devices
|
||||
// 0bRRRRGGBB for RGB
|
||||
|
||||
// RGB NeoPixel permutations; white and red offsets are always same
|
||||
// Offset: W R G B
|
||||
#define NEO_RGB ((0 << 6) | (0 << 4) | (1 << 2) | (2)) ///< Transmit as R,G,B
|
||||
#define NEO_RBG ((0 << 6) | (0 << 4) | (2 << 2) | (1)) ///< Transmit as R,B,G
|
||||
#define NEO_GRB ((1 << 6) | (1 << 4) | (0 << 2) | (2)) ///< Transmit as G,R,B
|
||||
#define NEO_GBR ((2 << 6) | (2 << 4) | (0 << 2) | (1)) ///< Transmit as G,B,R
|
||||
#define NEO_BRG ((1 << 6) | (1 << 4) | (2 << 2) | (0)) ///< Transmit as B,R,G
|
||||
#define NEO_BGR ((2 << 6) | (2 << 4) | (1 << 2) | (0)) ///< Transmit as B,G,R
|
||||
|
||||
// RGBW NeoPixel permutations; all 4 offsets are distinct
|
||||
// Offset: W R G B
|
||||
#define NEO_WRGB ((0 << 6) | (1 << 4) | (2 << 2) | (3)) ///< Transmit as W,R,G,B
|
||||
#define NEO_WRBG ((0 << 6) | (1 << 4) | (3 << 2) | (2)) ///< Transmit as W,R,B,G
|
||||
#define NEO_WGRB ((0 << 6) | (2 << 4) | (1 << 2) | (3)) ///< Transmit as W,G,R,B
|
||||
#define NEO_WGBR ((0 << 6) | (3 << 4) | (1 << 2) | (2)) ///< Transmit as W,G,B,R
|
||||
#define NEO_WBRG ((0 << 6) | (2 << 4) | (3 << 2) | (1)) ///< Transmit as W,B,R,G
|
||||
#define NEO_WBGR ((0 << 6) | (3 << 4) | (2 << 2) | (1)) ///< Transmit as W,B,G,R
|
||||
|
||||
#define NEO_RWGB ((1 << 6) | (0 << 4) | (2 << 2) | (3)) ///< Transmit as R,W,G,B
|
||||
#define NEO_RWBG ((1 << 6) | (0 << 4) | (3 << 2) | (2)) ///< Transmit as R,W,B,G
|
||||
#define NEO_RGWB ((2 << 6) | (0 << 4) | (1 << 2) | (3)) ///< Transmit as R,G,W,B
|
||||
#define NEO_RGBW ((3 << 6) | (0 << 4) | (1 << 2) | (2)) ///< Transmit as R,G,B,W
|
||||
#define NEO_RBWG ((2 << 6) | (0 << 4) | (3 << 2) | (1)) ///< Transmit as R,B,W,G
|
||||
#define NEO_RBGW ((3 << 6) | (0 << 4) | (2 << 2) | (1)) ///< Transmit as R,B,G,W
|
||||
|
||||
#define NEO_GWRB ((1 << 6) | (2 << 4) | (0 << 2) | (3)) ///< Transmit as G,W,R,B
|
||||
#define NEO_GWBR ((1 << 6) | (3 << 4) | (0 << 2) | (2)) ///< Transmit as G,W,B,R
|
||||
#define NEO_GRWB ((2 << 6) | (1 << 4) | (0 << 2) | (3)) ///< Transmit as G,R,W,B
|
||||
#define NEO_GRBW ((3 << 6) | (1 << 4) | (0 << 2) | (2)) ///< Transmit as G,R,B,W
|
||||
#define NEO_GBWR ((2 << 6) | (3 << 4) | (0 << 2) | (1)) ///< Transmit as G,B,W,R
|
||||
#define NEO_GBRW ((3 << 6) | (2 << 4) | (0 << 2) | (1)) ///< Transmit as G,B,R,W
|
||||
|
||||
#define NEO_BWRG ((1 << 6) | (2 << 4) | (3 << 2) | (0)) ///< Transmit as B,W,R,G
|
||||
#define NEO_BWGR ((1 << 6) | (3 << 4) | (2 << 2) | (0)) ///< Transmit as B,W,G,R
|
||||
#define NEO_BRWG ((2 << 6) | (1 << 4) | (3 << 2) | (0)) ///< Transmit as B,R,W,G
|
||||
#define NEO_BRGW ((3 << 6) | (1 << 4) | (2 << 2) | (0)) ///< Transmit as B,R,G,W
|
||||
#define NEO_BGWR ((2 << 6) | (3 << 4) | (1 << 2) | (0)) ///< Transmit as B,G,W,R
|
||||
#define NEO_BGRW ((3 << 6) | (2 << 4) | (1 << 2) | (0)) ///< Transmit as B,G,R,W
|
||||
|
||||
// Add NEO_KHZ400 to the color order value to indicate a 400 KHz device.
|
||||
// All but the earliest v1 NeoPixels expect an 800 KHz data stream, this is
|
||||
// the default if unspecified. Because flash space is very limited on ATtiny
|
||||
// devices (e.g. Trinket, Gemma), v1 NeoPixels aren't handled by default on
|
||||
// those chips, though it can be enabled by removing the ifndef/endif below,
|
||||
// but code will be bigger. Conversely, can disable the NEO_KHZ400 line on
|
||||
// other MCUs to remove v1 support and save a little space.
|
||||
|
||||
#define NEO_KHZ800 0x0000 ///< 800 KHz data transmission
|
||||
#ifndef __AVR_ATtiny85__
|
||||
#define NEO_KHZ400 0x0100 ///< 400 KHz data transmission
|
||||
#endif
|
||||
|
||||
// If 400 KHz support is enabled, the third parameter to the constructor
|
||||
// requires a 16-bit value (in order to select 400 vs 800 KHz speed).
|
||||
// If only 800 KHz is enabled (as is default on ATtiny), an 8-bit value
|
||||
// is sufficient to encode pixel color order, saving some space.
|
||||
|
||||
#ifdef NEO_KHZ400
|
||||
typedef uint16_t neoPixelType; ///< 3rd arg to Adafruit_NeoPixel constructor
|
||||
#else
|
||||
typedef uint8_t neoPixelType; ///< 3rd arg to Adafruit_NeoPixel constructor
|
||||
#endif
|
||||
|
||||
// These two tables are declared outside the Adafruit_NeoPixel class
|
||||
// because some boards may require oldschool compilers that don't
|
||||
// handle the C++11 constexpr keyword.
|
||||
|
||||
/* A PROGMEM (flash mem) table containing 8-bit unsigned sine wave (0-255).
|
||||
Copy & paste this snippet into a Python REPL to regenerate:
|
||||
import math
|
||||
for x in range(256):
|
||||
print("{:3},".format(int((math.sin(x/128.0*math.pi)+1.0)*127.5+0.5))),
|
||||
if x&15 == 15: print
|
||||
*/
|
||||
static const uint8_t PROGMEM _NeoPixelSineTable[256] = {
|
||||
128, 131, 134, 137, 140, 143, 146, 149, 152, 155, 158, 162, 165, 167, 170,
|
||||
173, 176, 179, 182, 185, 188, 190, 193, 196, 198, 201, 203, 206, 208, 211,
|
||||
213, 215, 218, 220, 222, 224, 226, 228, 230, 232, 234, 235, 237, 238, 240,
|
||||
241, 243, 244, 245, 246, 248, 249, 250, 250, 251, 252, 253, 253, 254, 254,
|
||||
254, 255, 255, 255, 255, 255, 255, 255, 254, 254, 254, 253, 253, 252, 251,
|
||||
250, 250, 249, 248, 246, 245, 244, 243, 241, 240, 238, 237, 235, 234, 232,
|
||||
230, 228, 226, 224, 222, 220, 218, 215, 213, 211, 208, 206, 203, 201, 198,
|
||||
196, 193, 190, 188, 185, 182, 179, 176, 173, 170, 167, 165, 162, 158, 155,
|
||||
152, 149, 146, 143, 140, 137, 134, 131, 128, 124, 121, 118, 115, 112, 109,
|
||||
106, 103, 100, 97, 93, 90, 88, 85, 82, 79, 76, 73, 70, 67, 65,
|
||||
62, 59, 57, 54, 52, 49, 47, 44, 42, 40, 37, 35, 33, 31, 29,
|
||||
27, 25, 23, 21, 20, 18, 17, 15, 14, 12, 11, 10, 9, 7, 6,
|
||||
5, 5, 4, 3, 2, 2, 1, 1, 1, 0, 0, 0, 0, 0, 0,
|
||||
0, 1, 1, 1, 2, 2, 3, 4, 5, 5, 6, 7, 9, 10, 11,
|
||||
12, 14, 15, 17, 18, 20, 21, 23, 25, 27, 29, 31, 33, 35, 37,
|
||||
40, 42, 44, 47, 49, 52, 54, 57, 59, 62, 65, 67, 70, 73, 76,
|
||||
79, 82, 85, 88, 90, 93, 97, 100, 103, 106, 109, 112, 115, 118, 121,
|
||||
124};
|
||||
|
||||
/* Similar to above, but for an 8-bit gamma-correction table.
|
||||
Copy & paste this snippet into a Python REPL to regenerate:
|
||||
import math
|
||||
gamma=2.6
|
||||
for x in range(256):
|
||||
print("{:3},".format(int(math.pow((x)/255.0,gamma)*255.0+0.5))),
|
||||
if x&15 == 15: print
|
||||
*/
|
||||
static const uint8_t PROGMEM _NeoPixelGammaTable[256] = {
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3,
|
||||
3, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 5, 6,
|
||||
6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 10,
|
||||
11, 11, 11, 12, 12, 13, 13, 13, 14, 14, 15, 15, 16, 16, 17,
|
||||
17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, 23, 24, 24, 25,
|
||||
25, 26, 27, 27, 28, 29, 29, 30, 31, 31, 32, 33, 34, 34, 35,
|
||||
36, 37, 38, 38, 39, 40, 41, 42, 42, 43, 44, 45, 46, 47, 48,
|
||||
49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63,
|
||||
64, 65, 66, 68, 69, 70, 71, 72, 73, 75, 76, 77, 78, 80, 81,
|
||||
82, 84, 85, 86, 88, 89, 90, 92, 93, 94, 96, 97, 99, 100, 102,
|
||||
103, 105, 106, 108, 109, 111, 112, 114, 115, 117, 119, 120, 122, 124, 125,
|
||||
127, 129, 130, 132, 134, 136, 137, 139, 141, 143, 145, 146, 148, 150, 152,
|
||||
154, 156, 158, 160, 162, 164, 166, 168, 170, 172, 174, 176, 178, 180, 182,
|
||||
184, 186, 188, 191, 193, 195, 197, 199, 202, 204, 206, 209, 211, 213, 215,
|
||||
218, 220, 223, 225, 227, 230, 232, 235, 237, 240, 242, 245, 247, 250, 252,
|
||||
255};
|
||||
|
||||
/*!
|
||||
@brief Class that stores state and functions for interacting with
|
||||
Adafruit NeoPixels and compatible devices.
|
||||
*/
|
||||
class Adafruit_NeoPixel {
|
||||
|
||||
public:
|
||||
// Constructor: number of LEDs, pin number, LED type
|
||||
Adafruit_NeoPixel(uint16_t n, int16_t pin = 6,
|
||||
neoPixelType type = NEO_GRB + NEO_KHZ800);
|
||||
Adafruit_NeoPixel(void);
|
||||
~Adafruit_NeoPixel();
|
||||
|
||||
void begin(void);
|
||||
void show(void);
|
||||
void setPin(int16_t p);
|
||||
void setPixelColor(uint16_t n, uint8_t r, uint8_t g, uint8_t b);
|
||||
void setPixelColor(uint16_t n, uint8_t r, uint8_t g, uint8_t b, uint8_t w);
|
||||
void setPixelColor(uint16_t n, uint32_t c);
|
||||
void fill(uint32_t c = 0, uint16_t first = 0, uint16_t count = 0);
|
||||
void setBrightness(uint8_t);
|
||||
void clear(void);
|
||||
void updateLength(uint16_t n);
|
||||
void updateType(neoPixelType t);
|
||||
/*!
|
||||
@brief Check whether a call to show() will start sending data
|
||||
immediately or will 'block' for a required interval. NeoPixels
|
||||
require a short quiet time (about 300 microseconds) after the
|
||||
last bit is received before the data 'latches' and new data can
|
||||
start being received. Usually one's sketch is implicitly using
|
||||
this time to generate a new frame of animation...but if it
|
||||
finishes very quickly, this function could be used to see if
|
||||
there's some idle time available for some low-priority
|
||||
concurrent task.
|
||||
@return 1 or true if show() will start sending immediately, 0 or false
|
||||
if show() would block (meaning some idle time is available).
|
||||
*/
|
||||
bool canShow(void) {
|
||||
// It's normal and possible for endTime to exceed micros() if the
|
||||
// 32-bit clock counter has rolled over (about every 70 minutes).
|
||||
// Since both are uint32_t, a negative delta correctly maps back to
|
||||
// positive space, and it would seem like the subtraction below would
|
||||
// suffice. But a problem arises if code invokes show() very
|
||||
// infrequently...the micros() counter may roll over MULTIPLE times in
|
||||
// that interval, the delta calculation is no longer correct and the
|
||||
// next update may stall for a very long time. The check below resets
|
||||
// the latch counter if a rollover has occurred. This can cause an
|
||||
// extra delay of up to 300 microseconds in the rare case where a
|
||||
// show() call happens precisely around the rollover, but that's
|
||||
// neither likely nor especially harmful, vs. other code that might
|
||||
// stall for 30+ minutes, or having to document and frequently remind
|
||||
// and/or provide tech support explaining an unintuitive need for
|
||||
// show() calls at least once an hour.
|
||||
uint32_t now = micros();
|
||||
if (endTime > now) {
|
||||
endTime = now;
|
||||
}
|
||||
return (now - endTime) >= 300L;
|
||||
}
|
||||
/*!
|
||||
@brief Get a pointer directly to the NeoPixel data buffer in RAM.
|
||||
Pixel data is stored in a device-native format (a la the NEO_*
|
||||
constants) and is not translated here. Applications that access
|
||||
this buffer will need to be aware of the specific data format
|
||||
and handle colors appropriately.
|
||||
@return Pointer to NeoPixel buffer (uint8_t* array).
|
||||
@note This is for high-performance applications where calling
|
||||
setPixelColor() on every single pixel would be too slow (e.g.
|
||||
POV or light-painting projects). There is no bounds checking
|
||||
on the array, creating tremendous potential for mayhem if one
|
||||
writes past the ends of the buffer. Great power, great
|
||||
responsibility and all that.
|
||||
*/
|
||||
uint8_t *getPixels(void) const { return pixels; };
|
||||
uint8_t getBrightness(void) const;
|
||||
/*!
|
||||
@brief Retrieve the pin number used for NeoPixel data output.
|
||||
@return Arduino pin number (-1 if not set).
|
||||
*/
|
||||
int16_t getPin(void) const { return pin; };
|
||||
/*!
|
||||
@brief Return the number of pixels in an Adafruit_NeoPixel strip object.
|
||||
@return Pixel count (0 if not set).
|
||||
*/
|
||||
uint16_t numPixels(void) const { return numLEDs; }
|
||||
uint32_t getPixelColor(uint16_t n) const;
|
||||
/*!
|
||||
@brief An 8-bit integer sine wave function, not directly compatible
|
||||
with standard trigonometric units like radians or degrees.
|
||||
@param x Input angle, 0-255; 256 would loop back to zero, completing
|
||||
the circle (equivalent to 360 degrees or 2 pi radians).
|
||||
One can therefore use an unsigned 8-bit variable and simply
|
||||
add or subtract, allowing it to overflow/underflow and it
|
||||
still does the expected contiguous thing.
|
||||
@return Sine result, 0 to 255, or -128 to +127 if type-converted to
|
||||
a signed int8_t, but you'll most likely want unsigned as this
|
||||
output is often used for pixel brightness in animation effects.
|
||||
*/
|
||||
static uint8_t sine8(uint8_t x) {
|
||||
return pgm_read_byte(&_NeoPixelSineTable[x]); // 0-255 in, 0-255 out
|
||||
}
|
||||
/*!
|
||||
@brief An 8-bit gamma-correction function for basic pixel brightness
|
||||
adjustment. Makes color transitions appear more perceptially
|
||||
correct.
|
||||
@param x Input brightness, 0 (minimum or off/black) to 255 (maximum).
|
||||
@return Gamma-adjusted brightness, can then be passed to one of the
|
||||
setPixelColor() functions. This uses a fixed gamma correction
|
||||
exponent of 2.6, which seems reasonably okay for average
|
||||
NeoPixels in average tasks. If you need finer control you'll
|
||||
need to provide your own gamma-correction function instead.
|
||||
*/
|
||||
static uint8_t gamma8(uint8_t x) {
|
||||
return pgm_read_byte(&_NeoPixelGammaTable[x]); // 0-255 in, 0-255 out
|
||||
}
|
||||
/*!
|
||||
@brief Convert separate red, green and blue values into a single
|
||||
"packed" 32-bit RGB color.
|
||||
@param r Red brightness, 0 to 255.
|
||||
@param g Green brightness, 0 to 255.
|
||||
@param b Blue brightness, 0 to 255.
|
||||
@return 32-bit packed RGB value, which can then be assigned to a
|
||||
variable for later use or passed to the setPixelColor()
|
||||
function. Packed RGB format is predictable, regardless of
|
||||
LED strand color order.
|
||||
*/
|
||||
static uint32_t Color(uint8_t r, uint8_t g, uint8_t b) {
|
||||
return ((uint32_t)r << 16) | ((uint32_t)g << 8) | b;
|
||||
}
|
||||
/*!
|
||||
@brief Convert separate red, green, blue and white values into a
|
||||
single "packed" 32-bit WRGB color.
|
||||
@param r Red brightness, 0 to 255.
|
||||
@param g Green brightness, 0 to 255.
|
||||
@param b Blue brightness, 0 to 255.
|
||||
@param w White brightness, 0 to 255.
|
||||
@return 32-bit packed WRGB value, which can then be assigned to a
|
||||
variable for later use or passed to the setPixelColor()
|
||||
function. Packed WRGB format is predictable, regardless of
|
||||
LED strand color order.
|
||||
*/
|
||||
static uint32_t Color(uint8_t r, uint8_t g, uint8_t b, uint8_t w) {
|
||||
return ((uint32_t)w << 24) | ((uint32_t)r << 16) | ((uint32_t)g << 8) | b;
|
||||
}
|
||||
static uint32_t ColorHSV(uint16_t hue, uint8_t sat = 255, uint8_t val = 255);
|
||||
/*!
|
||||
@brief A gamma-correction function for 32-bit packed RGB or WRGB
|
||||
colors. Makes color transitions appear more perceptially
|
||||
correct.
|
||||
@param x 32-bit packed RGB or WRGB color.
|
||||
@return Gamma-adjusted packed color, can then be passed in one of the
|
||||
setPixelColor() functions. Like gamma8(), this uses a fixed
|
||||
gamma correction exponent of 2.6, which seems reasonably okay
|
||||
for average NeoPixels in average tasks. If you need finer
|
||||
control you'll need to provide your own gamma-correction
|
||||
function instead.
|
||||
*/
|
||||
static uint32_t gamma32(uint32_t x);
|
||||
|
||||
void rainbow(uint16_t first_hue = 0, int8_t reps = 1,
|
||||
uint8_t saturation = 255, uint8_t brightness = 255,
|
||||
bool gammify = true);
|
||||
|
||||
private:
|
||||
#if defined(ARDUINO_ARCH_RP2040)
|
||||
void rp2040Init(uint8_t pin, bool is800KHz);
|
||||
void rp2040Show(uint8_t pin, uint8_t *pixels, uint32_t numBytes, bool is800KHz);
|
||||
#endif
|
||||
|
||||
protected:
|
||||
#ifdef NEO_KHZ400 // If 400 KHz NeoPixel support enabled...
|
||||
bool is800KHz; ///< true if 800 KHz pixels
|
||||
#endif
|
||||
bool begun; ///< true if begin() previously called
|
||||
uint16_t numLEDs; ///< Number of RGB LEDs in strip
|
||||
uint16_t numBytes; ///< Size of 'pixels' buffer below
|
||||
int16_t pin; ///< Output pin number (-1 if not yet set)
|
||||
uint8_t brightness; ///< Strip brightness 0-255 (stored as +1)
|
||||
uint8_t *pixels; ///< Holds LED color values (3 or 4 bytes each)
|
||||
uint8_t rOffset; ///< Red index within each 3- or 4-byte pixel
|
||||
uint8_t gOffset; ///< Index of green byte
|
||||
uint8_t bOffset; ///< Index of blue byte
|
||||
uint8_t wOffset; ///< Index of white (==rOffset if no white)
|
||||
uint32_t endTime; ///< Latch timing reference
|
||||
#ifdef __AVR__
|
||||
volatile uint8_t *port; ///< Output PORT register
|
||||
uint8_t pinMask; ///< Output PORT bitmask
|
||||
#endif
|
||||
#if defined(ARDUINO_ARCH_STM32) || defined(ARDUINO_ARCH_ARDUINO_CORE_STM32)
|
||||
GPIO_TypeDef *gpioPort; ///< Output GPIO PORT
|
||||
uint32_t gpioPin; ///< Output GPIO PIN
|
||||
#endif
|
||||
#if defined(ARDUINO_ARCH_RP2040)
|
||||
PIO pio = pio0;
|
||||
int sm = 0;
|
||||
bool init = true;
|
||||
#endif
|
||||
};
|
||||
|
||||
#endif // ADAFRUIT_NEOPIXEL_H
|
||||
13
lib/Adafruit_NeoPixel/CONTRIBUTING.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Contribution Guidelines
|
||||
|
||||
This library is the culmination of the expertise of many members of the open source community who have dedicated their time and hard work. The best way to ask for help or propose a new idea is to [create a new issue](https://github.com/adafruit/Adafruit_NeoPixel/issues/new) while creating a Pull Request with your code changes allows you to share your own innovations with the rest of the community.
|
||||
|
||||
The following are some guidelines to observe when creating issues or PRs:
|
||||
|
||||
- Be friendly; it is important that we can all enjoy a safe space as we are all working on the same project and it is okay for people to have different ideas
|
||||
|
||||
- [Use code blocks](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet#code); it helps us help you when we can read your code! On that note also refrain from pasting more than 30 lines of code in a post, instead [create a gist](https://gist.github.com/) if you need to share large snippets
|
||||
|
||||
- Use reasonable titles; refrain from using overly long or capitalized titles as they are usually annoying and do little to encourage others to help :smile:
|
||||
|
||||
- Be detailed; refrain from mentioning code problems without sharing your source code and always give information regarding your board and version of the library
|
||||
165
lib/Adafruit_NeoPixel/COPYING
Normal file
@@ -0,0 +1,165 @@
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
|
||||
This version of the GNU Lesser General Public License incorporates
|
||||
the terms and conditions of version 3 of the GNU General Public
|
||||
License, supplemented by the additional permissions listed below.
|
||||
|
||||
0. Additional Definitions.
|
||||
|
||||
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
||||
General Public License.
|
||||
|
||||
"The Library" refers to a covered work governed by this License,
|
||||
other than an Application or a Combined Work as defined below.
|
||||
|
||||
An "Application" is any work that makes use of an interface provided
|
||||
by the Library, but which is not otherwise based on the Library.
|
||||
Defining a subclass of a class defined by the Library is deemed a mode
|
||||
of using an interface provided by the Library.
|
||||
|
||||
A "Combined Work" is a work produced by combining or linking an
|
||||
Application with the Library. The particular version of the Library
|
||||
with which the Combined Work was made is also called the "Linked
|
||||
Version".
|
||||
|
||||
The "Minimal Corresponding Source" for a Combined Work means the
|
||||
Corresponding Source for the Combined Work, excluding any source code
|
||||
for portions of the Combined Work that, considered in isolation, are
|
||||
based on the Application, and not on the Linked Version.
|
||||
|
||||
The "Corresponding Application Code" for a Combined Work means the
|
||||
object code and/or source code for the Application, including any data
|
||||
and utility programs needed for reproducing the Combined Work from the
|
||||
Application, but excluding the System Libraries of the Combined Work.
|
||||
|
||||
1. Exception to Section 3 of the GNU GPL.
|
||||
|
||||
You may convey a covered work under sections 3 and 4 of this License
|
||||
without being bound by section 3 of the GNU GPL.
|
||||
|
||||
2. Conveying Modified Versions.
|
||||
|
||||
If you modify a copy of the Library, and, in your modifications, a
|
||||
facility refers to a function or data to be supplied by an Application
|
||||
that uses the facility (other than as an argument passed when the
|
||||
facility is invoked), then you may convey a copy of the modified
|
||||
version:
|
||||
|
||||
a) under this License, provided that you make a good faith effort to
|
||||
ensure that, in the event an Application does not supply the
|
||||
function or data, the facility still operates, and performs
|
||||
whatever part of its purpose remains meaningful, or
|
||||
|
||||
b) under the GNU GPL, with none of the additional permissions of
|
||||
this License applicable to that copy.
|
||||
|
||||
3. Object Code Incorporating Material from Library Header Files.
|
||||
|
||||
The object code form of an Application may incorporate material from
|
||||
a header file that is part of the Library. You may convey such object
|
||||
code under terms of your choice, provided that, if the incorporated
|
||||
material is not limited to numerical parameters, data structure
|
||||
layouts and accessors, or small macros, inline functions and templates
|
||||
(ten or fewer lines in length), you do both of the following:
|
||||
|
||||
a) Give prominent notice with each copy of the object code that the
|
||||
Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the object code with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
4. Combined Works.
|
||||
|
||||
You may convey a Combined Work under terms of your choice that,
|
||||
taken together, effectively do not restrict modification of the
|
||||
portions of the Library contained in the Combined Work and reverse
|
||||
engineering for debugging such modifications, if you also do each of
|
||||
the following:
|
||||
|
||||
a) Give prominent notice with each copy of the Combined Work that
|
||||
the Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
c) For a Combined Work that displays copyright notices during
|
||||
execution, include the copyright notice for the Library among
|
||||
these notices, as well as a reference directing the user to the
|
||||
copies of the GNU GPL and this license document.
|
||||
|
||||
d) Do one of the following:
|
||||
|
||||
0) Convey the Minimal Corresponding Source under the terms of this
|
||||
License, and the Corresponding Application Code in a form
|
||||
suitable for, and under terms that permit, the user to
|
||||
recombine or relink the Application with a modified version of
|
||||
the Linked Version to produce a modified Combined Work, in the
|
||||
manner specified by section 6 of the GNU GPL for conveying
|
||||
Corresponding Source.
|
||||
|
||||
1) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (a) uses at run time
|
||||
a copy of the Library already present on the user's computer
|
||||
system, and (b) will operate properly with a modified version
|
||||
of the Library that is interface-compatible with the Linked
|
||||
Version.
|
||||
|
||||
e) Provide Installation Information, but only if you would otherwise
|
||||
be required to provide such information under section 6 of the
|
||||
GNU GPL, and only to the extent that such information is
|
||||
necessary to install and execute a modified version of the
|
||||
Combined Work produced by recombining or relinking the
|
||||
Application with a modified version of the Linked Version. (If
|
||||
you use option 4d0, the Installation Information must accompany
|
||||
the Minimal Corresponding Source and Corresponding Application
|
||||
Code. If you use option 4d1, you must provide the Installation
|
||||
Information in the manner specified by section 6 of the GNU GPL
|
||||
for conveying Corresponding Source.)
|
||||
|
||||
5. Combined Libraries.
|
||||
|
||||
You may place library facilities that are a work based on the
|
||||
Library side by side in a single library together with other library
|
||||
facilities that are not Applications and are not covered by this
|
||||
License, and convey such a combined library under terms of your
|
||||
choice, if you do both of the following:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work based
|
||||
on the Library, uncombined with any other library facilities,
|
||||
conveyed under the terms of this License.
|
||||
|
||||
b) Give prominent notice with the combined library that part of it
|
||||
is a work based on the Library, and explaining where to find the
|
||||
accompanying uncombined form of the same work.
|
||||
|
||||
6. Revised Versions of the GNU Lesser General Public License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions
|
||||
of the GNU Lesser General Public License from time to time. Such new
|
||||
versions will be similar in spirit to the present version, but may
|
||||
differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Library as you received it specifies that a certain numbered version
|
||||
of the GNU Lesser General Public License "or any later version"
|
||||
applies to it, you have the option of following the terms and
|
||||
conditions either of that published version or of any later version
|
||||
published by the Free Software Foundation. If the Library as you
|
||||
received it does not specify a version number of the GNU Lesser
|
||||
General Public License, you may choose any version of the GNU Lesser
|
||||
General Public License ever published by the Free Software Foundation.
|
||||
|
||||
If the Library as you received it specifies that a proxy can decide
|
||||
whether future versions of the GNU Lesser General Public License shall
|
||||
apply, that proxy's public statement of acceptance of any version is
|
||||
permanent authorization for you to choose that version for the
|
||||
Library.
|
||||
157
lib/Adafruit_NeoPixel/README.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# Adafruit NeoPixel Library [](https://github.com/adafruit/Adafruit_NeoPixel/actions)[](http://adafruit.github.io/Adafruit_NeoPixel/html/index.html)
|
||||
|
||||
Arduino library for controlling single-wire-based LED pixels and strip such as the [Adafruit 60 LED/meter Digital LED strip][strip], the [Adafruit FLORA RGB Smart Pixel][flora], the [Adafruit Breadboard-friendly RGB Smart Pixel][pixel], the [Adafruit NeoPixel Stick][stick], and the [Adafruit NeoPixel Shield][shield].
|
||||
|
||||
After downloading, rename folder to 'Adafruit_NeoPixel' and install in Arduino Libraries folder. Restart Arduino IDE, then open File->Sketchbook->Library->Adafruit_NeoPixel->strandtest sketch.
|
||||
|
||||
Compatibility notes: Port A is not supported on any AVR processors at this time
|
||||
|
||||
[flora]: http://adafruit.com/products/1060
|
||||
[strip]: http://adafruit.com/products/1138
|
||||
[pixel]: http://adafruit.com/products/1312
|
||||
[stick]: http://adafruit.com/products/1426
|
||||
[shield]: http://adafruit.com/products/1430
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
### First Method
|
||||
|
||||

|
||||
|
||||
1. In the Arduino IDE, navigate to Sketch > Include Library > Manage Libraries
|
||||
1. Then the Library Manager will open and you will find a list of libraries that are already installed or ready for installation.
|
||||
1. Then search for Neopixel strip using the search bar.
|
||||
1. Click on the text area and then select the specific version and install it.
|
||||
|
||||
### Second Method
|
||||
|
||||
1. Navigate to the [Releases page](https://github.com/adafruit/Adafruit_NeoPixel/releases).
|
||||
1. Download the latest release.
|
||||
1. Extract the zip file
|
||||
1. In the Arduino IDE, navigate to Sketch > Include Library > Add .ZIP Library
|
||||
|
||||
## Features
|
||||
|
||||
- ### Simple to use
|
||||
|
||||
Controlling NeoPixels “from scratch” is quite a challenge, so we provide a library letting you focus on the fun and interesting bits.
|
||||
|
||||
- ### Give back
|
||||
|
||||
The library is free; you don’t have to pay for anything. Adafruit invests time and resources providing this open source code, please support Adafruit and open-source hardware by purchasing products from Adafruit!
|
||||
|
||||
- ### Supported Chipsets
|
||||
|
||||
We have included code for the following chips - sometimes these break for exciting reasons that we can't control in which case please open an issue!
|
||||
|
||||
- AVR ATmega and ATtiny (any 8-bit) - 8 MHz, 12 MHz and 16 MHz
|
||||
- Teensy 3.x and LC
|
||||
- Arduino Due
|
||||
- Arduino 101
|
||||
- ATSAMD21 (Arduino Zero/M0 and other SAMD21 boards) @ 48 MHz
|
||||
- ATSAMD51 @ 120 MHz
|
||||
- Adafruit STM32 Feather @ 120 MHz
|
||||
- ESP8266 any speed
|
||||
- ESP32 any speed
|
||||
- Nordic nRF52 (Adafruit Feather nRF52), nRF51 (micro:bit)
|
||||
- Infineon XMC1100 BootKit @ 32 MHz
|
||||
- Infineon XMC1100 2Go @ 32 MHz
|
||||
- Infineon XMC1300 BootKit @ 32 MHz
|
||||
- Infineon XMC4700 RelaxKit, XMC4800 RelaxKit, XMC4800 IoT Amazon FreeRTOS Kit @ 144 MHz
|
||||
|
||||
Check forks for other architectures not listed here!
|
||||
|
||||
- ### GNU Lesser General Public License
|
||||
|
||||
Adafruit_NeoPixel is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
## Functions
|
||||
|
||||
- begin()
|
||||
- updateLength()
|
||||
- updateType()
|
||||
- show()
|
||||
- delay_ns()
|
||||
- setPin()
|
||||
- setPixelColor()
|
||||
- fill()
|
||||
- ColorHSV()
|
||||
- getPixelColor()
|
||||
- setBrightness()
|
||||
- getBrightness()
|
||||
- clear()
|
||||
- gamma32()
|
||||
|
||||
## Examples
|
||||
|
||||
There are many examples implemented in this library. One of the examples is below. You can find other examples [here](https://github.com/adafruit/Adafruit_NeoPixel/tree/master/examples)
|
||||
|
||||
### Simple
|
||||
|
||||
```Cpp
|
||||
#include <Adafruit_NeoPixel.h>
|
||||
#ifdef __AVR__
|
||||
#include <avr/power.h>
|
||||
#endif
|
||||
#define PIN 6
|
||||
#define NUMPIXELS 16
|
||||
|
||||
Adafruit_NeoPixel pixels(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);
|
||||
#define DELAYVAL 500
|
||||
|
||||
void setup() {
|
||||
#if defined(__AVR_ATtiny85__) && (F_CPU == 16000000)
|
||||
clock_prescale_set(clock_div_1);
|
||||
#endif
|
||||
|
||||
pixels.begin();
|
||||
}
|
||||
|
||||
void loop() {
|
||||
pixels.clear();
|
||||
|
||||
for(int i=0; i<NUMPIXELS; i++) {
|
||||
|
||||
pixels.setPixelColor(i, pixels.Color(0, 150, 0));
|
||||
pixels.show();
|
||||
delay(DELAYVAL);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
If you want to contribute to this project:
|
||||
|
||||
- Report bugs and errors
|
||||
- Ask for enhancements
|
||||
- Create issues and pull requests
|
||||
- Tell others about this library
|
||||
- Contribute new protocols
|
||||
|
||||
Please read [CONTRIBUTING.md](https://github.com/adafruit/Adafruit_NeoPixel/blob/master/CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests to us.
|
||||
|
||||
### Roadmap
|
||||
|
||||
The PRIME DIRECTIVE is to maintain backward compatibility with existing Arduino sketches -- many are hosted elsewhere and don't track changes here, some are in print and can never be changed!
|
||||
|
||||
Please don't reformat code for the sake of reformatting code. The resulting large "visual diff" makes it impossible to untangle actual bug fixes from merely rearranged lines. (Exception for first item in wishlist below.)
|
||||
|
||||
Things I'd Like To Do But There's No Official Timeline So Please Don't Count On Any Of This Ever Being Canonical:
|
||||
|
||||
- For the show() function (with all the delicate pixel timing stuff), break out each architecture into separate source files rather than the current unmaintainable tangle of #ifdef statements!
|
||||
- Please don't use updateLength() or updateType() in new code. They should not have been implemented this way (use the C++ 'new' operator with the regular constructor instead) and are only sticking around because of the Prime Directive. setPin() is OK for now though, it's a trick we can use to 'recycle' pixel memory across multiple strips.
|
||||
- In the M0 and M4 code, use the hardware systick counter for bit timing rather than hand-tweaked NOPs (a temporary kludge at the time because I wasn't reading systick correctly). (As of 1.4.2, systick is used on M4 devices and it appears to be overclock-compatible. Not for M0 yet, which is why this item is still here.)
|
||||
- As currently written, brightness scaling is still a "destructive" operation -- pixel values are altered in RAM and the original value as set can't be accurately read back, only approximated, which has been confusing and frustrating to users. It was done this way at the time because NeoPixel timing is strict, AVR microcontrollers (all we had at the time) are limited, and assembly language is hard. All the 32-bit architectures should have no problem handling nondestructive brightness scaling -- calculating each byte immediately before it's sent out the wire, maintaining the original set value in RAM -- the work just hasn't been done. There's a fair chance even the AVR code could manage it with some intense focus. (The DotStar library achieves nondestructive brightness scaling because it doesn't have to manage data timing so carefully...every architecture, even ATtiny, just takes whatever cycles it needs for the multiply/shift operations.)
|
||||
|
||||
## Credits
|
||||
|
||||
This library is written by Phil "Paint Your Dragon" Burgess for Adafruit Industries, with contributions by PJRC, Michael Miller and other members of the open source community.
|
||||
|
||||
## License
|
||||
|
||||
Adafruit_NeoPixel is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
Adafruit_NeoPixel is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the [GNU Lesser General Public License](https://www.gnu.org/licenses/lgpl-3.0.en.html) for more details.
|
||||
You should have received a copy of the GNU Lesser General Public License along with NeoPixel. If not, see [this](https://www.gnu.org/licenses/)
|
||||
178
lib/Adafruit_NeoPixel/esp.c
Normal file
@@ -0,0 +1,178 @@
|
||||
// Implements the RMT peripheral on Espressif SoCs
|
||||
// Copyright (c) 2020 Lucian Copeland for Adafruit Industries
|
||||
|
||||
/* Uses code from Espressif RGB LED Strip demo and drivers
|
||||
* Copyright 2015-2020 Espressif Systems (Shanghai) PTE LTD
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#if defined(ESP32)
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "driver/rmt.h"
|
||||
|
||||
#if defined(ESP_IDF_VERSION)
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 0, 0)
|
||||
#define HAS_ESP_IDF_4
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// This code is adapted from the ESP-IDF v3.4 RMT "led_strip" example, altered
|
||||
// to work with the Arduino version of the ESP-IDF (3.2)
|
||||
|
||||
#define WS2812_T0H_NS (400)
|
||||
#define WS2812_T0L_NS (850)
|
||||
#define WS2812_T1H_NS (800)
|
||||
#define WS2812_T1L_NS (450)
|
||||
|
||||
#define WS2811_T0H_NS (500)
|
||||
#define WS2811_T0L_NS (2000)
|
||||
#define WS2811_T1H_NS (1200)
|
||||
#define WS2811_T1L_NS (1300)
|
||||
|
||||
static uint32_t t0h_ticks = 0;
|
||||
static uint32_t t1h_ticks = 0;
|
||||
static uint32_t t0l_ticks = 0;
|
||||
static uint32_t t1l_ticks = 0;
|
||||
|
||||
// Limit the number of RMT channels available for the Neopixels. Defaults to all
|
||||
// channels (8 on ESP32, 4 on ESP32-S2 and S3). Redefining this value will free
|
||||
// any channels with a higher number for other uses, such as IR send-and-recieve
|
||||
// libraries. Redefine as 1 to restrict Neopixels to only a single channel.
|
||||
#define ADAFRUIT_RMT_CHANNEL_MAX RMT_CHANNEL_MAX
|
||||
|
||||
#define RMT_LL_HW_BASE (&RMT)
|
||||
|
||||
bool rmt_reserved_channels[ADAFRUIT_RMT_CHANNEL_MAX];
|
||||
|
||||
static void IRAM_ATTR ws2812_rmt_adapter(const void *src, rmt_item32_t *dest, size_t src_size,
|
||||
size_t wanted_num, size_t *translated_size, size_t *item_num)
|
||||
{
|
||||
if (src == NULL || dest == NULL) {
|
||||
*translated_size = 0;
|
||||
*item_num = 0;
|
||||
return;
|
||||
}
|
||||
const rmt_item32_t bit0 = {{{ t0h_ticks, 1, t0l_ticks, 0 }}}; //Logical 0
|
||||
const rmt_item32_t bit1 = {{{ t1h_ticks, 1, t1l_ticks, 0 }}}; //Logical 1
|
||||
size_t size = 0;
|
||||
size_t num = 0;
|
||||
uint8_t *psrc = (uint8_t *)src;
|
||||
rmt_item32_t *pdest = dest;
|
||||
while (size < src_size && num < wanted_num) {
|
||||
for (int i = 0; i < 8; i++) {
|
||||
// MSB first
|
||||
if (*psrc & (1 << (7 - i))) {
|
||||
pdest->val = bit1.val;
|
||||
} else {
|
||||
pdest->val = bit0.val;
|
||||
}
|
||||
num++;
|
||||
pdest++;
|
||||
}
|
||||
size++;
|
||||
psrc++;
|
||||
}
|
||||
*translated_size = size;
|
||||
*item_num = num;
|
||||
}
|
||||
|
||||
void espShow(uint8_t pin, uint8_t *pixels, uint32_t numBytes, boolean is800KHz) {
|
||||
// Reserve channel
|
||||
rmt_channel_t channel = ADAFRUIT_RMT_CHANNEL_MAX;
|
||||
for (size_t i = 0; i < ADAFRUIT_RMT_CHANNEL_MAX; i++) {
|
||||
if (!rmt_reserved_channels[i]) {
|
||||
rmt_reserved_channels[i] = true;
|
||||
channel = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (channel == ADAFRUIT_RMT_CHANNEL_MAX) {
|
||||
// Ran out of channels!
|
||||
return;
|
||||
}
|
||||
|
||||
#if defined(HAS_ESP_IDF_4)
|
||||
rmt_config_t config = RMT_DEFAULT_CONFIG_TX(pin, channel);
|
||||
config.clk_div = 2;
|
||||
#else
|
||||
// Match default TX config from ESP-IDF version 3.4
|
||||
rmt_config_t config = {
|
||||
.rmt_mode = RMT_MODE_TX,
|
||||
.channel = channel,
|
||||
.gpio_num = pin,
|
||||
.clk_div = 2,
|
||||
.mem_block_num = 1,
|
||||
.tx_config = {
|
||||
.carrier_freq_hz = 38000,
|
||||
.carrier_level = RMT_CARRIER_LEVEL_HIGH,
|
||||
.idle_level = RMT_IDLE_LEVEL_LOW,
|
||||
.carrier_duty_percent = 33,
|
||||
.carrier_en = false,
|
||||
.loop_en = false,
|
||||
.idle_output_en = true,
|
||||
}
|
||||
};
|
||||
#endif
|
||||
rmt_config(&config);
|
||||
rmt_driver_install(config.channel, 0, 0);
|
||||
|
||||
// Convert NS timings to ticks
|
||||
uint32_t counter_clk_hz = 0;
|
||||
|
||||
#if defined(HAS_ESP_IDF_4)
|
||||
rmt_get_counter_clock(channel, &counter_clk_hz);
|
||||
#else
|
||||
// this emulates the rmt_get_counter_clock() function from ESP-IDF 3.4
|
||||
if (RMT_LL_HW_BASE->conf_ch[config.channel].conf1.ref_always_on == RMT_BASECLK_REF) {
|
||||
uint32_t div_cnt = RMT_LL_HW_BASE->conf_ch[config.channel].conf0.div_cnt;
|
||||
uint32_t div = div_cnt == 0 ? 256 : div_cnt;
|
||||
counter_clk_hz = REF_CLK_FREQ / (div);
|
||||
} else {
|
||||
uint32_t div_cnt = RMT_LL_HW_BASE->conf_ch[config.channel].conf0.div_cnt;
|
||||
uint32_t div = div_cnt == 0 ? 256 : div_cnt;
|
||||
counter_clk_hz = APB_CLK_FREQ / (div);
|
||||
}
|
||||
#endif
|
||||
|
||||
// NS to tick converter
|
||||
float ratio = (float)counter_clk_hz / 1e9;
|
||||
|
||||
if (is800KHz) {
|
||||
t0h_ticks = (uint32_t)(ratio * WS2812_T0H_NS);
|
||||
t0l_ticks = (uint32_t)(ratio * WS2812_T0L_NS);
|
||||
t1h_ticks = (uint32_t)(ratio * WS2812_T1H_NS);
|
||||
t1l_ticks = (uint32_t)(ratio * WS2812_T1L_NS);
|
||||
} else {
|
||||
t0h_ticks = (uint32_t)(ratio * WS2811_T0H_NS);
|
||||
t0l_ticks = (uint32_t)(ratio * WS2811_T0L_NS);
|
||||
t1h_ticks = (uint32_t)(ratio * WS2811_T1H_NS);
|
||||
t1l_ticks = (uint32_t)(ratio * WS2811_T1L_NS);
|
||||
}
|
||||
|
||||
// Initialize automatic timing translator
|
||||
rmt_translator_init(config.channel, ws2812_rmt_adapter);
|
||||
|
||||
// Write and wait to finish
|
||||
rmt_write_sample(config.channel, pixels, (size_t)numBytes, true);
|
||||
rmt_wait_tx_done(config.channel, pdMS_TO_TICKS(100));
|
||||
|
||||
// Free channel again
|
||||
rmt_driver_uninstall(config.channel);
|
||||
rmt_reserved_channels[channel] = false;
|
||||
|
||||
gpio_set_direction(pin, GPIO_MODE_OUTPUT);
|
||||
}
|
||||
|
||||
#endif
|
||||