mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-08-29 15:18:41 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ec1e57244 | ||
|
|
758b8144a1 | ||
|
|
590564c158 | ||
|
|
ea412cfe82 | ||
|
|
fa02bb5b74 | ||
|
|
683d29f9f7 | ||
|
|
3478e40017 | ||
|
|
ba0cc61965 | ||
|
|
8139d7f1f4 | ||
|
|
d487f37a7a | ||
|
|
087b85cb9f | ||
|
|
4bd281ce6c | ||
|
|
ced3d43dad | ||
|
|
91f22f1230 | ||
|
|
57d031d54a | ||
|
|
8e946623a9 | ||
|
|
e1d52ae43a | ||
|
|
f64cd7e8f3 | ||
|
|
df8e1687b2 | ||
|
|
3408e3c05a | ||
|
|
7a22cee639 | ||
|
|
dec91919bc | ||
|
|
313865b0a3 | ||
|
|
ee1a5f0400 | ||
|
|
7f2803b020 | ||
|
|
83205e43da | ||
|
|
45e127e22f | ||
|
|
d22d45a697 | ||
|
|
330a913e76 | ||
|
|
af0521b54c | ||
|
|
8cf8cc0231 | ||
|
|
f5f5b037b8 | ||
|
|
1252f10d39 | ||
|
|
a3c8e1c3c9 | ||
|
|
073989c7a4 | ||
|
|
05eff957eb | ||
|
|
3c855d2042 | ||
|
|
0bdf663ca1 | ||
|
|
813dd49a41 | ||
|
|
dab7e6a1ae | ||
|
|
b73c3e3dfb | ||
|
|
4ccfdfc6f4 | ||
|
|
15601d6339 | ||
|
|
1046f0e74a | ||
|
|
032e5238b8 | ||
|
|
5b3fdf7b9f | ||
|
|
0489e75e1e | ||
|
|
eb655f17db | ||
|
|
d8c7c00ca2 | ||
|
|
9aa7e1ec86 | ||
|
|
caa652b0ad | ||
|
|
08e82e4980 | ||
|
|
5067721fdf | ||
|
|
b014857a13 | ||
|
|
eb7945248b | ||
|
|
69638d7956 | ||
|
|
953f3a232b | ||
|
|
d1824f169b | ||
|
|
81973c3a2a | ||
|
|
213e2b50d6 | ||
|
|
0a63ae5376 | ||
|
|
61dd1dd96d | ||
|
|
ed85367e53 | ||
|
|
38c2ea5bf7 | ||
|
|
2d62027e24 | ||
|
|
a56719d19c | ||
|
|
94744525ba | ||
|
|
ea193f1938 | ||
|
|
88339473ce | ||
|
|
57566c76ed | ||
|
|
a09342836b | ||
|
|
b3fbf78bec | ||
|
|
a744838e9c | ||
|
|
712c7bdbaf | ||
|
|
6d3389b409 | ||
|
|
03005b38c8 | ||
|
|
17cff2a26f | ||
|
|
5777d76e55 | ||
|
|
91d67a0ef5 | ||
|
|
e083a6bff7 | ||
|
|
57aaa5a85a | ||
|
|
34bad92ec6 | ||
|
|
b6a4acda9b | ||
|
|
1dc0b24e9a | ||
|
|
38de19bb2c | ||
|
|
c47f5df021 | ||
|
|
4ad838c114 | ||
|
|
b0b3aee648 | ||
|
|
446dbbf1fc | ||
|
|
11f0be55d8 | ||
|
|
e10dd893ab | ||
|
|
0dd899679d | ||
|
|
05cab61e1f | ||
|
|
0c7e7e5089 | ||
|
|
5f71776434 | ||
|
|
a7e02a7be8 | ||
|
|
7df6b2bab5 | ||
|
|
21f51258a9 | ||
|
|
c6904686c3 | ||
|
|
a1bea8c5ab | ||
|
|
c23d244405 | ||
|
|
86898e5ceb | ||
|
|
3572c60770 | ||
|
|
ffc7ab83a2 | ||
|
|
c12bedb3e5 | ||
|
|
fd1622ba80 | ||
|
|
7564019ecd | ||
|
|
6d381da075 | ||
|
|
b10aa83fc8 | ||
|
|
a7997a3cd4 | ||
|
|
73c19bff71 | ||
|
|
80aa21123c | ||
|
|
afd1760b6f | ||
|
|
ddcab827ad | ||
|
|
d0a41548fd | ||
|
|
ff8e6b9c66 | ||
|
|
25f30a94f5 | ||
|
|
efe451df79 | ||
|
|
b97a09c75b | ||
|
|
f3b8c689a9 | ||
|
|
4d1a8ee873 | ||
|
|
f440c86019 | ||
|
|
4b20dfafab | ||
|
|
b228d9daf6 | ||
|
|
be82b737be | ||
|
|
d119f609de | ||
|
|
a71857c723 | ||
|
|
a404f9ff92 | ||
|
|
0cbbf40040 | ||
|
|
277ac10e44 | ||
|
|
2e8dfbb96e | ||
|
|
366642cf71 | ||
|
|
b7b3b51cfe | ||
|
|
e6f9aeda06 | ||
|
|
bad365b9a7 | ||
|
|
1f8a68d4c9 | ||
|
|
443d5ea43c | ||
|
|
af0cea0fb9 | ||
|
|
3701cce15e | ||
|
|
5b807c36ec | ||
|
|
7cea62995c | ||
|
|
2a7d49b709 | ||
|
|
1b7138e0c8 | ||
|
|
0476b3362e | ||
|
|
3ed7ac90c9 | ||
|
|
8b579b6533 | ||
|
|
2d731ba1b6 | ||
|
|
6a80d187fd | ||
|
|
1e8c3d05e5 | ||
|
|
ce7f09afc6 | ||
|
|
c0e8efcde5 | ||
|
|
1344ae0616 | ||
|
|
24baed3053 | ||
|
|
8e95061fe5 | ||
|
|
626b2c47c1 | ||
|
|
95dff212e7 | ||
|
|
c6416c9c61 | ||
|
|
1898140f64 | ||
|
|
8457c3735b | ||
|
|
47c08b6cbd | ||
|
|
b3de973af6 | ||
|
|
0d17d26133 | ||
|
|
c802e776d6 | ||
|
|
aa0f78269f | ||
|
|
97b14cc2d4 | ||
|
|
d952ee60a9 | ||
|
|
3d3bff88d7 | ||
|
|
c1cb4697f7 | ||
|
|
bfc51e0dc5 | ||
|
|
d339460269 | ||
|
|
fb60deeebc | ||
|
|
1b65ff5a65 | ||
|
|
e3ebb1c5bc | ||
|
|
099a8d35b0 | ||
|
|
3085608a35 | ||
|
|
307e0dcadd | ||
|
|
aeb737012c | ||
|
|
dddd2bca88 | ||
|
|
daedc54dd6 | ||
|
|
df80a11663 | ||
|
|
a6bebf1e7a | ||
|
|
52fdbbe057 | ||
|
|
b2422589dc | ||
|
|
d602426951 | ||
|
|
64009d0dbb | ||
|
|
b8e46075df | ||
|
|
589d656594 | ||
|
|
0872c670ad | ||
|
|
9b68684428 | ||
|
|
626b8d2b08 | ||
|
|
10d111e36a | ||
|
|
eb93c8aed7 | ||
|
|
fe005e478a | ||
|
|
8f88c4d26c | ||
|
|
946887b841 | ||
|
|
b2dd21e164 | ||
|
|
01f4f1f940 | ||
|
|
563eb3a890 | ||
|
|
ab4101dae8 | ||
|
|
9ab37735fe | ||
|
|
8f275f414b | ||
|
|
55b17c95a4 | ||
|
|
13c7348a97 | ||
|
|
62453a5d2a | ||
|
|
9840433fc0 | ||
|
|
8fe36d74ec | ||
|
|
8e85a1e081 | ||
|
|
ce9a8ed86f | ||
|
|
49a1f56641 | ||
|
|
76cf6fa444 | ||
|
|
e1ecbd97d0 | ||
|
|
c389ab28cd | ||
|
|
4c0c69b15f | ||
|
|
704bbb54d3 | ||
|
|
17947aa809 | ||
|
|
33811fec9f | ||
|
|
76cd07a701 | ||
|
|
e2b18d8f02 | ||
|
|
b3c886638a | ||
|
|
f9121b2f0f | ||
|
|
fb827f9dc0 | ||
|
|
e5be5b0219 | ||
|
|
c360f5a7b1 | ||
|
|
15f741b446 | ||
|
|
3b648c9e63 | ||
|
|
951360beba | ||
|
|
71df01d613 | ||
|
|
a8f4daff30 | ||
|
|
d7dc083351 | ||
|
|
45d55c9c71 | ||
|
|
085d5c9f8b | ||
|
|
09a5650345 | ||
|
|
24050a3a10 | ||
|
|
c6a2400f56 | ||
|
|
9da7f53e78 | ||
|
|
f5a2c802ca | ||
|
|
372d991db9 | ||
|
|
bab0a3e902 | ||
|
|
b85aaf8e54 | ||
|
|
64d744ea88 | ||
|
|
7748dd3321 | ||
|
|
24c6b2e2c9 | ||
|
|
9589d12353 | ||
|
|
dcc13b7b53 | ||
|
|
2d1733d0e7 | ||
|
|
1368a78767 | ||
|
|
f29073e413 | ||
|
|
7005497cba | ||
|
|
298c595deb | ||
|
|
bdbe2b4a70 | ||
|
|
653334b208 | ||
|
|
24c6c0bf8e | ||
|
|
3e252e4307 | ||
|
|
ae2de04251 | ||
|
|
07d238d5b9 | ||
|
|
407f0df6ad | ||
|
|
2e593e37a6 | ||
|
|
9f7fd479c4 | ||
|
|
0407a3b67c | ||
|
|
2c57bf81b0 | ||
|
|
960456401e | ||
|
|
d4774526c3 | ||
|
|
2a365d39ac | ||
|
|
347c023968 | ||
|
|
4272d9a13e | ||
|
|
54900c3680 | ||
|
|
461519cf39 | ||
|
|
8edb176dc0 | ||
|
|
61cdda96ff | ||
|
|
43394b3376 | ||
|
|
ca7f9ec8ff | ||
|
|
0857d087bd | ||
|
|
818a9e3946 | ||
|
|
51460aacc8 | ||
|
|
354beb486d | ||
|
|
8261d4efc6 | ||
|
|
bbb19371f3 | ||
|
|
04ae93f00c | ||
|
|
999c01086e | ||
|
|
0e67f8fa0a | ||
|
|
f352d18854 | ||
|
|
f865c976e3 | ||
|
|
aa90765db8 | ||
|
|
017fc03157 | ||
|
|
cdf62ea560 | ||
|
|
5dcd3f67ed | ||
|
|
b013a7d8ef | ||
|
|
8e6e38b968 | ||
|
|
8f20640008 | ||
|
|
57707cd9b9 | ||
|
|
911683ad13 | ||
|
|
99204682b8 | ||
|
|
fec7fca89e | ||
|
|
6bea88eeb6 | ||
|
|
6afcb97c0d | ||
|
|
73d60396bc | ||
|
|
942faf412f | ||
|
|
b62f65f045 | ||
|
|
f5cceb1719 | ||
|
|
0b6040dd25 | ||
|
|
45fda8aefd | ||
|
|
85f32c039f | ||
|
|
7fa2b5fcc3 | ||
|
|
08a0ace11a | ||
|
|
7605e1ec06 | ||
|
|
e934b0d94e | ||
|
|
4aa8e17214 | ||
|
|
a9f5f7c620 | ||
|
|
6e193ca442 | ||
|
|
2eadf30199 | ||
|
|
600e743941 | ||
|
|
932541649e | ||
|
|
0217593c77 | ||
|
|
10e65b5f26 | ||
|
|
e9a38353fa | ||
|
|
68caf0260d | ||
|
|
7d887adb7f | ||
|
|
ec57d899e5 | ||
|
|
61b1732ecb | ||
|
|
a35705cd38 | ||
|
|
eb878d3d8e | ||
|
|
3420cf508b | ||
|
|
d5a3bf626f | ||
|
|
0e1220ff24 | ||
|
|
1496b95067 | ||
|
|
bd0694e164 | ||
|
|
d580741dfe | ||
|
|
a24586bec3 | ||
|
|
1e7a76139d | ||
|
|
ea0a9990b9 | ||
|
|
97af5a64e7 | ||
|
|
8f781db93e | ||
|
|
1a6ba18ec2 | ||
|
|
d91e363d76 | ||
|
|
f7e45b8284 | ||
|
|
bbbaaf4d89 | ||
|
|
c9076f8ce6 | ||
|
|
82fd062668 | ||
|
|
769a1afd96 | ||
|
|
f943619387 | ||
|
|
43e204b7d1 | ||
|
|
a3f1399489 | ||
|
|
4179d879d2 | ||
|
|
6ce2d2e73f | ||
|
|
981542827c | ||
|
|
2f0a2cee6e | ||
|
|
0bc685a8e3 | ||
|
|
02a99703a8 | ||
|
|
7639ce44a7 | ||
|
|
b89a99fe0c | ||
|
|
06820d2220 | ||
|
|
140213950d | ||
|
|
f4f2c498b7 | ||
|
|
31807c6c6b | ||
|
|
8ccad8ee53 | ||
|
|
a930539dc3 | ||
|
|
b33db9397d | ||
|
|
de4b7fb022 | ||
|
|
9e85419f9e | ||
|
|
80c464c951 | ||
|
|
e56dff4972 | ||
|
|
d8501f04eb | ||
|
|
c927b4b935 | ||
|
|
fb9872a4ff | ||
|
|
39b0964739 | ||
|
|
2c509c0368 | ||
|
|
49e1c68cac | ||
|
|
bb3e8f6d41 | ||
|
|
835a3288a0 | ||
|
|
cddbb9a5e5 | ||
|
|
65366478e3 | ||
|
|
1d8b19dce9 | ||
|
|
f600066134 | ||
|
|
75533c20b8 | ||
|
|
0605a5e0e3 | ||
|
|
cf9640bd59 | ||
|
|
328b9fbd60 | ||
|
|
bc6d1070c7 | ||
|
|
bf339636b5 | ||
|
|
d90dae5d9d | ||
|
|
b34b0e866f | ||
|
|
2288c0f1c2 | ||
|
|
9c4abb64c3 | ||
|
|
559f235e88 | ||
|
|
30df0e25f9 | ||
|
|
e7735112e8 | ||
|
|
b943feef29 | ||
|
|
e92d3f4ede | ||
|
|
044cd0b3a6 | ||
|
|
3351bc2f42 | ||
|
|
6a8d236b4d | ||
|
|
7d67ffc429 | ||
|
|
f984734961 | ||
|
|
217fbb284a | ||
|
|
9fe48e2a88 | ||
|
|
1c7817ad77 | ||
|
|
cf801344f8 | ||
|
|
50bca161ce | ||
|
|
15ccf945c3 | ||
|
|
d49f06f34d | ||
|
|
826f03220d | ||
|
|
cdb984f830 | ||
|
|
4e40c8223f | ||
|
|
02d86ab233 | ||
|
|
a49079f560 | ||
|
|
dddce571d3 | ||
|
|
f1841377db | ||
|
|
f3edfbf75b | ||
|
|
d701d96bb2 | ||
|
|
3b9d6c5651 | ||
|
|
19bd87de4a | ||
|
|
af27e910d8 | ||
|
|
d8f1f3ef06 | ||
|
|
5b2756fef3 | ||
|
|
75db8cf3f1 | ||
|
|
b4660417f8 | ||
|
|
75e677608b | ||
|
|
05cb1406bb | ||
|
|
10c6d1e4d9 | ||
|
|
6d08a2f0f6 | ||
|
|
8d8c46ecae | ||
|
|
1f642d86e8 | ||
|
|
5afda96cf4 | ||
|
|
2550ca5978 | ||
|
|
566fc625cd | ||
|
|
7f9de55b6c | ||
|
|
ccb5b49c80 | ||
|
|
dea44579c3 | ||
|
|
58445a2327 | ||
|
|
98cf8dbe5e | ||
|
|
8fc83caac7 | ||
|
|
56f7f3e1bd | ||
|
|
01b46cf74e | ||
|
|
825c761c26 | ||
|
|
443eb23a91 | ||
|
|
9b78d85a46 | ||
|
|
6731c85981 | ||
|
|
da44667926 | ||
|
|
3796084a27 | ||
|
|
4d32326a4f | ||
|
|
54d8002fd3 | ||
|
|
21a30fc555 | ||
|
|
b90c0c4114 | ||
|
|
e2deb23d77 | ||
|
|
b69d31d3ee | ||
|
|
3481423810 | ||
|
|
d093200fec | ||
|
|
eaf834a921 | ||
|
|
27377b0658 | ||
|
|
2c53bc2c61 | ||
|
|
83fae81724 | ||
|
|
d075f0752e | ||
|
|
a29f90b441 | ||
|
|
891392ff9a | ||
|
|
ba2d9aefe6 | ||
|
|
586dda4d46 | ||
|
|
1aa600ee59 | ||
|
|
b0e352bfea | ||
|
|
959ca95219 | ||
|
|
54bd9aa024 | ||
|
|
0e4ea6a335 | ||
|
|
ab1261385b | ||
|
|
52e545f900 | ||
|
|
350abb354b | ||
|
|
87f253997c | ||
|
|
4e9b18fcbf | ||
|
|
e43c371d76 | ||
|
|
a7e85a0bd9 | ||
|
|
7f6f6a69c7 | ||
|
|
87667bdac7 | ||
|
|
bd8edd70bb | ||
|
|
4a3e6b9561 | ||
|
|
f92134fc5f | ||
|
|
9dba2d7f11 | ||
|
|
9289a26f11 | ||
|
|
17054627e9 | ||
|
|
66c4ca072e | ||
|
|
3b221d4fb0 | ||
|
|
4464554f90 | ||
|
|
b15cd433c4 | ||
|
|
4b5a03c6c5 | ||
|
|
ca4badca4d | ||
|
|
1b3dd7540b | ||
|
|
c7183ca080 | ||
|
|
119531e9e2 | ||
|
|
85be7d28d5 | ||
|
|
6c147dae6c | ||
|
|
1fe138c34c | ||
|
|
1258aee464 | ||
|
|
fbb0ed0e67 | ||
|
|
8510e99010 | ||
|
|
342955666a | ||
|
|
50cb421f64 | ||
|
|
16398b7810 | ||
|
|
f3b5aee173 | ||
|
|
e8f85b35c1 | ||
|
|
dd8f13aa8d | ||
|
|
ced2ba7344 | ||
|
|
a2f8534823 | ||
|
|
d05ef70901 | ||
|
|
4bc0c3ed6b | ||
|
|
489d1de756 | ||
|
|
499b993410 | ||
|
|
9f0b16291a | ||
|
|
98a6a2dfe8 | ||
|
|
fb14ba20ca | ||
|
|
b68b984de6 | ||
|
|
1d36dfd932 | ||
|
|
672da64913 | ||
|
|
cb29e164f8 | ||
|
|
1e6a457190 | ||
|
|
cd22dafc89 | ||
|
|
83d8a0e6a2 | ||
|
|
e0b765ba88 | ||
|
|
5e2df18b75 | ||
|
|
9b48334e0e | ||
|
|
c8ae15e92d | ||
|
|
f88ac154a2 | ||
|
|
e66fe8534e | ||
|
|
c2e95bdb6a | ||
|
|
7c5ef8a3d6 | ||
|
|
4a2190f969 | ||
|
|
a3ed236524 | ||
|
|
f4d8e6f4c5 | ||
|
|
360a777a4e | ||
|
|
5ed8b36cf2 | ||
|
|
9540b6c182 | ||
|
|
31f002294e | ||
|
|
ecfc7e8906 | ||
|
|
60bf855e74 | ||
|
|
c3f6e793fa | ||
|
|
48117fddfa | ||
|
|
7e70a5984f | ||
|
|
4589c9f691 | ||
|
|
d81c15b5af | ||
|
|
043ae738d2 | ||
|
|
68184d669e | ||
|
|
fb3ee70c7a | ||
|
|
c5697e6c2b | ||
|
|
58c800181a | ||
|
|
fd023268c5 | ||
|
|
9dc93c7d27 | ||
|
|
c943992e49 | ||
|
|
dd9e92f81f | ||
|
|
60de5b7676 | ||
|
|
6c0430b6e2 | ||
|
|
ddc4345f01 | ||
|
|
41053aa024 | ||
|
|
08ceae235e | ||
|
|
599380dd63 | ||
|
|
a426ee1041 | ||
|
|
939fdf0496 | ||
|
|
3cffc2fdd8 | ||
|
|
22d2170d41 | ||
|
|
50a9de4e2b | ||
|
|
2c12cbaf18 | ||
|
|
ef6826f263 | ||
|
|
ceeaabafbc | ||
|
|
c49f5b157d | ||
|
|
0a71e39253 | ||
|
|
52a0f68c87 | ||
|
|
457fc9da7c | ||
|
|
5b39884c13 | ||
|
|
4d7b1a6c03 | ||
|
|
50b5c67dde | ||
|
|
005ada0237 | ||
|
|
e9b1080734 | ||
|
|
56cc1432e9 | ||
|
|
e3dcc7b0a6 | ||
|
|
37aea8df8f | ||
|
|
f8d64b67ad | ||
|
|
77fc3a9308 | ||
|
|
db3730ea44 | ||
|
|
98ceaa04bb | ||
|
|
e3e4a0a440 | ||
|
|
52150b7257 | ||
|
|
5727fe3a49 | ||
|
|
03ba647dc6 | ||
|
|
be1e9b4421 | ||
|
|
57ce1e98a7 | ||
|
|
3d2d4fd298 | ||
|
|
2258ed1dfa | ||
|
|
2d750f8b9a | ||
|
|
b54e29917e | ||
|
|
3b60a8e02a | ||
|
|
a95d47fef9 | ||
|
|
d385d6c1af | ||
|
|
3453585fd0 | ||
|
|
7b72b85cf7 | ||
|
|
47378c46bd | ||
|
|
647d45c4fa | ||
|
|
6775423b86 | ||
|
|
3413d472ab | ||
|
|
d654e085ae | ||
|
|
802803479e | ||
|
|
639babd9ab | ||
|
|
49f7a5ca1d | ||
|
|
615c70f3bb | ||
|
|
9b4a03c0e8 | ||
|
|
0ccfb5aded | ||
|
|
1966456814 | ||
|
|
925714022f | ||
|
|
1e24734a6c | ||
|
|
c4fc5335d5 | ||
|
|
0133afaef0 | ||
|
|
4d0fd3275b | ||
|
|
5f992cae76 | ||
|
|
5a24e62b51 | ||
|
|
ba1e3a06b6 | ||
|
|
809f5d3ed1 | ||
|
|
125ca61812 | ||
|
|
d38b8ff5f1 | ||
|
|
3f7e9941e2 | ||
|
|
8ba0fe9367 | ||
|
|
cbf75d6339 | ||
|
|
0ea528f750 | ||
|
|
d12b6e0ca9 | ||
|
|
2b1d1ff6ef | ||
|
|
fc7de95d1d | ||
|
|
2bca5deb66 | ||
|
|
8be8c4df56 | ||
|
|
9519ffdac7 | ||
|
|
0b645a4e6a | ||
|
|
67a94621ab | ||
|
|
24d83b05f7 | ||
|
|
178f72c304 | ||
|
|
7dc48fb6d6 | ||
|
|
5017f556fa | ||
|
|
437eeb350c | ||
|
|
cfd792dc70 | ||
|
|
2a4722bd38 | ||
|
|
d381f2daa4 | ||
|
|
b05b4adf57 | ||
|
|
96d72103f2 | ||
|
|
97ffa9610b | ||
|
|
09d6c8601c | ||
|
|
fc409a6884 | ||
|
|
fa63ede4b6 | ||
|
|
fd24c8186e | ||
|
|
0816d07b93 | ||
|
|
c1b9345613 | ||
|
|
7f198d18ac | ||
|
|
802bccd1f3 | ||
|
|
77bd76d1ca | ||
|
|
8dd8b2848c | ||
|
|
c5d54a845a | ||
|
|
f1a19151fc | ||
|
|
963c2d86d3 | ||
|
|
9e6b4083b2 | ||
|
|
f29c4a24c2 | ||
|
|
a75e1c2b2b | ||
|
|
62499483ce | ||
|
|
304bb7b9d1 | ||
|
|
15a069e3c7 | ||
|
|
e0a9323d6e | ||
|
|
a0182c2e39 | ||
|
|
327f816c0e | ||
|
|
040d2a1b36 | ||
|
|
497a48b8b0 | ||
|
|
36a4754430 | ||
|
|
60b9f6f6b2 | ||
|
|
2bdc7b7aca | ||
|
|
fea5d44a57 | ||
|
|
281e3870d3 | ||
|
|
c9b2a7f6d4 | ||
|
|
6314b86d9b | ||
|
|
619d5232db | ||
|
|
da4a3b9a77 | ||
|
|
62284dbd1a | ||
|
|
addb09275a | ||
|
|
9c45a5fdf0 | ||
|
|
0aa29a0fab | ||
|
|
5f778a7570 | ||
|
|
e8f6def246 | ||
|
|
99485fa324 | ||
|
|
483ff4aa8a | ||
|
|
b6c0afc129 | ||
|
|
7def5ea9bf | ||
|
|
4e590ece19 | ||
|
|
c2faa3815a | ||
|
|
52f8753a09 | ||
|
|
a9b8808237 | ||
|
|
26b323e1f6 | ||
|
|
5990bb384a | ||
|
|
be70b83be5 | ||
|
|
7f28c557cd | ||
|
|
b1f9f5f3b3 | ||
|
|
f5740021a1 | ||
|
|
2996cfaf07 | ||
|
|
eda087af99 | ||
|
|
343ef7fd9f | ||
|
|
316daee49d | ||
|
|
68aae1a450 | ||
|
|
d92b2f2ea9 | ||
|
|
e132a22f20 | ||
|
|
ebcf49b504 | ||
|
|
6a2de3cd3b | ||
|
|
8d8da9f286 | ||
|
|
759c2f3f00 | ||
|
|
b140e4bfe6 | ||
|
|
4ef9ea749d | ||
|
|
11f3f5b170 | ||
|
|
b6f88aad80 | ||
|
|
d07b65a03d | ||
|
|
aabc010191 | ||
|
|
cf8e7b10db | ||
|
|
63777464cc | ||
|
|
26fc4609f0 | ||
|
|
1d8d6fe070 | ||
|
|
ca2186a9c5 | ||
|
|
7d8b506c49 | ||
|
|
994de20f49 | ||
|
|
f553601ff1 | ||
|
|
3478715926 | ||
|
|
ed351ee712 | ||
|
|
affa15cbae | ||
|
|
c6d504a5df | ||
|
|
420b34c909 | ||
|
|
4347034ba1 | ||
|
|
ec8e7ed946 | ||
|
|
440fc74368 | ||
|
|
a0255d0f2c | ||
|
|
14216b7454 | ||
|
|
0da033ce95 | ||
|
|
3bd3196a40 | ||
|
|
47e05c32ec | ||
|
|
bcd19509fb | ||
|
|
05a9920638 | ||
|
|
8af3ad5541 | ||
|
|
6ef9e4f479 | ||
|
|
183dad97e7 | ||
|
|
3bbdf17fed | ||
|
|
db021e6711 | ||
|
|
4a58dbf84c | ||
|
|
ee227d85c7 | ||
|
|
d8c0ef8a9c | ||
|
|
53ae00c46f | ||
|
|
dedbf336af | ||
|
|
bc4ffc00b6 | ||
|
|
5662e6c29e | ||
|
|
4cc838accf | ||
|
|
3b58ac79dc | ||
|
|
bab764e2be | ||
|
|
81892ef70d | ||
|
|
cd23aeaa46 | ||
|
|
5d5b731053 | ||
|
|
c973632efd | ||
|
|
e0b63f2baa | ||
|
|
3d406c3c0d | ||
|
|
d4811791fc | ||
|
|
3b7ef63300 | ||
|
|
8adb2f1247 | ||
|
|
326c2aaa90 | ||
|
|
e291687870 | ||
|
|
80f8fb4d98 | ||
|
|
402081e39d | ||
|
|
05309db92b | ||
|
|
d3fa977ebf | ||
|
|
92f7bc8eb5 | ||
|
|
80b6f1dfa3 | ||
|
|
1eaa73bb52 | ||
|
|
0fc831cd82 | ||
|
|
59c18af1f7 | ||
|
|
0fd47c0ad1 | ||
|
|
6b643f0e67 | ||
|
|
3c9fab143c | ||
|
|
f2e953cf18 | ||
|
|
ecb18a6c41 | ||
|
|
d405c43202 | ||
|
|
24e7a368cb | ||
|
|
8848e1e6ce | ||
|
|
820c20af80 | ||
|
|
4b8b951c28 | ||
|
|
8964421350 | ||
|
|
0895f29a83 | ||
|
|
f213729dcb | ||
|
|
6df20010dd | ||
|
|
a5204cdeb1 | ||
|
|
dd129830d3 | ||
|
|
a8233e0521 | ||
|
|
9b2a92106e | ||
|
|
8fe6ab4866 | ||
|
|
a08ff9bf6d | ||
|
|
83b490a968 | ||
|
|
3498c6e260 | ||
|
|
5c93aaf6db | ||
|
|
e860978564 | ||
|
|
2d7a6e9f42 | ||
|
|
1390bc1d04 | ||
|
|
a9349ac49d | ||
|
|
5bed214aef | ||
|
|
4f181b0107 | ||
|
|
8e2f0f613f | ||
|
|
2bb4f91d1c | ||
|
|
389ff844c4 | ||
|
|
cb0542eaad | ||
|
|
300b2973e1 | ||
|
|
5c44582b69 | ||
|
|
2f2a66db2e | ||
|
|
b075d6bbda | ||
|
|
a252914ecd | ||
|
|
87edd1e6f1 | ||
|
|
ba69e1788f | ||
|
|
05538ec28b | ||
|
|
2676c3f641 | ||
|
|
e525919160 | ||
|
|
d064a73131 | ||
|
|
64ab3b0492 | ||
|
|
e1c9d68c68 | ||
|
|
35b9e01230 | ||
|
|
1596714ccb | ||
|
|
e852851273 | ||
|
|
24e9c5bc9f | ||
|
|
dc4f4ad047 | ||
|
|
cc42ab7584 | ||
|
|
03fdb0cb88 | ||
|
|
f7c6518506 | ||
|
|
3c5854fa9f | ||
|
|
900175af47 | ||
|
|
9b9e2289d3 | ||
|
|
ea10936641 | ||
|
|
6b56b0bcf8 | ||
|
|
975eb7d6ae | ||
|
|
4de04c19c0 | ||
|
|
1db12612e6 | ||
|
|
ec010df79a | ||
|
|
88b65625de | ||
|
|
4e28df88ae | ||
|
|
44815435e7 | ||
|
|
fe011ba7b3 | ||
|
|
68035a1256 | ||
|
|
cc535f11c6 | ||
|
|
7001b2fc79 | ||
|
|
05975109a4 | ||
|
|
502443aae8 | ||
|
|
43a82ad835 | ||
|
|
a1db076c02 | ||
|
|
bfc70c3bb8 | ||
|
|
b268b1ed75 | ||
|
|
64769b3e9f | ||
|
|
66e804d71e | ||
|
|
1c30a4f4bc | ||
|
|
be55e3ee80 | ||
|
|
1aa7971ca1 | ||
|
|
6e2ab644a0 | ||
|
|
4fa6c33622 | ||
|
|
3fd405e570 | ||
|
|
c1372852e9 | ||
|
|
95eedc9ec1 | ||
|
|
d7b926d47f | ||
|
|
67039a1a94 | ||
|
|
f79fb16e41 | ||
|
|
d910c05e6e | ||
|
|
4858e06060 | ||
|
|
f71db6816a | ||
|
|
c6b129c68d | ||
|
|
e5dbe01329 | ||
|
|
c00768b7c7 | ||
|
|
ad070c99ef | ||
|
|
aec082c0cf | ||
|
|
cc84452f23 | ||
|
|
84bac2b8d8 | ||
|
|
29a9d227de | ||
|
|
39ae0c775a | ||
|
|
f1eaf2d7aa | ||
|
|
c6840a9a43 | ||
|
|
3a1c64a88e | ||
|
|
d4a0c93c91 | ||
|
|
5ae8347317 | ||
|
|
afbfa4631f | ||
|
|
096f906ac7 | ||
|
|
b844f607ee | ||
|
|
6002bc475b | ||
|
|
24cfdf4594 | ||
|
|
bc5fd1a52f | ||
|
|
fd344a3017 | ||
|
|
f9bd184543 | ||
|
|
f2c1e85b4b | ||
|
|
920b4924b0 | ||
|
|
99e67b1495 | ||
|
|
e65344c9fa | ||
|
|
147fe22f08 | ||
|
|
7f1f1497c6 | ||
|
|
6278c86d02 | ||
|
|
6f09d45a7b | ||
|
|
49a730f77d | ||
|
|
e5a74894d2 | ||
|
|
723dd8358f | ||
|
|
47a22cce2a | ||
|
|
b3791684fd | ||
|
|
ebd3255fc6 | ||
|
|
9b594a4cba | ||
|
|
d0530b8214 | ||
|
|
5c7e968028 | ||
|
|
40ef638e0e | ||
|
|
b9499618d3 | ||
|
|
6618872839 | ||
|
|
b977dd1140 | ||
|
|
71d5c7b4a2 | ||
|
|
dd8a83e592 | ||
|
|
664182def2 | ||
|
|
3f6a13786e | ||
|
|
a800197262 | ||
|
|
0c145d81cf | ||
|
|
7d7f8aabfe | ||
|
|
95c2fcfb00 | ||
|
|
2a6a6c43ce | ||
|
|
47ed86d735 | ||
|
|
50990d1f7a | ||
|
|
f7b484f24f | ||
|
|
f6ad3c9926 | ||
|
|
11b415634c | ||
|
|
d980d38747 | ||
|
|
808671dd59 | ||
|
|
cf22dd60a0 | ||
|
|
3b820bae9f | ||
|
|
60a987c004 | ||
|
|
d90be2dc66 | ||
|
|
e305ecf571 | ||
|
|
ec2bf1a95d | ||
|
|
7d5c44a5bd | ||
|
|
8c4312db72 | ||
|
|
037a72e0f3 | ||
|
|
d657ea48cf | ||
|
|
b4e0c3afcf | ||
|
|
27bc646cf6 | ||
|
|
23ef22e9ba | ||
|
|
84c8da2532 | ||
|
|
e6ec66c844 | ||
|
|
d1c26f9aa0 | ||
|
|
a7704b9038 | ||
|
|
eabd662757 | ||
|
|
764895b9d2 | ||
|
|
8f5b26a2fb | ||
|
|
533cd36ffd | ||
|
|
3a77ebf52d | ||
|
|
0e3bef7e42 | ||
|
|
dda63bd65a | ||
|
|
2aa1b4ead3 | ||
|
|
422b7e0168 | ||
|
|
8053eceb84 | ||
|
|
d10d3c9b56 | ||
|
|
6ff45b1006 | ||
|
|
c901e66e7a | ||
|
|
6ee7f56df3 | ||
|
|
d7073409d4 | ||
|
|
e46ddb8c79 | ||
|
|
f91d0f6bfe | ||
|
|
7e61252d11 | ||
|
|
67c5481965 | ||
|
|
e251fc0803 | ||
|
|
5d4268515c | ||
|
|
91d80f1bb0 | ||
|
|
c9fbc6d52e | ||
|
|
b84ad454e5 | ||
|
|
51ae801cb8 | ||
|
|
cabef43421 | ||
|
|
8fd8df75be | ||
|
|
51f8c1c487 | ||
|
|
aeb9927c20 | ||
|
|
0163b7281a | ||
|
|
0471eb5501 | ||
|
|
2679219d87 | ||
|
|
d83eb72c4b | ||
|
|
414ea10b14 | ||
|
|
d6e29538b1 | ||
|
|
eb9b764cc5 | ||
|
|
db642832f8 | ||
|
|
a56e5e0f36 | ||
|
|
1b0fbf556b | ||
|
|
3e7eddf3ce | ||
|
|
ad24c4f677 |
@@ -15,6 +15,7 @@ module.exports = {
|
||||
'indent': ['error', 4],
|
||||
'max-len': ['error', {'code': 120}],
|
||||
'no-prototype-builtins': 'off',
|
||||
'linebreak-style': ['error', (process.platform === 'win32' ? 'windows' : 'unix')], // https://stackoverflow.com/q/39114446/2771889
|
||||
},
|
||||
'plugins': [
|
||||
'jest',
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: "IMPORTANT: Check development branch changelog first!!"
|
||||
url: https://github.com/Koenkk/zigbee2mqtt/releases/tag/latest-dev
|
||||
about: "Before submitting an issue, check if the issue hasn't already been solved in the development branch. Click 'Open' to see the release notes of the development branch. In case it is, you can read here how to switch to the development branch: https://www.zigbee2mqtt.io/advanced/more/switch-to-dev-branch.html"
|
||||
- name: Questions/discussion
|
||||
url: https://github.com/Koenkk/zigbee2mqtt/discussions/new
|
||||
about: Ask questions, discuss about devices or show things you made
|
||||
|
||||
@@ -8,6 +8,7 @@ body:
|
||||
value: |
|
||||
**IMPORTANT:** Before submitting:
|
||||
- Make sure this device is not already supported in the [dev branch](https://www.zigbee2mqtt.io/advanced/more/switch-to-dev-branch.html)
|
||||
- Make sure there is no existing issue or PR for this device already, search for your device here: https://github.com/Koenkk/zigbee2mqtt/issues
|
||||
- Follow this [guide](https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html)
|
||||
- If you are using the Home Assistant addon and are still on 1.18.1, check the first point of [this](https://github.com/Koenkk/zigbee2mqtt/releases/tag/1.19.0)
|
||||
- type: input
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
name: Wrong device picture/vendor/model/description
|
||||
description: Use if device is detected as supported and is fully functional but has a wrong picture, vendor, model or description
|
||||
title: "[Wrong device]: "
|
||||
labels: [wrong device]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Only use this if the device is detected as **supported** and is **fully functional** but has a wrong picture, vendor, model or description.
|
||||
- type: input
|
||||
id: link
|
||||
attributes:
|
||||
label: Link
|
||||
description: Link of this device (product page)
|
||||
placeholder: https://www.linktomydevice.org
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: model
|
||||
attributes:
|
||||
label: Model
|
||||
description: Expected model, model that is printed on the device, for TuYa device this is NOT something like TS0601 or _TZE200_cf1sl3tj
|
||||
placeholder: RTCGQ01LM
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: Expected description
|
||||
placeholder: Motion sensor
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: vendor
|
||||
attributes:
|
||||
label: Vendor
|
||||
description: Expected vendor
|
||||
placeholder: Xiaomi
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: picture
|
||||
attributes:
|
||||
label: Picture (link)
|
||||
description: Expected picture
|
||||
placeholder: https://www.linktomydevice.org/RTCGQ01LM.jpg
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: database
|
||||
attributes:
|
||||
label: Database entry
|
||||
description: Entry of this device in `data/database.db` after pairing it
|
||||
placeholder: '{"id":53,"type":"Router","ieeeAddr":"0x10458d00024284f69","nwkAddr":10148,"manufId":4151,"manufName":"LUMI","powerSource":"DC Source","modelId":"lumi.relay.c2acn01","epList":[1,2],"endpoints":{"1":{"profId":260,"epId":1,"devId":257,"inClusterList":[0,3,4,5,1,2,10,6,16,2820,12],"outClusterList":[25,10],"clusters":{"genBasic":{"attributes":{"modelId":"lumi.relay.c2acn01","appVersion":1,"manufacturerName":"LUMI","powerSource":4,"zclVersion":0,"stackVersion":2,"hwVersion":18,"dateCode":"8-6-2020"}},"genAnalogInput":{"attributes":{"presentValue":129.04425048828125}},"genOnOff":{"attributes":{"61440":117440715,"onOff":1}}},"binds":[],"configuredReportings":[],"meta":{}},"2":{"profId":260,"epId":2,"devId":257,"inClusterList":[6,16,4,5],"outClusterList":[],"clusters":{"genOnOff":{"attributes":{"61440":237478966,"onOff":0}}},"binds":[],"configuredReportings":[],"meta":{}}},"appVersion":1,"stackVersion":2,"hwVersion":18,"dateCode":"8-6-2020","zclVersion":0,"interviewCompleted":true,"meta":{},"lastSeen":1640285631405}'
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: notes
|
||||
attributes:
|
||||
label: Notes
|
||||
placeholder: Some additional notes...
|
||||
validations:
|
||||
required: false
|
||||
@@ -0,0 +1,13 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
target-branch: dev
|
||||
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/docker"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
target-branch: dev
|
||||
+46
-27
@@ -1,27 +1,22 @@
|
||||
name: CI
|
||||
name: ci
|
||||
|
||||
on: [push, pull_request]
|
||||
on: [pull_request, push]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
- name: Get NodeJS Version
|
||||
id: nodeversion
|
||||
run: |
|
||||
echo "::set-output name=version::$(node --version)"
|
||||
- name: Restore node_modules cache
|
||||
id: cache-node-modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-${{ steps.nodeversion.outputs.version }}-${{ hashFiles('npm-shrinkwrap.json') }}
|
||||
node-version: 20
|
||||
registry-url: https://registry.npmjs.org/
|
||||
cache: npm
|
||||
- name: Install dependencies
|
||||
if: steps.cache-node-modules.outputs.cache-hit != 'true'
|
||||
run: npm ci
|
||||
- name: Build
|
||||
run: npm run build
|
||||
@@ -34,16 +29,16 @@ jobs:
|
||||
run: echo ${{ secrets.DOCKER_KEY }} | docker login -u koenkk --password-stdin
|
||||
- name: Docker login ghcr.io
|
||||
if: (github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push'
|
||||
run: echo ${{ secrets.CR_PAT }} | docker login ghcr.io -u koenkk --password-stdin
|
||||
run: echo ${{ secrets.GH_TOKEN }} | docker login ghcr.io -u koenkk --password-stdin
|
||||
- name: Docker setup - QEMU
|
||||
if: (github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push'
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v2
|
||||
with:
|
||||
platforms: all
|
||||
- name: Docker setup - Buildx
|
||||
if: (github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push'
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
version: latest
|
||||
- name: Docker build dev
|
||||
@@ -51,8 +46,9 @@ jobs:
|
||||
run: |
|
||||
docker buildx build \
|
||||
--build-arg COMMIT=$(git rev-parse --short HEAD) \
|
||||
--platform linux/arm64/v8,linux/amd64,linux/arm/v6,linux/arm/v7 \
|
||||
--platform linux/arm64/v8,linux/386,linux/amd64,linux/arm/v6,linux/arm/v7 \
|
||||
-f docker/Dockerfile \
|
||||
--provenance=false \
|
||||
--push \
|
||||
-t koenkk/zigbee2mqtt:latest-dev -t ghcr.io/koenkk/zigbee2mqtt:latest-dev \
|
||||
.
|
||||
@@ -62,8 +58,9 @@ jobs:
|
||||
TAG="$(git describe --tags)"
|
||||
docker buildx build \
|
||||
--build-arg COMMIT=$(git rev-parse --short HEAD) \
|
||||
--platform linux/arm64/v8,linux/amd64,linux/arm/v6,linux/arm/v7 \
|
||||
--platform linux/arm64/v8,linux/386,linux/amd64,linux/arm/v6,linux/arm/v7 \
|
||||
-f docker/Dockerfile \
|
||||
--provenance=false \
|
||||
--push \
|
||||
-t koenkk/zigbee2mqtt:latest -t "koenkk/zigbee2mqtt:$TAG" -t ghcr.io/koenkk/zigbee2mqtt:latest -t "ghcr.io/koenkk/zigbee2mqtt:$TAG" \
|
||||
.
|
||||
@@ -75,9 +72,31 @@ jobs:
|
||||
- name: Trigger zigbee2mqtt/hassio-zigbee2mqtt build
|
||||
if: github.ref == 'refs/heads/dev' && github.event_name == 'push'
|
||||
run: |
|
||||
curl -s \
|
||||
--request POST \
|
||||
--header "Content-Type: application/json" \
|
||||
--data '{"definition": {"id": 9}}' \
|
||||
-u ${{ secrets.HASSIO_TRIGGER_SECRET }} \
|
||||
"https://dev.azure.com/zigbee2mqtt/Zigbee2mqtt%20Add-on/_apis/build/builds?api-version=5.1"
|
||||
curl \
|
||||
-X POST \
|
||||
-H "Authorization: token ${{ secrets.GH_TOKEN }}" \
|
||||
-H "Accept: application/vnd.github.v3+json" \
|
||||
https://api.github.com/repos/zigbee2mqtt/hassio-zigbee2mqtt/actions/workflows/ci.yml/dispatches \
|
||||
-d '{"ref":"master","inputs":{}}'
|
||||
|
||||
tests:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
node: [16, 18, 20]
|
||||
runs-on: ${{ matrix.os }}
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
registry-url: https://registry.npmjs.org/
|
||||
cache: 'npm'
|
||||
- name: Install dependencies
|
||||
# --ignore-scripts prevents the serialport build which often fails on Windows
|
||||
run: npm ci --ignore-scripts
|
||||
- name: Build
|
||||
run: npm run build
|
||||
- name: Test
|
||||
run: npm run test-with-coverage
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
name: codeql
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
CodeQL-Build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
security-events: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
@@ -0,0 +1,14 @@
|
||||
name: deps-review
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout repository'
|
||||
uses: actions/checkout@v3
|
||||
- name: 'Dependency review'
|
||||
uses: actions/dependency-review-action@v3
|
||||
@@ -0,0 +1,34 @@
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
name: ghcr-cleanup
|
||||
|
||||
permissions: {}
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Delete untagged images
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
const response = await github.request("GET /${{ env.OWNER }}/packages/container/${{ env.PACKAGE_NAME }}/versions",
|
||||
{ per_page: ${{ env.PER_PAGE }}
|
||||
});
|
||||
for(version of response.data) {
|
||||
if (version.metadata.container.tags.length == 0) {
|
||||
try {
|
||||
console.log("delete " + version.id)
|
||||
const deleteResponse = await github.request("DELETE /${{ env.OWNER }}/packages/container/${{ env.PACKAGE_NAME }}/versions/" + version.id, { });
|
||||
console.log("status " + deleteResponse.status)
|
||||
} catch (e) {
|
||||
console.log("failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
env:
|
||||
OWNER: user
|
||||
PACKAGE_NAME: zigbee2mqtt
|
||||
PER_PAGE: 2000
|
||||
@@ -0,0 +1,19 @@
|
||||
name: merge-master-to-dev
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
merge_master_to_dev:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: devmasx/merge-branch@master
|
||||
with:
|
||||
type: now
|
||||
head_to_merge: master
|
||||
target_branch: dev
|
||||
message: "chore: merge master to dev"
|
||||
github_token: ${{ secrets.GH_TOKEN }}
|
||||
@@ -0,0 +1,117 @@
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
name: release-please
|
||||
|
||||
jobs:
|
||||
release-please:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
release_created: ${{ steps.release.outputs.release_created }}
|
||||
version: "${{steps.release.outputs.major}}.${{steps.release.outputs.minor}}.${{steps.release.outputs.patch}}"
|
||||
steps:
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- uses: google-github-actions/release-please-action@v3
|
||||
id: release
|
||||
with:
|
||||
release-type: node
|
||||
package-name: release-please-action
|
||||
default-branch: dev
|
||||
token: ${{secrets.GH_TOKEN}}
|
||||
include-v-in-tag: false
|
||||
skip-github-release: true
|
||||
|
||||
# Checkout repos
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
repository: koenkk/zigbee2mqtt
|
||||
path: ./z2m
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
repository: koenkk/zigbee2mqtt
|
||||
path: ./z2m-master
|
||||
ref: master
|
||||
|
||||
- name: Restore cache commit-user-lookup.json
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: z2m/scripts/commit-user-lookup.json
|
||||
key: commit-user-lookup-dummy
|
||||
restore-keys: |
|
||||
commit-user-lookup-
|
||||
- name: Generate changelog
|
||||
run: |
|
||||
MASTER_Z2M_VERSION=$(cat z2m-master/package.json | jq -r '.version')
|
||||
MASTER_ZHC_VERSION=$(cat z2m-master/package.json | jq -r '.dependencies."zigbee-herdsman-converters"')
|
||||
MASTER_ZH_VERSION=$(cat z2m-master/package.json | jq -r '.dependencies."zigbee-herdsman"')
|
||||
wget -q -O - https://raw.githubusercontent.com/Koenkk/zigbee2mqtt/release-please--branches--dev--components--release-please-action/CHANGELOG.md > z2m/CHANGELOG.md || true
|
||||
cd z2m
|
||||
npm ci
|
||||
node scripts/generateChangelog.js $MASTER_Z2M_VERSION $MASTER_ZHC_VERSION $MASTER_ZH_VERSION >> ../changelog.txt
|
||||
env:
|
||||
GH_TOKEN: ${{secrets.GH_TOKEN}}
|
||||
- name: Update changelog in release PR
|
||||
run: |
|
||||
PR=$(echo '${{ steps.release.outputs.pr }}' | jq -r .number)
|
||||
cd z2m
|
||||
gh pr edit $PR -F ../changelog.txt
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
- name: Upload changelog
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: changelog
|
||||
path: changelog.txt
|
||||
- name: Save cache commit-user-lookup.json
|
||||
uses: actions/cache/save@v3
|
||||
if: always()
|
||||
with:
|
||||
path: z2m/scripts/commit-user-lookup.json
|
||||
key: commit-user-lookup-${{ hashFiles('z2m/scripts/commit-user-lookup.json') }}
|
||||
|
||||
create_release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [release-please]
|
||||
if: ${{ needs.release-please.outputs.release_created }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: dev
|
||||
token: ${{secrets.GH_TOKEN}}
|
||||
- name: Download changelog
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: changelog
|
||||
path: ../changelog
|
||||
- uses: ncipollo/release-action@v1
|
||||
name: Create release
|
||||
with:
|
||||
bodyFile: ../changelog/changelog.txt
|
||||
draft: true
|
||||
name: ${{ needs.release-please.outputs.version }}
|
||||
tag: ${{ needs.release-please.outputs.version }}
|
||||
- name: Merge dev -> master
|
||||
uses: devmasx/merge-branch@master
|
||||
with:
|
||||
type: now
|
||||
from_branch: dev
|
||||
target_branch: master
|
||||
github_token: ${{secrets.GH_TOKEN}}
|
||||
- name: Promote to dev
|
||||
run: |
|
||||
jq '.version = "${{ needs.release-please.outputs.version }}-dev"' package.json > package.json.tmp
|
||||
jq '.version = "${{ needs.release-please.outputs.version }}-dev"' package-lock.json > package-lock.json.tmp
|
||||
mv package.json.tmp package.json
|
||||
mv package-lock.json.tmp package-lock.json
|
||||
- uses: stefanzweifel/git-auto-commit-action@v4
|
||||
with:
|
||||
commit_message: "chore: promote to dev"
|
||||
@@ -1,4 +1,5 @@
|
||||
name: "Close stale issues/pull requests"
|
||||
name: stale
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
@@ -8,12 +9,12 @@ jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v3
|
||||
- uses: actions/stale@v8
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 7 days'
|
||||
stale-pr-message: 'This pull request is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 7 days'
|
||||
days-before-stale: 30
|
||||
days-before-close: 7
|
||||
exempt-issue-labels: dont-stale
|
||||
operations-per-run: 500
|
||||
exempt-issue-labels: 'breaking change'
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
on:
|
||||
repository_dispatch:
|
||||
types: update_dep
|
||||
|
||||
name: update-dep
|
||||
|
||||
permissions: {}
|
||||
jobs:
|
||||
update_dep:
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: dev
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
- run: npm install ${{ github.event.client_payload.package }}@${{ github.event.client_payload.version }} --save-exact
|
||||
- uses: peter-evans/create-pull-request@v5
|
||||
id: cpr
|
||||
with:
|
||||
commit-message: "fix(ignore): update ${{ github.event.client_payload.package }} to ${{ github.event.client_payload.version }}"
|
||||
branch: "deps/${{ github.event.client_payload.package }}"
|
||||
title: Update ${{ github.event.client_payload.package }} to ${{ github.event.client_payload.version }}
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
- run: sleep 5 # Otherwise pull request may not exist yet causing automerge to fail
|
||||
- uses: peter-evans/enable-pull-request-automerge@v3
|
||||
if: steps.cpr.outputs.pull-request-operation == 'created'
|
||||
with:
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
|
||||
merge-method: squash
|
||||
@@ -4,22 +4,31 @@ on:
|
||||
schedule:
|
||||
- cron: "0 0 * * 0"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
update_deps:
|
||||
permissions:
|
||||
contents: write # for peter-evans/create-pull-request to create branch
|
||||
pull-requests: write # for peter-evans/create-pull-request to create a PR
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: dev
|
||||
ssh-key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
- uses: actions/setup-node@v2-beta
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
- run: rm -rf npm-shrinkwrap.json
|
||||
- run: npx npm-check-updates -u
|
||||
- run: npm install && npm shrinkwrap --dev
|
||||
- uses: peter-evans/create-pull-request@v3
|
||||
node-version: 20
|
||||
cache: npm
|
||||
# connect-gzip-static only supports node 16
|
||||
# @types/ws 8.5.5 is broken
|
||||
- run: npx npm-check-updates -u -x connect-gzip-static -x @types/ws
|
||||
- run: npm install
|
||||
- uses: peter-evans/create-pull-request@v5
|
||||
with:
|
||||
commit-message: Update dependencies
|
||||
branch: "update_deps"
|
||||
commit-message: "fix(ignore): update dependencies"
|
||||
branch: "deps/all"
|
||||
title: Update dependencies
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
name: Update zigbee-frontend
|
||||
on:
|
||||
repository_dispatch:
|
||||
types: update_frontend
|
||||
|
||||
jobs:
|
||||
update_frontend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: dev
|
||||
ssh-key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
- uses: actions/setup-node@v2-beta
|
||||
with:
|
||||
node-version: 16
|
||||
- run: npm install zigbee2mqtt-frontend@${{ github.event.client_payload.version }} --save-exact
|
||||
- uses: peter-evans/create-pull-request@v3
|
||||
id: cpr
|
||||
with:
|
||||
commit-message: Update zigbee2mqtt-frontend to ${{ github.event.client_payload.version }}
|
||||
branch: "update_frontend"
|
||||
title: Update zigbee-frontend to ${{ github.event.client_payload.version }}
|
||||
- run: sleep 5 # Otherwise pull request may not exist yet causing automerge to fail
|
||||
- uses: peter-evans/enable-pull-request-automerge@v1
|
||||
if: steps.cpr.outputs.pull-request-operation == 'created'
|
||||
with:
|
||||
token: ${{ secrets.CR_PAT }}
|
||||
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
|
||||
merge-method: squash
|
||||
@@ -1,30 +0,0 @@
|
||||
name: Update zigbee-herdsman
|
||||
on:
|
||||
repository_dispatch:
|
||||
types: update_zh
|
||||
|
||||
jobs:
|
||||
update_zh:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: dev
|
||||
ssh-key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
- uses: actions/setup-node@v2-beta
|
||||
with:
|
||||
node-version: 16
|
||||
- run: npm install zigbee-herdsman@${{ github.event.client_payload.version }} --save-exact
|
||||
- uses: peter-evans/create-pull-request@v3
|
||||
id: cpr
|
||||
with:
|
||||
commit-message: Update zigbee-herdsman to ${{ github.event.client_payload.version }}
|
||||
branch: "update_zh"
|
||||
title: Update zigbee-herdsman to ${{ github.event.client_payload.version }}
|
||||
- run: sleep 5 # Otherwise pull request may not exist yet causing automerge to fail
|
||||
- uses: peter-evans/enable-pull-request-automerge@v1
|
||||
if: steps.cpr.outputs.pull-request-operation == 'created'
|
||||
with:
|
||||
token: ${{ secrets.CR_PAT }}
|
||||
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
|
||||
merge-method: squash
|
||||
@@ -1,30 +0,0 @@
|
||||
name: Update zigbee-herdsman-converters
|
||||
on:
|
||||
repository_dispatch:
|
||||
types: update_zhc
|
||||
|
||||
jobs:
|
||||
update_zhc:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: dev
|
||||
ssh-key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
- uses: actions/setup-node@v2-beta
|
||||
with:
|
||||
node-version: 16
|
||||
- run: npm install zigbee-herdsman-converters@${{ github.event.client_payload.version }} --save-exact
|
||||
- uses: peter-evans/create-pull-request@v3
|
||||
id: cpr
|
||||
with:
|
||||
commit-message: Update zigbee-herdsman-converters to ${{ github.event.client_payload.version }}
|
||||
branch: "update_zhc"
|
||||
title: Update zigbee-herdsman-converters to ${{ github.event.client_payload.version }}
|
||||
- run: sleep 5 # Otherwise pull request may not exist yet causing automerge to fail
|
||||
- uses: peter-evans/enable-pull-request-automerge@v1
|
||||
if: steps.cpr.outputs.pull-request-operation == 'created'
|
||||
with:
|
||||
token: ${{ secrets.CR_PAT }}
|
||||
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
|
||||
merge-method: squash
|
||||
@@ -80,3 +80,6 @@ data/extension
|
||||
|
||||
# Ignore config
|
||||
data/*.yaml
|
||||
|
||||
# commit-user-lookup.json
|
||||
scripts/commit-user-lookup.json
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
".": "1.32.2",
|
||||
"bootstrap-sha": "b2dd21e164b457d5cd0c9d455cdb879d881c44bd"
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
# Changelog
|
||||
|
||||
## [1.32.2](https://github.com/Koenkk/zigbee2mqtt/compare/1.32.1...1.32.2) (2023-08-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fix `'dict object' has no attribute 'ir_code_to_send'` error. https://github.com/Koenkk/zigbee2mqtt/issues/18180 ([dec9191](https://github.com/Koenkk/zigbee2mqtt/commit/dec91919bc50aed1f1d3fd9525abd2db49242e7f))
|
||||
* Fix insecure Zigbee network encryption key generation ([#18372](https://github.com/Koenkk/zigbee2mqtt/issues/18372)) ([df8e168](https://github.com/Koenkk/zigbee2mqtt/commit/df8e1687b2db3f49ba8c3d97a4c92b7892a54f1e))
|
||||
* Fix not all cluster unbound/bound when specified. https://github.com/Koenkk/zigbee2mqtt/issues/10740 ([0bdf663](https://github.com/Koenkk/zigbee2mqtt/commit/0bdf663ca126d0569b62871e1b787992472faea4))
|
||||
* Fix socket error crashing Zigbee2MQTT ([#18388](https://github.com/Koenkk/zigbee2mqtt/issues/18388)) ([f64cd7e](https://github.com/Koenkk/zigbee2mqtt/commit/f64cd7e8f38f0c1fc1bbd85b5b5caf560565c024))
|
||||
* **ignore:** Add ep `l` till 24 ([#18422](https://github.com/Koenkk/zigbee2mqtt/issues/18422)) ([4bd281c](https://github.com/Koenkk/zigbee2mqtt/commit/4bd281ce6cbd43f38b2a3131cd38b25885a22b3a))
|
||||
* **ignore:** Add th1-th10 endpoints for Ubisys H10 ([#18405](https://github.com/Koenkk/zigbee2mqtt/issues/18405)) ([91f22f1](https://github.com/Koenkk/zigbee2mqtt/commit/91f22f1230241f5fb3b27b2ee84d944a1c3c5107))
|
||||
* **ignore:** Attempt 2 to fix cache saving ([ea412cf](https://github.com/Koenkk/zigbee2mqtt/commit/ea412cfe82a0462930ec8dddf00b9a363c484b73))
|
||||
* **ignore:** Attempt 3 to fix commit-user-lookup ([590564c](https://github.com/Koenkk/zigbee2mqtt/commit/590564c15888f73826f20eca91e2c253c7d000e7))
|
||||
* **ignore:** Attempt to fix cache not saving ([fa02bb5](https://github.com/Koenkk/zigbee2mqtt/commit/fa02bb5b74216db78b23db144c915d00c7dc48c1))
|
||||
* **ignore:** fix commit-user-lookup cache ([758b814](https://github.com/Koenkk/zigbee2mqtt/commit/758b8144a19ca04eac5faae5b7e16c090e9fda77))
|
||||
* **ignore:** Fix failing test ([ba0cc61](https://github.com/Koenkk/zigbee2mqtt/commit/ba0cc6196550b3b6873f3733690daef9002c5863))
|
||||
* **ignore:** update dependencies ([#18260](https://github.com/Koenkk/zigbee2mqtt/issues/18260)) ([ee1a5f0](https://github.com/Koenkk/zigbee2mqtt/commit/ee1a5f04008beaac54fb68e20c31c1664563a4f2))
|
||||
* **ignore:** update dependencies ([#18403](https://github.com/Koenkk/zigbee2mqtt/issues/18403)) ([8e94662](https://github.com/Koenkk/zigbee2mqtt/commit/8e946623a9db6282a53b55d5d5f3e4b000d080ee))
|
||||
* **ignore:** update zigbee-herdsman to 0.17.2 ([#18291](https://github.com/Koenkk/zigbee2mqtt/issues/18291)) ([073989c](https://github.com/Koenkk/zigbee2mqtt/commit/073989c7a434c3fd73abaee95fa4ada9394d695d))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.38.0 ([#18262](https://github.com/Koenkk/zigbee2mqtt/issues/18262)) ([813dd49](https://github.com/Koenkk/zigbee2mqtt/commit/813dd49a41e7d3f41b6c79db708bb08d29cd7f0c))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.39.0 ([#18282](https://github.com/Koenkk/zigbee2mqtt/issues/18282)) ([3c855d2](https://github.com/Koenkk/zigbee2mqtt/commit/3c855d20424761d43ace9b5ed55460eedce092f0))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.39.1 ([#18292](https://github.com/Koenkk/zigbee2mqtt/issues/18292)) ([a3c8e1c](https://github.com/Koenkk/zigbee2mqtt/commit/a3c8e1c3c98bd97ef52c29929dc9bb5f9598d0fd))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.40.0 ([#18314](https://github.com/Koenkk/zigbee2mqtt/issues/18314)) ([af0521b](https://github.com/Koenkk/zigbee2mqtt/commit/af0521b54ca9195e62d68ccd9fcb49d0d0c226e4))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.41.0 ([#18328](https://github.com/Koenkk/zigbee2mqtt/issues/18328)) ([330a913](https://github.com/Koenkk/zigbee2mqtt/commit/330a913e76b556295f57290743670a40e48139ca))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.42.0 ([#18343](https://github.com/Koenkk/zigbee2mqtt/issues/18343)) ([313865b](https://github.com/Koenkk/zigbee2mqtt/commit/313865b0a38754df52ef2f28132053a47379cc43))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.43.0 ([#18356](https://github.com/Koenkk/zigbee2mqtt/issues/18356)) ([7a22cee](https://github.com/Koenkk/zigbee2mqtt/commit/7a22cee639bf9115c6782302900b89f4978468d9))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.44.0 ([#18365](https://github.com/Koenkk/zigbee2mqtt/issues/18365)) ([3408e3c](https://github.com/Koenkk/zigbee2mqtt/commit/3408e3c05a102d32818260c0368a47c9726aa40d))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.45.0 ([#18404](https://github.com/Koenkk/zigbee2mqtt/issues/18404)) ([57d031d](https://github.com/Koenkk/zigbee2mqtt/commit/57d031d54aadaf87513d2c7d32f3302db501be67))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.46.0 ([#18421](https://github.com/Koenkk/zigbee2mqtt/issues/18421)) ([ced3d43](https://github.com/Koenkk/zigbee2mqtt/commit/ced3d43dad5f8a578f1875ffaf68ffb1d177c584))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.47.0 ([#18430](https://github.com/Koenkk/zigbee2mqtt/issues/18430)) ([087b85c](https://github.com/Koenkk/zigbee2mqtt/commit/087b85cb9f3bc7a317dc22483461de32a9c1a0d8))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.48.0 ([#18438](https://github.com/Koenkk/zigbee2mqtt/issues/18438)) ([d487f37](https://github.com/Koenkk/zigbee2mqtt/commit/d487f37a7a755e37caf3a8da4f8190981fdb5625))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.49.0 ([#18443](https://github.com/Koenkk/zigbee2mqtt/issues/18443)) ([8139d7f](https://github.com/Koenkk/zigbee2mqtt/commit/8139d7f1f494b4051c1bba7ea453e76e4be77fad))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.50.0 ([#18475](https://github.com/Koenkk/zigbee2mqtt/issues/18475)) ([3478e40](https://github.com/Koenkk/zigbee2mqtt/commit/3478e4001760541b65d9bfdbdcd1d082a3da8023))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.50.1 ([#18487](https://github.com/Koenkk/zigbee2mqtt/issues/18487)) ([683d29f](https://github.com/Koenkk/zigbee2mqtt/commit/683d29f9f70f3149cbbff6b543d53af0f931e706))
|
||||
* **ignore:** update zigbee2mqtt-frontend to 0.6.132 ([#18289](https://github.com/Koenkk/zigbee2mqtt/issues/18289)) ([05eff95](https://github.com/Koenkk/zigbee2mqtt/commit/05eff957eb3c5414df7bf2909b5b6d9ff6fdb258))
|
||||
* **ignore:** update zigbee2mqtt-frontend to 0.6.133 ([#18390](https://github.com/Koenkk/zigbee2mqtt/issues/18390)) ([e1d52ae](https://github.com/Koenkk/zigbee2mqtt/commit/e1d52ae43aa132f296d151eced7d208cfb1b3a62))
|
||||
* Use HA sensor device class `atmospheric_pressure` instead of `pressure` ([#18306](https://github.com/Koenkk/zigbee2mqtt/issues/18306)) ([1252f10](https://github.com/Koenkk/zigbee2mqtt/commit/1252f10d39beed640370410a45d5c2fdc504b440))
|
||||
|
||||
## [1.32.1](https://github.com/Koenkk/zigbee2mqtt/compare/1.32.0...1.32.1) (2023-07-08)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.33.1-hotfix.0 ([15601d6](https://github.com/Koenkk/zigbee2mqtt/commit/15601d6339cfc7408872a71071a98abadd9b33ff))
|
||||
|
||||
## [1.32.0](https://github.com/Koenkk/zigbee2mqtt/compare/1.31.2...1.32.0) (2023-07-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Support Home Assistant water device_class ([#18066](https://github.com/Koenkk/zigbee2mqtt/issues/18066)) ([e083a6b](https://github.com/Koenkk/zigbee2mqtt/commit/e083a6bff7fafd17d5ff6ad4b87916b83727d8be))
|
||||
* Support settable Home Assistant text ([#18114](https://github.com/Koenkk/zigbee2mqtt/issues/18114)) ([9474452](https://github.com/Koenkk/zigbee2mqtt/commit/94744525ba466c6512ab6a6181b3073970a02023))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fix Home Assistant truncate error when `program` is `null`. https://github.com/Koenkk/zigbee2mqtt/issues/16460 ([11f0be5](https://github.com/Koenkk/zigbee2mqtt/commit/11f0be55d871363cb4c07adbbeac57fb4ddf479e))
|
||||
* **ignore:** Better fix for [#17891](https://github.com/Koenkk/zigbee2mqtt/issues/17891) ([#17951](https://github.com/Koenkk/zigbee2mqtt/issues/17951)) ([a7e02a7](https://github.com/Koenkk/zigbee2mqtt/commit/a7e02a7be847418df463095b68951c1a1459ca83))
|
||||
* **ignore:** Fix DeviceMessage.meta type ([8833947](https://github.com/Koenkk/zigbee2mqtt/commit/88339473ce7eb09add8f1836ab4c33b4a405d7e7))
|
||||
* **ignore:** Fix e10dd893ab2875e5420952b4452bf75aeec93922 ([446dbbf](https://github.com/Koenkk/zigbee2mqtt/commit/446dbbf1fc0e07c61cd233c8b4ea509ce6423c86))
|
||||
* **ignore:** update dependencies ([#17991](https://github.com/Koenkk/zigbee2mqtt/issues/17991)) ([e10dd89](https://github.com/Koenkk/zigbee2mqtt/commit/e10dd893ab2875e5420952b4452bf75aeec93922))
|
||||
* **ignore:** update dependencies ([#18052](https://github.com/Koenkk/zigbee2mqtt/issues/18052)) ([1dc0b24](https://github.com/Koenkk/zigbee2mqtt/commit/1dc0b24e9a8ae9a03809aff5d3ff9758b63d9f22))
|
||||
* **ignore:** update dependencies ([#18112](https://github.com/Koenkk/zigbee2mqtt/issues/18112)) ([57566c7](https://github.com/Koenkk/zigbee2mqtt/commit/57566c76ed8ccaabcdc32f84a7fddcf5da3b02b3))
|
||||
* **ignore:** update zigbee-herdsman to 0.15.0 ([#17966](https://github.com/Koenkk/zigbee2mqtt/issues/17966)) ([05cab61](https://github.com/Koenkk/zigbee2mqtt/commit/05cab61e1faa15a9a72d87f8e9ec26e3d1eb4046))
|
||||
* **ignore:** update zigbee-herdsman to 0.15.1 ([#18080](https://github.com/Koenkk/zigbee2mqtt/issues/18080)) ([17cff2a](https://github.com/Koenkk/zigbee2mqtt/commit/17cff2a26f16207bd30bb2659e11ce792d02dd75))
|
||||
* **ignore:** update zigbee-herdsman to 0.15.2 ([#18091](https://github.com/Koenkk/zigbee2mqtt/issues/18091)) ([03005b3](https://github.com/Koenkk/zigbee2mqtt/commit/03005b38c86f79384e20bb240a8947ce969411c6))
|
||||
* **ignore:** update zigbee-herdsman to 0.15.3 ([#18116](https://github.com/Koenkk/zigbee2mqtt/issues/18116)) ([2d62027](https://github.com/Koenkk/zigbee2mqtt/commit/2d62027e24ea2de401155e0bc7fabaee85fa7bab))
|
||||
* **ignore:** update zigbee-herdsman to 0.16.0 ([#18124](https://github.com/Koenkk/zigbee2mqtt/issues/18124)) ([38c2ea5](https://github.com/Koenkk/zigbee2mqtt/commit/38c2ea5bf72f67f29b89dc84524e91923989fd27))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.20.0 ([#17954](https://github.com/Koenkk/zigbee2mqtt/issues/17954)) ([0c7e7e5](https://github.com/Koenkk/zigbee2mqtt/commit/0c7e7e5089d1023150f23123df7eab90ea65fe59))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.22.0 ([#18004](https://github.com/Koenkk/zigbee2mqtt/issues/18004)) ([b0b3aee](https://github.com/Koenkk/zigbee2mqtt/commit/b0b3aee6483efe9fbb2f434c44ed76d6b3a4e54a))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.23.0 ([#18015](https://github.com/Koenkk/zigbee2mqtt/issues/18015)) ([4ad838c](https://github.com/Koenkk/zigbee2mqtt/commit/4ad838c114edc7dd5d9d104df9b2ce49933a8c20))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.24.0 ([#18025](https://github.com/Koenkk/zigbee2mqtt/issues/18025)) ([c47f5df](https://github.com/Koenkk/zigbee2mqtt/commit/c47f5df021478e8997f13f06ea17591b745615e3))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.25.0 ([#18056](https://github.com/Koenkk/zigbee2mqtt/issues/18056)) ([57aaa5a](https://github.com/Koenkk/zigbee2mqtt/commit/57aaa5a85aff308d4bd66353fd42969eb38a60d4))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.26.0 ([#18067](https://github.com/Koenkk/zigbee2mqtt/issues/18067)) ([91d67a0](https://github.com/Koenkk/zigbee2mqtt/commit/91d67a0ef5135e08ac81171b78931b7903efcc5a))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.27.0 ([#18075](https://github.com/Koenkk/zigbee2mqtt/issues/18075)) ([5777d76](https://github.com/Koenkk/zigbee2mqtt/commit/5777d76e5508d7dba4750aa5efc34977ba01de1a))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.28.0 ([#18092](https://github.com/Koenkk/zigbee2mqtt/issues/18092)) ([6d3389b](https://github.com/Koenkk/zigbee2mqtt/commit/6d3389b409b003740ac4b2b90aa39a8ccbec26e3))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.29.0 ([#18099](https://github.com/Koenkk/zigbee2mqtt/issues/18099)) ([a744838](https://github.com/Koenkk/zigbee2mqtt/commit/a744838e9c982461479f0b5273e25abd32c4074d))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.30.0 ([#18115](https://github.com/Koenkk/zigbee2mqtt/issues/18115)) ([a56719d](https://github.com/Koenkk/zigbee2mqtt/commit/a56719d19ca32fabf375e293cdef0172826c8672))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.31.0 ([#18125](https://github.com/Koenkk/zigbee2mqtt/issues/18125)) ([ed85367](https://github.com/Koenkk/zigbee2mqtt/commit/ed85367e5383b9a59a69fe2836b02763f6744849))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.32.0 ([#18132](https://github.com/Koenkk/zigbee2mqtt/issues/18132)) ([61dd1dd](https://github.com/Koenkk/zigbee2mqtt/commit/61dd1dd96dc9154f2b46a635bf2ccaeac2d69ba5))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.33.0 ([#18156](https://github.com/Koenkk/zigbee2mqtt/issues/18156)) ([81973c3](https://github.com/Koenkk/zigbee2mqtt/commit/81973c3a2a356c52859d167d3eedaf78f747b8e5))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.33.1 ([#18161](https://github.com/Koenkk/zigbee2mqtt/issues/18161)) ([d1824f1](https://github.com/Koenkk/zigbee2mqtt/commit/d1824f169b71e0590dce14d7f451dbbb67bdf5d9))
|
||||
|
||||
## [1.31.0](https://github.com/Koenkk/zigbee2mqtt/compare/1.30.4...v1.31.0) (2023-06-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **ignore:** update CI/CD ([b3de973](https://github.com/Koenkk/zigbee2mqtt/commit/b3de973af66e7c308987adeaab4d89759d4bfef0))
|
||||
* **ignore:** update CI/CD ([0d17d26](https://github.com/Koenkk/zigbee2mqtt/commit/0d17d2613357db225f224bdbf191fbd9ad17b4a5))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.6.0 ([#17774](https://github.com/Koenkk/zigbee2mqtt/issues/17774)) ([24baed3](https://github.com/Koenkk/zigbee2mqtt/commit/24baed3053d91276369d0cad02e96afee97329f8))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.7.0 ([#17777](https://github.com/Koenkk/zigbee2mqtt/issues/17777)) ([1344ae0](https://github.com/Koenkk/zigbee2mqtt/commit/1344ae06163f5b6a3c68e532f39b379158040c6f))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fix Home Assistant `Payload is not supported (e.g. open, closed, opening, closing, stopped): STOP` error. https://github.com/Koenkk/zigbee2mqtt/issues/17552 ([e6f9aed](https://github.com/Koenkk/zigbee2mqtt/commit/e6f9aeda06ec96c76c3d455bb016a10073d0bf54))
|
||||
* **ignore:** Fix build ([#17838](https://github.com/Koenkk/zigbee2mqtt/issues/17838)) ([d119f60](https://github.com/Koenkk/zigbee2mqtt/commit/d119f609de683313335fbb37e2ce358a16fdc597))
|
||||
* **ignore:** update dependencies ([#17811](https://github.com/Koenkk/zigbee2mqtt/issues/17811)) ([a71857c](https://github.com/Koenkk/zigbee2mqtt/commit/a71857c7236c66da577352160de96eacc683c7c3))
|
||||
* **ignore:** update zigbee-herdsman to 0.14.117 ([#17770](https://github.com/Koenkk/zigbee2mqtt/issues/17770)) ([8457c37](https://github.com/Koenkk/zigbee2mqtt/commit/8457c3735b2f2c9c5106ce76fc7d2988f9eb1ea1))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.10.0 ([#17797](https://github.com/Koenkk/zigbee2mqtt/issues/17797)) ([b7b3b51](https://github.com/Koenkk/zigbee2mqtt/commit/b7b3b51cfeb9b672f2cca128733b273f50966e19))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.11.0 ([#17808](https://github.com/Koenkk/zigbee2mqtt/issues/17808)) ([2e8dfbb](https://github.com/Koenkk/zigbee2mqtt/commit/2e8dfbb96ecbebece84cc1aab12899162e27f82e))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.12.0 ([#17837](https://github.com/Koenkk/zigbee2mqtt/issues/17837)) ([be82b73](https://github.com/Koenkk/zigbee2mqtt/commit/be82b737be5f7a7e58d653628ce66e0813fad111))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.13.0 ([#17842](https://github.com/Koenkk/zigbee2mqtt/issues/17842)) ([b228d9d](https://github.com/Koenkk/zigbee2mqtt/commit/b228d9daf64c8d315803120814dc31272ee1c459))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.13.1 ([#17851](https://github.com/Koenkk/zigbee2mqtt/issues/17851)) ([4b20dfa](https://github.com/Koenkk/zigbee2mqtt/commit/4b20dfafab6193f328aa9133d2b063284abf6cbf))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.8.2 ([#17789](https://github.com/Koenkk/zigbee2mqtt/issues/17789)) ([8b579b6](https://github.com/Koenkk/zigbee2mqtt/commit/8b579b6533876f3b7440dc74b2b709cbfc7ccaf3))
|
||||
* **ignore:** update zigbee-herdsman-converters to 15.9.0 ([#17791](https://github.com/Koenkk/zigbee2mqtt/issues/17791)) ([5b807c3](https://github.com/Koenkk/zigbee2mqtt/commit/5b807c36eced55e790a8393667e07680736f37be))
|
||||
@@ -53,8 +53,18 @@ Zigbee2MQTT integrates well with (almost) every home automation solution because
|
||||
<img align="left" height="100px" width="100px" src="https://user-images.githubusercontent.com/7738048/40914297-49e6e560-6800-11e8-8904-36cce896e5a8.png">
|
||||
|
||||
### [Home Assistant](https://www.home-assistant.io/)
|
||||
- [Home Assistant](https://www.home-assistant.io/hassio/): Using [the official addon](https://github.com/zigbee2mqtt/hassio-zigbee2mqtt)
|
||||
- Generic install or Hassbian: Using instructions [here](https://www.zigbee2mqtt.io/guide/usage/integrations/home_assistant.html)
|
||||
- [Home Assistant OS](https://www.home-assistant.io/installation/): Using [the official addon](https://github.com/zigbee2mqtt/hassio-zigbee2mqtt)
|
||||
- Other installation: using instructions [here](https://www.zigbee2mqtt.io/guide/usage/integrations/home_assistant.html)
|
||||
|
||||
<br>
|
||||
|
||||
<img align="left" height="100px" width="100px" src="https://etc.athom.com/logo/white/256.png">
|
||||
|
||||
### [Homey](https://homey.app/)
|
||||
- Integration implemented in the [Homey App](https://homey.app/nl-nl/app/com.gruijter.zigbee2mqtt/)
|
||||
- Documentation and support in the [Homey Forum](https://community.homey.app/t/83214)
|
||||
|
||||
<br>
|
||||
|
||||
<img align="left" height="100px" width="100px" src="https://user-images.githubusercontent.com/2734836/47615848-b8dd8700-dabd-11e8-9d77-175002dd8987.png">
|
||||
|
||||
@@ -63,6 +73,20 @@ Zigbee2MQTT integrates well with (almost) every home automation solution because
|
||||
|
||||
<br>
|
||||
|
||||
<img align="left" height="100px" width="100px" src="./images/gladys-assistant-logo.jpg">
|
||||
|
||||
### [Gladys Assistant](https://gladysassistant.com/)
|
||||
- Integration implemented natively in Gladys Assistant. Read documentation [here](https://gladysassistant.com/docs/integrations/zigbee2mqtt/)
|
||||
|
||||
<br>
|
||||
|
||||
<img align="left" height="100px" width="100px" src="https://forum.iobroker.net/assets/uploads/system/site-logo.png">
|
||||
|
||||
### [IoBroker](https://www.iobroker.net/)
|
||||
- Integration implemented in IoBroker. Read documentation [here](https://github.com/o0shojo0o/ioBroker.zigbee2mqtt)
|
||||
|
||||
<br>
|
||||
|
||||
## Architecture
|
||||

|
||||
|
||||
@@ -73,7 +97,7 @@ Zigbee2MQTT is made up of three modules, each developed in its own Github projec
|
||||
Zigbee2MQTT uses TypeScript (partially for now). Therefore after making changes to files in the `lib/` directory you need to recompile Zigbee2MQTT. This can be done by executing `npm run build`. For faster development instead of running `npm run build` you can run `npm run build-watch` in another terminal session, this will recompile as you change files.
|
||||
|
||||
## Supported devices
|
||||
See [Supported devices](https://www.zigbee2mqtt.io/supported-devices) to check whether your device is supported. There is quite an extensive list, including devices from vendors like Xiaomi, Ikea, Philips, OSRAM and more.
|
||||
See [Supported devices](https://www.zigbee2mqtt.io/supported-devices) to check whether your device is supported. There is quite an extensive list, including devices from vendors like [Xiaomi](https://www.zigbee2mqtt.io/supported-devices/#v=Xiaomi), [Ikea](https://www.zigbee2mqtt.io/supported-devices/#v=IKEA), [Philips](https://www.zigbee2mqtt.io/supported-devices/#v=Philips), [OSRAM](https://www.zigbee2mqtt.io/supported-devices/#v=OSRAM) and more.
|
||||
|
||||
If it's not listed in [Supported devices](https://www.zigbee2mqtt.io/supported-devices), support can be added (fairly) easily, see [How to support new devices](https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html).
|
||||
|
||||
|
||||
+10
-11
@@ -1,29 +1,26 @@
|
||||
FROM node:16-alpine3.15 as base
|
||||
FROM alpine:3.18.2 as base
|
||||
|
||||
WORKDIR /app
|
||||
RUN apk add --no-cache tzdata eudev tini
|
||||
|
||||
COPY package.json ./
|
||||
RUN apk add --no-cache tzdata eudev tini nodejs
|
||||
|
||||
# Dependencies and build
|
||||
FROM base as dependencies_and_build
|
||||
|
||||
COPY npm-shrinkwrap.json tsconfig.json index.js ./
|
||||
COPY package*.json tsconfig.json index.js ./
|
||||
COPY lib ./lib
|
||||
|
||||
RUN apk add --no-cache --virtual .buildtools make gcc g++ python3 linux-headers git && \
|
||||
npm ci --no-audit --no-optional --no-update-notifier && \
|
||||
npm run build && \
|
||||
rm -rf node_modules && \
|
||||
RUN apk add --no-cache --virtual .buildtools make gcc g++ python3 linux-headers git npm && \
|
||||
npm ci --production --no-audit --no-optional --no-update-notifier && \
|
||||
# Serialport needs to be rebuild for Alpine https://serialport.io/docs/9.x.x/guide-installation#alpine-linux
|
||||
npm rebuild --build-from-source && \
|
||||
apk del .buildtools
|
||||
|
||||
# Release
|
||||
FROM base as release
|
||||
|
||||
COPY --from=dependencies_and_build /app/node_modules ./node_modules
|
||||
COPY --from=dependencies_and_build /app/dist ./dist
|
||||
COPY LICENSE index.js data/configuration.yaml ./
|
||||
COPY dist ./dist
|
||||
COPY package.json LICENSE index.js data/configuration.yaml ./
|
||||
|
||||
COPY docker/docker-entrypoint.sh /usr/local/bin/
|
||||
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||
@@ -33,5 +30,7 @@ RUN mkdir /app/data
|
||||
ARG COMMIT
|
||||
RUN echo "$COMMIT" > dist/.hash
|
||||
|
||||
ENV NODE_ENV production
|
||||
|
||||
ENTRYPOINT ["docker-entrypoint.sh"]
|
||||
CMD [ "/sbin/tini", "--", "node", "index.js"]
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 69 KiB |
@@ -1,12 +1,10 @@
|
||||
require('core-js/features/object/from-entries');
|
||||
require('core-js/features/array/flat');
|
||||
const semver = require('semver');
|
||||
const engines = require('./package.json').engines;
|
||||
const indexJsRestart = 'indexjs.restart';
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const {exec} = require('child_process');
|
||||
const rimraf = require('rimraf');
|
||||
const {rimrafSync} = require('rimraf');
|
||||
require('source-map-support').install();
|
||||
|
||||
let controller;
|
||||
@@ -15,12 +13,12 @@ let stopping = false;
|
||||
const hashFile = path.join(__dirname, 'dist', '.hash');
|
||||
|
||||
async function restart() {
|
||||
await stop(indexJsRestart);
|
||||
await stop(true);
|
||||
await start();
|
||||
}
|
||||
|
||||
async function exit(code, reason) {
|
||||
if (reason !== indexJsRestart) {
|
||||
async function exit(code, restart) {
|
||||
if (!restart) {
|
||||
process.exit(code);
|
||||
}
|
||||
}
|
||||
@@ -43,8 +41,16 @@ async function writeHash() {
|
||||
async function build(reason) {
|
||||
return new Promise((resolve, reject) => {
|
||||
process.stdout.write(`Building Zigbee2MQTT... (${reason})`);
|
||||
rimraf.sync('dist');
|
||||
exec('npm run build', {cwd: __dirname}, async (err, stdout, stderr) => {
|
||||
rimrafSync('dist');
|
||||
const env = {...process.env};
|
||||
const _600mb = 629145600;
|
||||
if (_600mb > os.totalmem() && !env.NODE_OPTIONS) {
|
||||
// Prevent OOM on tsc compile for system with low memory
|
||||
// https://github.com/Koenkk/zigbee2mqtt/issues/12034
|
||||
env.NODE_OPTIONS = '--max_old_space_size=256';
|
||||
}
|
||||
|
||||
exec('npm run build', {env, cwd: __dirname}, async (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
process.stdout.write(', failed\n');
|
||||
if (err.code === 134) {
|
||||
@@ -101,21 +107,27 @@ async function start() {
|
||||
await controller.start();
|
||||
}
|
||||
|
||||
async function stop(reason=null) {
|
||||
await controller.stop(reason);
|
||||
async function stop(restart) {
|
||||
await controller.stop(restart);
|
||||
}
|
||||
|
||||
async function handleQuit() {
|
||||
if (!stopping && controller) {
|
||||
stopping = true;
|
||||
await stop();
|
||||
await stop(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (process.argv.length === 3 && process.argv[2] === 'writehash') {
|
||||
writeHash();
|
||||
if (require.main === module || require.main.filename.endsWith(path.sep + 'cli.js')) {
|
||||
if (process.argv.length === 3 && process.argv[2] === 'writehash') {
|
||||
writeHash();
|
||||
} else {
|
||||
process.on('SIGINT', handleQuit);
|
||||
process.on('SIGTERM', handleQuit);
|
||||
start();
|
||||
}
|
||||
} else {
|
||||
process.on('SIGINT', handleQuit);
|
||||
process.on('SIGTERM', handleQuit);
|
||||
start();
|
||||
module.exports = {start};
|
||||
}
|
||||
|
||||
+22
-19
@@ -39,24 +39,25 @@ const AllExtensions = [
|
||||
type ExtensionArgs = [Zigbee, MQTT, State, PublishEntityState, EventBus,
|
||||
(enable: boolean, name: string) => Promise<void>, () => void, (extension: Extension) => Promise<void>];
|
||||
|
||||
class Controller {
|
||||
export class Controller {
|
||||
private eventBus: EventBus;
|
||||
private zigbee: Zigbee;
|
||||
private state: State;
|
||||
private mqtt: MQTT;
|
||||
private restartCallback: () => void;
|
||||
private exitCallback: (code: number) => void;
|
||||
private exitCallback: (code: number, restart: boolean) => void;
|
||||
private extensions: Extension[];
|
||||
private extensionArgs: ExtensionArgs;
|
||||
|
||||
constructor(restartCallback: () => void, exitCallback: (code: number) => void) {
|
||||
constructor(restartCallback: () => void, exitCallback: (code: number, restart: boolean) => void) {
|
||||
logger.init();
|
||||
this.eventBus = new EventBus( /* istanbul ignore next */ (error) => {
|
||||
logger.error(`Error: ${error.message}`);
|
||||
logger.debug(error.stack);
|
||||
});
|
||||
this.zigbee = new Zigbee(this.eventBus);
|
||||
this.mqtt = new MQTT(this.eventBus);
|
||||
this.state = new State(this.eventBus);
|
||||
this.state = new State(this.eventBus, this.zigbee);
|
||||
this.restartCallback = restartCallback;
|
||||
this.exitCallback = exitCallback;
|
||||
|
||||
@@ -143,8 +144,7 @@ class Controller {
|
||||
try {
|
||||
await this.mqtt.connect();
|
||||
} catch (error) {
|
||||
logger.error(`MQTT failed to connect: ${error.message}`);
|
||||
logger.error('Exiting...');
|
||||
logger.error(`MQTT failed to connect, exiting...`);
|
||||
await this.zigbee.stop();
|
||||
await this.exit(1);
|
||||
}
|
||||
@@ -154,15 +154,17 @@ class Controller {
|
||||
|
||||
// Send all cached states.
|
||||
if (settings.get().advanced.cache_state_send_on_startup && settings.get().advanced.cache_state) {
|
||||
for (const device of devices) {
|
||||
if (this.state.exists(device)) {
|
||||
this.publishEntityState(device, this.state.get(device));
|
||||
for (const entity of [...devices, ...this.zigbee.groups()]) {
|
||||
if (this.state.exists(entity)) {
|
||||
this.publishEntityState(entity, this.state.get(entity), 'publishCached');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.eventBus.onLastSeenChanged(this,
|
||||
(data) => utils.publishLastSeen(data, settings.get(), false, this.publishEntityState));
|
||||
|
||||
logger.info(`Zigbee2MQTT started!`);
|
||||
}
|
||||
|
||||
@bind async enableDisableExtension(enable: boolean, name: string): Promise<void> {
|
||||
@@ -186,7 +188,7 @@ class Controller {
|
||||
await this.callExtensions('start', [extension]);
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
async stop(restart = false): Promise<void> {
|
||||
// Call extensions
|
||||
await this.callExtensions('stop', this.extensions);
|
||||
this.eventBus.removeListeners(this);
|
||||
@@ -198,16 +200,16 @@ class Controller {
|
||||
try {
|
||||
await this.zigbee.stop();
|
||||
logger.info('Stopped Zigbee2MQTT');
|
||||
await this.exit(0);
|
||||
await this.exit(0, restart);
|
||||
} catch (error) {
|
||||
logger.error('Failed to stop Zigbee2MQTT');
|
||||
await this.exit(1);
|
||||
await this.exit(1, restart);
|
||||
}
|
||||
}
|
||||
|
||||
async exit(code: number): Promise<void> {
|
||||
async exit(code: number, restart = false): Promise<void> {
|
||||
await logger.end();
|
||||
this.exitCallback(code);
|
||||
this.exitCallback(code, restart);
|
||||
}
|
||||
|
||||
@bind async onZigbeeAdapterDisconnected(): Promise<void> {
|
||||
@@ -241,11 +243,14 @@ class Controller {
|
||||
message.device = {
|
||||
friendlyName: entity.name, model: entity.definition ? entity.definition.model : 'unknown',
|
||||
ieeeAddr: entity.ieeeAddr, networkAddress: entity.zh.networkAddress, type: entity.zh.type,
|
||||
manufacturerID: entity.zh.manufacturerID, manufacturerName: entity.zh.manufacturerName,
|
||||
manufacturerID: entity.zh.manufacturerID,
|
||||
powerSource: entity.zh.powerSource, applicationVersion: entity.zh.applicationVersion,
|
||||
stackVersion: entity.zh.stackVersion, zclVersion: entity.zh.zclVersion,
|
||||
hardwareVersion: entity.zh.hardwareVersion, dateCode: entity.zh.dateCode,
|
||||
softwareBuildID: entity.zh.softwareBuildID,
|
||||
// Manufacturer name can contain \u0000, remove this.
|
||||
// https://github.com/home-assistant/core/issues/85691
|
||||
manufacturerName: entity.zh.manufacturerName?.split('\u0000')[0],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -264,10 +269,8 @@ class Controller {
|
||||
extension.adjustMessageBeforePublish?.(entity, message);
|
||||
}
|
||||
|
||||
// filter mqtt message attributes
|
||||
if (entity.options.filtered_attributes) {
|
||||
entity.options.filtered_attributes.forEach((a) => delete message[a]);
|
||||
}
|
||||
// Filter mqtt message attributes
|
||||
utils.filterProperties(entity.options.filtered_attributes, message);
|
||||
|
||||
if (Object.entries(message).length) {
|
||||
const output = settings.get().advanced.output;
|
||||
|
||||
@@ -47,7 +47,7 @@ export default class Availability extends Extension {
|
||||
private resetTimer(device: Device): void {
|
||||
clearTimeout(this.timers[device.ieeeAddr]);
|
||||
|
||||
// If the timer triggers, the device is not avaiable anymore otherwise resetTimer already have been called
|
||||
// If the timer triggers, the device is not available anymore otherwise resetTimer already have been called
|
||||
if (this.isActiveDevice(device)) {
|
||||
// If device did not check in, ping it, if that fails it will be marked as offline
|
||||
this.timers[device.ieeeAddr] = setTimeout(
|
||||
@@ -82,7 +82,7 @@ export default class Availability extends Extension {
|
||||
const disableRecovery = !(i == 1 && available);
|
||||
await device.zh.ping(disableRecovery);
|
||||
pingedSuccessfully = true;
|
||||
logger.debug(`Succesfully pinged '${device.name}' (attempt ${i + 1}/${attempts})`);
|
||||
logger.debug(`Successfully pinged '${device.name}' (attempt ${i + 1}/${attempts})`);
|
||||
break;
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to ping '${device.name}' (attempt ${i + 1}/${attempts}, ${error.message})`);
|
||||
@@ -200,7 +200,7 @@ export default class Availability extends Extension {
|
||||
if (item.condition && this.state.get(device) && !item.condition(this.state.get(device))) continue;
|
||||
const converter = device.definition.toZigbee.find((c) => c.key.find((k) => item.keys.includes(k)));
|
||||
await converter?.convertGet?.(device.endpoint(), item.keys[0],
|
||||
{message: this.state.get(device) || {}, mapped: device.definition})
|
||||
{message: this.state.get(device), mapped: device.definition})
|
||||
.catch((e) => {
|
||||
logger.error(`Failed to read state of '${device.name}' after reconnect (${e.message})`);
|
||||
});
|
||||
|
||||
+47
-24
@@ -4,7 +4,7 @@ import utils from '../util/utils';
|
||||
import Extension from './extension';
|
||||
import stringify from 'json-stable-stringify-without-jsonify';
|
||||
import debounce from 'debounce';
|
||||
import * as zigbeeHersdman from 'zigbee-herdsman/dist';
|
||||
import * as zigbeeHerdsman from 'zigbee-herdsman/dist';
|
||||
import bind from 'bind-decorator';
|
||||
import Device from '../model/device';
|
||||
import Group from '../model/group';
|
||||
@@ -12,8 +12,9 @@ import Group from '../model/group';
|
||||
const legacyApi = settings.get().advanced.legacy_api;
|
||||
const legacyTopicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/(bind|unbind)/.+$`);
|
||||
const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/request/device/(bind|unbind)`);
|
||||
const clusterCandidates = ['genScenes', 'genOnOff', 'genLevelCtrl', 'lightingColorCtrl', 'closuresWindowCovering',
|
||||
'hvacThermostat', 'msTemperatureMeasurement'];
|
||||
const allClusterCandidates = ['genScenes', 'genOnOff', 'genLevelCtrl', 'lightingColorCtrl', 'closuresWindowCovering',
|
||||
'hvacThermostat', 'msIlluminanceMeasurement', 'msTemperatureMeasurement', 'msRelativeHumidity',
|
||||
'msSoilMoisture', 'msCO2'];
|
||||
|
||||
// See zigbee-herdsman-converters
|
||||
const defaultBindGroup = {type: 'group_number', ID: 901, name: 'default_bind_group'};
|
||||
@@ -67,7 +68,8 @@ const reportClusters: {[s: string]:
|
||||
type PollOnMessage = {
|
||||
cluster: {[s: string]: {type: string, data: KeyValue}[]}
|
||||
read: {cluster: string, attributes: string[], attributesForEndpoint?: (endpoint: zh.Endpoint) => Promise<string[]>}
|
||||
manufacturerIDs: number[]
|
||||
manufacturerIDs: number[],
|
||||
manufacturerNames: string [],
|
||||
}[];
|
||||
|
||||
const pollOnMessage: PollOnMessage = [
|
||||
@@ -95,11 +97,16 @@ const pollOnMessage: PollOnMessage = [
|
||||
read: {cluster: 'genLevelCtrl', attributes: ['currentLevel']},
|
||||
// When the bound devices/members of group have the following manufacturerIDs
|
||||
manufacturerIDs: [
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.Philips,
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.ATMEL,
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.GLEDOPTO_CO_LTD,
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.MUELLER_LICHT_INT,
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.TELINK,
|
||||
zigbeeHerdsman.Zcl.ManufacturerCode.Philips,
|
||||
zigbeeHerdsman.Zcl.ManufacturerCode.ATMEL,
|
||||
zigbeeHerdsman.Zcl.ManufacturerCode.GLEDOPTO_CO_LTD,
|
||||
zigbeeHerdsman.Zcl.ManufacturerCode.MUELLER_LICHT_INT,
|
||||
zigbeeHerdsman.Zcl.ManufacturerCode.TELINK,
|
||||
zigbeeHerdsman.Zcl.ManufacturerCode.BUSCH_JAEGER,
|
||||
],
|
||||
manufacturerNames: [
|
||||
'GLEDOPTO',
|
||||
'Trust International B.V.\u0000',
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -126,11 +133,16 @@ const pollOnMessage: PollOnMessage = [
|
||||
},
|
||||
read: {cluster: 'genOnOff', attributes: ['onOff']},
|
||||
manufacturerIDs: [
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.Philips,
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.ATMEL,
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.GLEDOPTO_CO_LTD,
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.MUELLER_LICHT_INT,
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.TELINK,
|
||||
zigbeeHerdsman.Zcl.ManufacturerCode.Philips,
|
||||
zigbeeHerdsman.Zcl.ManufacturerCode.ATMEL,
|
||||
zigbeeHerdsman.Zcl.ManufacturerCode.GLEDOPTO_CO_LTD,
|
||||
zigbeeHerdsman.Zcl.ManufacturerCode.MUELLER_LICHT_INT,
|
||||
zigbeeHerdsman.Zcl.ManufacturerCode.TELINK,
|
||||
zigbeeHerdsman.Zcl.ManufacturerCode.BUSCH_JAEGER,
|
||||
],
|
||||
manufacturerNames: [
|
||||
'GLEDOPTO',
|
||||
'Trust International B.V.\u0000',
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -153,11 +165,16 @@ const pollOnMessage: PollOnMessage = [
|
||||
},
|
||||
},
|
||||
manufacturerIDs: [
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.Philips,
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.ATMEL,
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.GLEDOPTO_CO_LTD,
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.MUELLER_LICHT_INT,
|
||||
zigbeeHersdman.Zcl.ManufacturerCode.TELINK,
|
||||
zigbeeHerdsman.Zcl.ManufacturerCode.Philips,
|
||||
zigbeeHerdsman.Zcl.ManufacturerCode.ATMEL,
|
||||
zigbeeHerdsman.Zcl.ManufacturerCode.GLEDOPTO_CO_LTD,
|
||||
zigbeeHerdsman.Zcl.ManufacturerCode.MUELLER_LICHT_INT,
|
||||
zigbeeHerdsman.Zcl.ManufacturerCode.TELINK,
|
||||
// Note: ManufacturerCode.BUSCH_JAEGER is left out intentionally here as their devices don't support colors
|
||||
],
|
||||
manufacturerNames: [
|
||||
'GLEDOPTO',
|
||||
'Trust International B.V.\u0000',
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -229,8 +246,8 @@ export default class Bind extends Extension {
|
||||
|
||||
// Find which clusters are supported by both the source and target.
|
||||
// Groups are assumed to support all clusters.
|
||||
const clusterCandidates = clusters ?? allClusterCandidates;
|
||||
for (const cluster of clusterCandidates) {
|
||||
if (clusters && !clusters.includes(cluster)) continue;
|
||||
let matchingClusters = false;
|
||||
|
||||
const anyClusterValid = utils.isZHGroup(bindTarget) || typeof bindTarget === 'number' ||
|
||||
@@ -372,7 +389,7 @@ export default class Bind extends Extension {
|
||||
}
|
||||
|
||||
await endpoint.configureReporting(bind.cluster.name, items);
|
||||
logger.info(`Succesfully setup reporting for '${entity}' cluster '${bind.cluster.name}'`);
|
||||
logger.info(`Successfully setup reporting for '${entity}' cluster '${bind.cluster.name}'`);
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to setup reporting for '${entity}' cluster '${bind.cluster.name}'`);
|
||||
}
|
||||
@@ -414,7 +431,7 @@ export default class Bind extends Extension {
|
||||
}
|
||||
|
||||
await endpoint.configureReporting(cluster, items);
|
||||
logger.info(`Succesfully disabled reporting for '${entity}' cluster '${cluster}'`);
|
||||
logger.info(`Successfully disabled reporting for '${entity}' cluster '${cluster}'`);
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to disable reporting for '${entity}' cluster '${cluster}'`);
|
||||
}
|
||||
@@ -455,7 +472,8 @@ export default class Bind extends Extension {
|
||||
|
||||
for (const endpoint of toPoll) {
|
||||
for (const poll of polls) {
|
||||
if (!poll.manufacturerIDs.includes(endpoint.getDevice().manufacturerID) ||
|
||||
if ((!poll.manufacturerIDs.includes(endpoint.getDevice().manufacturerID) &&
|
||||
!poll.manufacturerNames.includes(endpoint.getDevice().manufacturerName)) ||
|
||||
!endpoint.supportsInputCluster(poll.read.cluster)) {
|
||||
continue;
|
||||
}
|
||||
@@ -469,7 +487,12 @@ export default class Bind extends Extension {
|
||||
const key = `${endpoint.getDevice().ieeeAddr}_${endpoint.ID}_${pollOnMessage.indexOf(poll)}`;
|
||||
if (!this.pollDebouncers[key]) {
|
||||
this.pollDebouncers[key] = debounce(async () => {
|
||||
await endpoint.read(poll.read.cluster, readAttrs);
|
||||
try {
|
||||
await endpoint.read(poll.read.cluster, readAttrs);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to poll ${readAttrs} from ` +
|
||||
`${this.zigbee.resolveEntity(endpoint.getDevice()).name}`);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
|
||||
+41
-34
@@ -6,10 +6,12 @@ import Transport from 'winston-transport';
|
||||
import bind from 'bind-decorator';
|
||||
import stringify from 'json-stable-stringify-without-jsonify';
|
||||
import objectAssignDeep from 'object-assign-deep';
|
||||
import {detailedDiff} from 'deep-object-diff';
|
||||
import Extension from './extension';
|
||||
import Device from '../model/device';
|
||||
import Group from '../model/group';
|
||||
import data from '../util/data';
|
||||
import JSZip from 'jszip';
|
||||
import fs from 'fs';
|
||||
|
||||
const requestRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/request/(.*)`);
|
||||
|
||||
@@ -38,8 +40,10 @@ export default class Bridge extends Extension {
|
||||
'group/rename': this.groupRename,
|
||||
'permit_join': this.permitJoin,
|
||||
'restart': this.restart,
|
||||
'backup': this.backup,
|
||||
'touchlink/factory_reset': this.touchlinkFactoryReset,
|
||||
'touchlink/identify': this.touchlinkIdentify,
|
||||
'install_code/add': this.installCodeAdd,
|
||||
'touchlink/scan': this.touchlinkScan,
|
||||
'health_check': this.healthCheck,
|
||||
'options': this.bridgeOptions,
|
||||
@@ -143,35 +147,7 @@ export default class Bridge extends Extension {
|
||||
throw new Error(`Invalid payload`);
|
||||
}
|
||||
|
||||
const diff: KeyValue = detailedDiff(settings.get(), message.options);
|
||||
|
||||
// Remove any settings that are in the deleted.diff but not in the passed options
|
||||
const cleanupDeleted = (options: KeyValue, deleted: KeyValue): void => {
|
||||
for (const key of Object.keys(deleted)) {
|
||||
if (!(key in options)) {
|
||||
delete deleted[key];
|
||||
} else if (!Array.isArray(options[key])) {
|
||||
cleanupDeleted(options[key], deleted[key]);
|
||||
}
|
||||
}
|
||||
};
|
||||
cleanupDeleted(message.options, diff.deleted);
|
||||
|
||||
const newSettings = objectAssignDeep({}, diff.added, diff.updated, diff.deleted);
|
||||
|
||||
// deep-object-diff converts arrays to objects, set original array back here
|
||||
const convertBackArray = (before: KeyValue, after: KeyValue): void => {
|
||||
for (const [key, afterValue] of Object.entries(after)) {
|
||||
const beforeValue = before[key];
|
||||
if (Array.isArray(beforeValue)) {
|
||||
after[key] = beforeValue;
|
||||
} else if (afterValue && typeof beforeValue === 'object') {
|
||||
convertBackArray(beforeValue, afterValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
convertBackArray(message.options, newSettings);
|
||||
|
||||
const newSettings = utils.computeSettingsToChange(settings.get(), message.options);
|
||||
const restartRequired = settings.apply(newSettings);
|
||||
if (restartRequired) this.restartRequired = true;
|
||||
|
||||
@@ -188,7 +164,7 @@ export default class Bridge extends Extension {
|
||||
logger.setLevel(newSettings.advanced.log_level);
|
||||
}
|
||||
|
||||
logger.info('Succesfully changed options');
|
||||
logger.info('Successfully changed options');
|
||||
this.publishInfo();
|
||||
return utils.getResponse(message, {restart_required: this.restartRequired}, null);
|
||||
}
|
||||
@@ -233,6 +209,28 @@ export default class Bridge extends Extension {
|
||||
return utils.getResponse(message, {}, null);
|
||||
}
|
||||
|
||||
@bind async backup(message: string | KeyValue): Promise<MQTTResponse> {
|
||||
await this.zigbee.backup();
|
||||
const dataPath = data.getPath();
|
||||
const files = utils.getAllFiles(dataPath).map((f) => [f, f.substring(dataPath.length + 1)])
|
||||
.filter((f) => !f[1].startsWith('log'));
|
||||
const zip = new JSZip();
|
||||
files.forEach((f) => zip.file(f[1], fs.readFileSync(f[0])));
|
||||
const base64Zip = await zip.generateAsync({type: 'base64'});
|
||||
return utils.getResponse(message, {zip: base64Zip}, null);
|
||||
}
|
||||
|
||||
@bind async installCodeAdd(message: KeyValue | string): Promise<MQTTResponse> {
|
||||
if (typeof message === 'object' && !message.hasOwnProperty('value')) {
|
||||
throw new Error('Invalid payload');
|
||||
}
|
||||
|
||||
const value = typeof message === 'object' ? message.value : message;
|
||||
await this.zigbee.addInstallCode(value);
|
||||
logger.info('Successfully added new install code');
|
||||
return utils.getResponse(message, {value}, null);
|
||||
}
|
||||
|
||||
@bind async permitJoin(message: KeyValue | string): Promise<MQTTResponse> {
|
||||
if (typeof message === 'object' && !message.hasOwnProperty('value')) {
|
||||
throw new Error('Invalid payload');
|
||||
@@ -392,15 +390,23 @@ export default class Bridge extends Extension {
|
||||
|
||||
const ID = message.id;
|
||||
const entity = this.getEntity(entityType, ID);
|
||||
const currentOptions = entityType === 'device' ? settings.get().devices[entity.ID] :
|
||||
settings.get().groups[entity.ID];
|
||||
const options = utils.computeSettingsToChange(currentOptions, message.options);
|
||||
const oldOptions = objectAssignDeep({}, cleanup(entity.options));
|
||||
settings.changeEntityOptions(ID, message.options);
|
||||
const restartRequired = settings.changeEntityOptions(ID, options);
|
||||
if (restartRequired) this.restartRequired = true;
|
||||
const newOptions = cleanup(entity.options);
|
||||
await this.publishInfo();
|
||||
|
||||
logger.info(`Changed config for ${entityType} ${ID}`);
|
||||
|
||||
this.eventBus.emitEntityOptionsChanged({from: oldOptions, to: newOptions, entity});
|
||||
return utils.getResponse(message, {from: oldOptions, to: newOptions, id: ID}, null);
|
||||
return utils.getResponse(
|
||||
message,
|
||||
{from: oldOptions, to: newOptions, id: ID, restart_required: this.restartRequired},
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
@bind async deviceConfigureReporting(message: string | KeyValue): Promise<MQTTResponse> {
|
||||
@@ -464,7 +470,7 @@ export default class Bridge extends Extension {
|
||||
this.publishInfo();
|
||||
}
|
||||
|
||||
// Repulish entity state
|
||||
// Republish entity state
|
||||
this.publishEntityState(entity, {});
|
||||
|
||||
return utils.getResponse(
|
||||
@@ -650,6 +656,7 @@ export default class Bridge extends Extension {
|
||||
network_address: device.zh.networkAddress,
|
||||
supported: !!device.definition,
|
||||
friendly_name: device.name,
|
||||
disabled: !!device.options.disabled,
|
||||
description: device.options.description,
|
||||
definition: this.getDefinitionPayload(device),
|
||||
power_source: device.zh.powerSource,
|
||||
|
||||
@@ -64,9 +64,11 @@ export default class Configure extends Extension {
|
||||
}
|
||||
|
||||
override async start(): Promise<void> {
|
||||
for (const device of this.zigbee.devices(false)) {
|
||||
await this.configure(device, 'started');
|
||||
}
|
||||
setImmediate(async () => {
|
||||
for (const device of this.zigbee.devices(false)) {
|
||||
await this.configure(device, 'started');
|
||||
}
|
||||
});
|
||||
|
||||
this.eventBus.onDeviceJoined(this, (data) => {
|
||||
if (data.device.zh.meta.hasOwnProperty('configured')) {
|
||||
@@ -83,9 +85,9 @@ export default class Configure extends Extension {
|
||||
}
|
||||
|
||||
private async configure(device: Device, event: 'started' | 'zigbee_event' | 'reporting_disabled' | 'mqtt_message',
|
||||
force=false, thowError=false): Promise<void> {
|
||||
force=false, throwError=false): Promise<void> {
|
||||
if (!force) {
|
||||
if (!device.definition?.configure || !device.zh.interviewCompleted) {
|
||||
if (device.options.disabled || !device.definition?.configure || !device.zh.interviewCompleted) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -124,7 +126,7 @@ export default class Configure extends Extension {
|
||||
const msg = `Failed to configure '${device.name}', attempt ${attempt} (${error.stack})`;
|
||||
logger.error(msg);
|
||||
|
||||
if (thowError) {
|
||||
if (throwError) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,8 +28,8 @@ export default class ExternalExtension extends Extension {
|
||||
const basePath = this.getExtensionsBasePath();
|
||||
if (fs.existsSync(basePath)) {
|
||||
return fs.readdirSync(basePath).filter((f) => f.endsWith('.js')).map((fileName) => {
|
||||
const extensonFilePath = path.join(basePath, fileName);
|
||||
return {'name': fileName, 'code': fs.readFileSync(extensonFilePath, 'utf-8')};
|
||||
const extensionFilePath = path.join(basePath, fileName);
|
||||
return {'name': fileName, 'code': fs.readFileSync(extensionFilePath, 'utf-8')};
|
||||
});
|
||||
} else {
|
||||
return [];
|
||||
@@ -56,15 +56,15 @@ export default class ExternalExtension extends Extension {
|
||||
|
||||
@bind private async saveExtension(message: KeyValue): Promise<MQTTResponse> {
|
||||
const {name, code} = message;
|
||||
const ModuleConstructor = utils.loadModuleFromText(code) as typeof Extension;
|
||||
const ModuleConstructor = utils.loadModuleFromText(code, name) as typeof Extension;
|
||||
await this.loadExtension(ModuleConstructor);
|
||||
const basePath = this.getExtensionsBasePath();
|
||||
/* istanbul ignore else */
|
||||
if (!fs.existsSync(basePath)) {
|
||||
fs.mkdirSync(basePath);
|
||||
}
|
||||
const extensonFilePath = path.join(basePath, path.basename(name));
|
||||
fs.writeFileSync(extensonFilePath, code);
|
||||
const extensionFilePath = path.join(basePath, path.basename(name));
|
||||
fs.writeFileSync(extensionFilePath, code);
|
||||
this.publishExtensions();
|
||||
logger.info(`Extension ${name} loaded`);
|
||||
return utils.getResponse(message, {}, null);
|
||||
@@ -95,7 +95,7 @@ export default class ExternalExtension extends Extension {
|
||||
private loadUserDefinedExtensions(): void {
|
||||
const extensions = this.getListOfUserDefinedExtensions();
|
||||
extensions
|
||||
.map(({code}) => utils.loadModuleFromText(code))
|
||||
.map(({code, name}) => utils.loadModuleFromText(code, name))
|
||||
.map(this.loadExtension);
|
||||
}
|
||||
|
||||
|
||||
+42
-13
@@ -1,4 +1,5 @@
|
||||
import http from 'http';
|
||||
import https from 'https';
|
||||
import gzipStatic, {RequestHandler} from 'connect-gzip-static';
|
||||
import finalhandler from 'finalhandler';
|
||||
import logger from '../util/logger';
|
||||
@@ -6,6 +7,7 @@ import frontend from 'zigbee2mqtt-frontend';
|
||||
import WebSocket from 'ws';
|
||||
import net from 'net';
|
||||
import url from 'url';
|
||||
import fs from 'fs';
|
||||
import * as settings from '../util/settings';
|
||||
import utils from '../util/utils';
|
||||
import stringify from 'json-stable-stringify-without-jsonify';
|
||||
@@ -19,6 +21,8 @@ export default class Frontend extends Extension {
|
||||
private mqttBaseTopic = settings.get().mqtt.base_topic;
|
||||
private host = settings.get().frontend.host;
|
||||
private port = settings.get().frontend.port;
|
||||
private sslCert = settings.get().frontend.ssl_cert;
|
||||
private sslKey = settings.get().frontend.ssl_key;
|
||||
private authToken = settings.get().frontend.auth_token;
|
||||
private retainedMessages = new Map();
|
||||
private server: http.Server;
|
||||
@@ -33,8 +37,28 @@ export default class Frontend extends Extension {
|
||||
this.eventBus.onMQTTMessagePublished(this, this.onMQTTPublishMessage);
|
||||
}
|
||||
|
||||
private isHttpsConfigured():boolean {
|
||||
if (this.sslCert && this.sslKey) {
|
||||
if (!fs.existsSync(this.sslCert) || !fs.existsSync(this.sslKey)) {
|
||||
logger.error(`defined ssl_cert '${this.sslCert}' or ssl_key '${this.sslKey}' file path does not exists, server won't be secured.`); /* eslint-disable-line max-len */
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
override async start(): Promise<void> {
|
||||
this.server = http.createServer(this.onRequest);
|
||||
if (this.isHttpsConfigured()) {
|
||||
const serverOptions = {
|
||||
key: fs.readFileSync(this.sslKey),
|
||||
cert: fs.readFileSync(this.sslCert)};
|
||||
this.server = https.createServer(serverOptions, this.onRequest);
|
||||
} else {
|
||||
this.server = http.createServer(this.onRequest);
|
||||
}
|
||||
|
||||
this.server.on('upgrade', this.onUpgrade);
|
||||
|
||||
/* istanbul ignore next */
|
||||
@@ -50,18 +74,26 @@ export default class Frontend extends Extension {
|
||||
this.wss = new WebSocket.Server({noServer: true});
|
||||
this.wss.on('connection', this.onWebSocketConnection);
|
||||
|
||||
this.server.listen(this.port, this.host);
|
||||
logger.info(`Started frontend on port ${this.host}:${this.port}`);
|
||||
if (this.host.startsWith('/')) {
|
||||
this.server.listen(this.host);
|
||||
logger.info(`Started frontend on socket ${this.host}`);
|
||||
} else {
|
||||
this.server.listen(this.port, this.host);
|
||||
logger.info(`Started frontend on port ${this.host}:${this.port}`);
|
||||
}
|
||||
}
|
||||
|
||||
override async stop(): Promise<void> {
|
||||
super.stop();
|
||||
for (const client of this.wss.clients) {
|
||||
this.wss?.clients.forEach((client) => {
|
||||
client.send(stringify({topic: 'bridge/state', payload: 'offline'}));
|
||||
client.terminate();
|
||||
});
|
||||
this.wss?.close();
|
||||
/* istanbul ignore else */
|
||||
if (this.server) {
|
||||
return new Promise((cb: () => void) => this.server.close(cb));
|
||||
}
|
||||
this.wss.close();
|
||||
return new Promise((cb: () => void) => this.server.close(cb));
|
||||
}
|
||||
|
||||
@bind private onRequest(request: http.IncomingMessage, response: http.ServerResponse): void {
|
||||
@@ -76,8 +108,8 @@ export default class Frontend extends Extension {
|
||||
|
||||
@bind private onUpgrade(request: http.IncomingMessage, socket: net.Socket, head: Buffer): void {
|
||||
this.wss.handleUpgrade(request, socket, head, (ws) => {
|
||||
this.authenticate(request, (isAuthentificated) => {
|
||||
if (isAuthentificated) {
|
||||
this.authenticate(request, (isAuthenticated) => {
|
||||
if (isAuthenticated) {
|
||||
this.wss.emit('connection', ws, request);
|
||||
} else {
|
||||
ws.close(4401, 'Unauthorized');
|
||||
@@ -87,6 +119,7 @@ export default class Frontend extends Extension {
|
||||
}
|
||||
|
||||
@bind private onWebSocketConnection(ws: WebSocket): void {
|
||||
ws.on('error', (msg) => logger.error(`WebSocket error: ${msg.message}`));
|
||||
ws.on('message', (data: Buffer, isBinary: boolean) => {
|
||||
if (!isBinary && data) {
|
||||
const message = data.toString();
|
||||
@@ -100,11 +133,7 @@ export default class Frontend extends Extension {
|
||||
}
|
||||
|
||||
for (const device of this.zigbee.devices(false)) {
|
||||
let payload: KeyValue = {};
|
||||
if (this.state.exists(device)) {
|
||||
payload = {...payload, ...this.state.get(device)};
|
||||
}
|
||||
|
||||
const payload = this.state.get(device);
|
||||
const lastSeen = settings.get().advanced.last_seen;
|
||||
/* istanbul ignore if */
|
||||
if (lastSeen !== 'disable') {
|
||||
|
||||
+25
-13
@@ -101,7 +101,7 @@ export default class Groups extends Extension {
|
||||
|
||||
@bind async onStateChange(data: eventdata.StateChange): Promise<void> {
|
||||
const reason = 'groupOptimistic';
|
||||
if (data.reason === reason) {
|
||||
if (data.reason === reason || data.reason === 'publishCached') {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -129,11 +129,10 @@ export default class Groups extends Extension {
|
||||
if (entity instanceof Device) {
|
||||
for (const group of groups) {
|
||||
if (group.zh.hasMember(entity.endpoint(endpointName)) &&
|
||||
!equals(this.lastOptimisticState[group.ID], payload)) {
|
||||
if (!payload || payload.state !== 'OFF' || this.areAllMembersOff(group)) {
|
||||
this.lastOptimisticState[group.ID] = payload;
|
||||
await this.publishEntityState(group, payload, reason);
|
||||
}
|
||||
!equals(this.lastOptimisticState[group.ID], payload) &&
|
||||
this.shouldPublishPayloadForGroup(group, payload)) {
|
||||
this.lastOptimisticState[group.ID] = payload;
|
||||
await this.publishEntityState(group, payload, reason);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -143,6 +142,7 @@ export default class Groups extends Extension {
|
||||
const groupsToPublish: Set<Group> = new Set();
|
||||
for (const member of entity.zh.members) {
|
||||
const device = this.zigbee.resolveEntity(member.getDevice()) as Device;
|
||||
if (device.options.disabled) continue;
|
||||
const exposes = device.exposes();
|
||||
const memberPayload: KeyValue = {};
|
||||
Object.keys(payload).forEach((key) => {
|
||||
@@ -161,10 +161,9 @@ export default class Groups extends Extension {
|
||||
|
||||
await this.publishEntityState(device, memberPayload, reason);
|
||||
for (const zigbeeGroup of groups) {
|
||||
if (zigbeeGroup.zh.hasMember(member)) {
|
||||
if (!payload || payload.state !== 'OFF' || this.areAllMembersOff(zigbeeGroup)) {
|
||||
groupsToPublish.add(zigbeeGroup);
|
||||
}
|
||||
if (zigbeeGroup.zh.hasMember(member) &&
|
||||
this.shouldPublishPayloadForGroup(zigbeeGroup, payload)) {
|
||||
groupsToPublish.add(zigbeeGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -176,12 +175,19 @@ export default class Groups extends Extension {
|
||||
}
|
||||
}
|
||||
|
||||
private shouldPublishPayloadForGroup(group: Group, payload: KeyValue): boolean {
|
||||
if (group.options.off_state === 'last_member_state') return true;
|
||||
if (!payload || payload.state !== 'OFF') return true;
|
||||
if (this.areAllMembersOff(group)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private areAllMembersOff(group: Group): boolean {
|
||||
for (const member of group.zh.members) {
|
||||
const device = this.zigbee.resolveEntity(member.getDevice());
|
||||
if (this.state.exists(device)) {
|
||||
const state = this.state.get(device);
|
||||
if (state && state.state === 'ON') {
|
||||
if (state.state === 'ON') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -287,6 +293,7 @@ export default class Groups extends Extension {
|
||||
groupKey, deviceKey, skipDisableReporting, resolvedEntityEndpoint,
|
||||
} = parsed;
|
||||
const message = utils.parseJSON(data.message, data.message);
|
||||
let changedGroups: Group[] = [];
|
||||
|
||||
const responseData: KeyValue = {device: deviceKey};
|
||||
if (groupKey) {
|
||||
@@ -315,6 +322,7 @@ export default class Groups extends Extension {
|
||||
logger.info(`Adding '${resolvedEntityDevice.name}' to '${resolvedEntityGroup.name}'`);
|
||||
await resolvedEntityEndpoint.addToGroup(resolvedEntityGroup.zh);
|
||||
settings.addDeviceToGroup(resolvedEntityGroup.ID.toString(), keys);
|
||||
changedGroups.push(resolvedEntityGroup);
|
||||
|
||||
/* istanbul ignore else */
|
||||
if (settings.get().advanced.legacy_api) {
|
||||
@@ -328,6 +336,7 @@ export default class Groups extends Extension {
|
||||
logger.info(`Removing '${resolvedEntityDevice.name}' from '${resolvedEntityGroup.name}'`);
|
||||
await resolvedEntityEndpoint.removeFromGroup(resolvedEntityGroup.zh);
|
||||
settings.removeDeviceFromGroup(resolvedEntityGroup.ID.toString(), keys);
|
||||
changedGroups.push(resolvedEntityGroup);
|
||||
|
||||
/* istanbul ignore else */
|
||||
if (settings.get().advanced.legacy_api) {
|
||||
@@ -339,6 +348,7 @@ export default class Groups extends Extension {
|
||||
}
|
||||
} else { // remove_all
|
||||
logger.info(`Removing '${resolvedEntityDevice.name}' from all groups`);
|
||||
changedGroups = this.zigbee.groups().filter((g) => g.zh.members.includes(resolvedEntityEndpoint));
|
||||
await resolvedEntityEndpoint.removeFromAllGroups();
|
||||
for (const settingsGroup of settings.getGroups()) {
|
||||
settings.removeDeviceFromGroup(settingsGroup.ID.toString(), keys);
|
||||
@@ -367,8 +377,10 @@ export default class Groups extends Extension {
|
||||
if (error) {
|
||||
logger.error(error);
|
||||
} else {
|
||||
this.eventBus.emitGroupMembersChanged({
|
||||
group: resolvedEntityGroup, action: type, endpoint: resolvedEntityEndpoint, skipDisableReporting});
|
||||
for (const group of changedGroups) {
|
||||
this.eventBus.emitGroupMembersChanged({
|
||||
group, action: type, endpoint: resolvedEntityEndpoint, skipDisableReporting});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+249
-137
@@ -111,7 +111,7 @@ export default class HomeAssistant extends Extension {
|
||||
}
|
||||
|
||||
private exposeToConfig(exposes: zhc.DefinitionExpose[], entityType: 'device' | 'group',
|
||||
definition?: zhc.Definition, definitionExposes?: zhc.DefinitionExpose[]): DiscoveryEntry[] {
|
||||
allExposes: zhc.DefinitionExpose[], definition?: zhc.Definition): DiscoveryEntry[] {
|
||||
// For groups an array of exposes (of the same type) is passed, this is to determine e.g. what features
|
||||
// to use for a bulb (e.g. color_xy/color_temp)
|
||||
assert(entityType === 'group' || exposes.length === 1, 'Multiple exposes for device not allowed');
|
||||
@@ -171,10 +171,11 @@ export default class HomeAssistant extends Extension {
|
||||
discoveryEntry.discovery_payload.min_mireds = min;
|
||||
}
|
||||
|
||||
const effect = definitionExposes?.find((e) => e.type === 'enum' && e.name === 'effect');
|
||||
if (effect) {
|
||||
const effects = utils.arrayUnique(utils.flatten(
|
||||
allExposes.filter((e) => e.type === 'enum' && e.name === 'effect').map((e) => e.values)));
|
||||
if (effects.length) {
|
||||
discoveryEntry.discovery_payload.effect = true;
|
||||
discoveryEntry.discovery_payload.effect_list = effect.values;
|
||||
discoveryEntry.discovery_payload.effect_list = effects;
|
||||
}
|
||||
|
||||
discoveryEntries.push(discoveryEntry);
|
||||
@@ -236,7 +237,7 @@ export default class HomeAssistant extends Extension {
|
||||
const mode = firstExpose.features.find((f) => f.name === 'system_mode');
|
||||
if (mode) {
|
||||
if (mode.values.includes('sleep')) {
|
||||
// 'sleep' is not supported by homeassistent, but is valid according to ZCL
|
||||
// 'sleep' is not supported by Home Assistant, but is valid according to ZCL
|
||||
// TRV that support sleep (e.g. Viessmann) will have it removed from here,
|
||||
// this allows other expose consumers to still use it, e.g. the frontend.
|
||||
mode.values.splice(mode.values.indexOf('sleep'), 1);
|
||||
@@ -282,21 +283,22 @@ export default class HomeAssistant extends Extension {
|
||||
discoveryEntry.discovery_payload.fan_mode_state_topic = true;
|
||||
}
|
||||
|
||||
const preset = firstExpose.features.find((f) => f.name === 'preset');
|
||||
if (preset) {
|
||||
discoveryEntry.discovery_payload.hold_modes = preset.values;
|
||||
discoveryEntry.discovery_payload.hold_command_topic = true;
|
||||
discoveryEntry.discovery_payload.hold_state_template =
|
||||
`{{ value_json.${preset.property} }}`;
|
||||
discoveryEntry.discovery_payload.hold_state_topic = true;
|
||||
const swingMode = firstExpose.features.find((f) => f.name === 'swing_mode');
|
||||
if (swingMode) {
|
||||
discoveryEntry.discovery_payload.swing_modes = swingMode.values;
|
||||
discoveryEntry.discovery_payload.swing_mode_command_topic = true;
|
||||
discoveryEntry.discovery_payload.swing_mode_state_template =
|
||||
`{{ value_json.${swingMode.property} }}`;
|
||||
discoveryEntry.discovery_payload.swing_mode_state_topic = true;
|
||||
}
|
||||
|
||||
const awayMode = firstExpose.features.find((f) => f.name === 'away_mode');
|
||||
if (awayMode) {
|
||||
discoveryEntry.discovery_payload.away_mode_command_topic = true;
|
||||
discoveryEntry.discovery_payload.away_mode_state_topic = true;
|
||||
discoveryEntry.discovery_payload.away_mode_state_template =
|
||||
`{{ value_json.${awayMode.property} }}`;
|
||||
const preset = firstExpose.features.find((f) => f.name === 'preset');
|
||||
if (preset) {
|
||||
discoveryEntry.discovery_payload.preset_modes = preset.values;
|
||||
discoveryEntry.discovery_payload.preset_mode_command_topic = 'preset';
|
||||
discoveryEntry.discovery_payload.preset_mode_value_template =
|
||||
`{{ value_json.${preset.property} }}`;
|
||||
discoveryEntry.discovery_payload.preset_mode_state_topic = true;
|
||||
}
|
||||
|
||||
const tempCalibration = firstExpose.features.find((f) => f.name === 'local_temperature_calibration');
|
||||
@@ -310,6 +312,7 @@ export default class HomeAssistant extends Extension {
|
||||
command_topic: true,
|
||||
command_topic_prefix: endpoint,
|
||||
command_topic_postfix: tempCalibration.property,
|
||||
device_class: 'temperature',
|
||||
entity_category: 'config',
|
||||
icon: 'mdi:math-compass',
|
||||
...(tempCalibration.unit && {unit_of_measurement: tempCalibration.unit}),
|
||||
@@ -318,6 +321,9 @@ export default class HomeAssistant extends Extension {
|
||||
|
||||
if (tempCalibration.value_min != null) discoveryEntry.discovery_payload.min = tempCalibration.value_min;
|
||||
if (tempCalibration.value_max != null) discoveryEntry.discovery_payload.max = tempCalibration.value_max;
|
||||
if (tempCalibration.value_step != null) {
|
||||
discoveryEntry.discovery_payload.step = tempCalibration.value_step;
|
||||
}
|
||||
discoveryEntries.push(discoveryEntry);
|
||||
}
|
||||
|
||||
@@ -378,87 +384,84 @@ export default class HomeAssistant extends Extension {
|
||||
|
||||
discoveryEntries.push(discoveryEntry);
|
||||
} else if (firstExpose.type === 'cover') {
|
||||
const position = exposes.find((expose) => expose.features.find((e) => e.name === 'position'));
|
||||
const tilt = exposes.find((expose) => expose.features.find((e) => e.name === 'tilt'));
|
||||
const motorState = definitionExposes?.find((e) => e.type === 'enum' && e.name === 'motor_state' &&
|
||||
e.access === ACCESS_STATE);
|
||||
const state = exposes.find((expose) => expose.features.find((e) => e.name === 'state'))
|
||||
?.features.find((f) => f.name === 'state');
|
||||
const position = exposes.find((expose) => expose.features.find((e) => e.name === 'position'))
|
||||
?.features.find((f) => f.name === 'position');
|
||||
const tilt = exposes.find((expose) => expose.features.find((e) => e.name === 'tilt'))
|
||||
?.features.find((f) => f.name === 'tilt');
|
||||
const motorState = allExposes?.find((e) => e.type === 'enum' &&
|
||||
['motor_state', 'moving'].includes(e.name) && e.access === ACCESS_STATE);
|
||||
const running = allExposes?.find((e) => e.type === 'binary' && e.name === 'running');
|
||||
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: 'cover',
|
||||
mockProperties: [],
|
||||
mockProperties: [{property: state.property, value: null}],
|
||||
object_id: endpoint ? `cover_${endpoint}` : 'cover',
|
||||
discovery_payload: {
|
||||
command_topic_prefix: endpoint,
|
||||
command_topic: true,
|
||||
state_topic: true,
|
||||
state_topic_postfix: endpoint,
|
||||
},
|
||||
};
|
||||
|
||||
// For covers only supporting tilt don't discover the command/state_topic, otherwise
|
||||
// HA does not correctly reflect the state
|
||||
// - https://github.com/home-assistant/core/issues/51793
|
||||
// - https://github.com/Koenkk/zigbee-herdsman-converters/pull/2663
|
||||
if (!tilt || (tilt && position)) {
|
||||
discoveryEntry.discovery_payload.command_topic = true;
|
||||
discoveryEntry.discovery_payload.state_topic = !position;
|
||||
discoveryEntry.discovery_payload.command_topic_prefix = endpoint;
|
||||
// If curtains have `running` property, use this in discovery.
|
||||
// The movement direction is calculated (assumed) in this case.
|
||||
if (running) {
|
||||
discoveryEntry.discovery_payload.value_template = `{% if "${running.property}" in value_json ` +
|
||||
`and value_json.${running.property} %} {% if value_json.${position.property} > 0 %} closing ` +
|
||||
`{% else %} opening {% endif %} {% else %} stopped {% endif %}`;
|
||||
}
|
||||
|
||||
// For curtains that have `motor_state` lookup a possible state names and make this
|
||||
// available for discovery. If the curtains only support the `running` value,
|
||||
// then we use it anyway. The movement direction is calculated (assumed) in this case.
|
||||
if (motorState) {
|
||||
const openingLookup = ['opening', 'open', 'forward', 'up', 'rising'];
|
||||
const closingLookup = ['closing', 'close', 'backward', 'back', 'reverse', 'down', 'declining'];
|
||||
const stoppedLookup = ['stopped', 'stop', 'pause', 'paused'];
|
||||
// If curtains have `motor_state` or `moving` property, lookup for possible
|
||||
// state names to detect movement direction and use this in discovery.
|
||||
if (motorState) {
|
||||
const openingLookup = ['opening', 'open', 'forward', 'up', 'rising'];
|
||||
const closingLookup = ['closing', 'close', 'backward', 'back', 'reverse', 'down', 'declining'];
|
||||
const stoppedLookup = ['stopped', 'stop', 'pause', 'paused'];
|
||||
|
||||
const openingState = motorState.values.find((s) => openingLookup.includes(s.toLowerCase()));
|
||||
const closingState = motorState.values.find((s) => closingLookup.includes(s.toLowerCase()));
|
||||
const stoppedState = motorState.values.find((s) => stoppedLookup.includes(s.toLowerCase()));
|
||||
const openingState = motorState.values.find((s) => openingLookup.includes(s.toLowerCase()));
|
||||
const closingState = motorState.values.find((s) => closingLookup.includes(s.toLowerCase()));
|
||||
const stoppedState = motorState.values.find((s) => stoppedLookup.includes(s.toLowerCase()));
|
||||
|
||||
if (openingState) {
|
||||
discoveryEntry.discovery_payload.state_opening = openingState;
|
||||
}
|
||||
if (closingState) {
|
||||
discoveryEntry.discovery_payload.state_closing = closingState;
|
||||
}
|
||||
if (stoppedState) {
|
||||
discoveryEntry.discovery_payload.state_stopped = stoppedState;
|
||||
}
|
||||
|
||||
if (openingState || closingState || stoppedState) {
|
||||
discoveryEntry.discovery_payload.state_topic = true;
|
||||
discoveryEntry.discovery_payload.value_template = `{{ value_json.motor_state }}`;
|
||||
}
|
||||
} else {
|
||||
const running = definitionExposes?.find((e) => e.type === 'binary' && e.name === 'running');
|
||||
|
||||
if (running) {
|
||||
discoveryEntry.discovery_payload.state_topic = true;
|
||||
discoveryEntry.discovery_payload.value_template = `{% if not value_json.running %} ` +
|
||||
`stopped {% else %} {% if value_json.position > 0 %} closing {% else %} ` +
|
||||
`opening {% endif %} {% endif %}`;
|
||||
}
|
||||
if (openingState && closingState && stoppedState) {
|
||||
discoveryEntry.discovery_payload.state_opening = openingState;
|
||||
discoveryEntry.discovery_payload.state_closing = closingState;
|
||||
discoveryEntry.discovery_payload.state_stopped = stoppedState;
|
||||
discoveryEntry.discovery_payload.value_template = `{% if "${motorState.property}" in value_json ` +
|
||||
`and value_json.${motorState.property} %} {{ value_json.${motorState.property} }} {% else %} ` +
|
||||
`${stoppedState} {% endif %}`;
|
||||
}
|
||||
}
|
||||
|
||||
// If curtains do not have `running`, `motor_state` or `moving` properties.
|
||||
if (!discoveryEntry.discovery_payload.value_template) {
|
||||
discoveryEntry.discovery_payload.value_template =
|
||||
`{{ value_json.${featurePropertyWithoutEndpoint(state)} }}`,
|
||||
discoveryEntry.discovery_payload.state_open = 'OPEN';
|
||||
discoveryEntry.discovery_payload.state_closed = 'CLOSE';
|
||||
discoveryEntry.discovery_payload.state_stopped = 'STOP';
|
||||
}
|
||||
|
||||
if (!position && !tilt) {
|
||||
discoveryEntry.discovery_payload.optimistic = true;
|
||||
}
|
||||
|
||||
if (position) {
|
||||
const p = position.features.find((f) => f.name === 'position');
|
||||
discoveryEntry.discovery_payload = {...discoveryEntry.discovery_payload,
|
||||
position_template: `{{ value_json.${getProperty(p)} }}`,
|
||||
set_position_template: `{ "${getProperty(p)}": {{ position }} }`,
|
||||
position_template: `{{ value_json.${featurePropertyWithoutEndpoint(position)} }}`,
|
||||
set_position_template: `{ "${getProperty(position)}": {{ position }} }`,
|
||||
set_position_topic: true,
|
||||
position_topic: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (tilt) {
|
||||
const t = tilt.features.find((f) => f.name === 'tilt');
|
||||
discoveryEntry.discovery_payload = {...discoveryEntry.discovery_payload,
|
||||
tilt_command_topic: true,
|
||||
tilt_status_topic: true,
|
||||
tilt_status_template: `{{ value_json.${getProperty(t)} }}`,
|
||||
tilt_status_template: `{{ value_json.${featurePropertyWithoutEndpoint(tilt)} }}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -520,7 +523,7 @@ export default class HomeAssistant extends Extension {
|
||||
discoveryEntry.discovery_payload.speed_range_max = speeds.length - 1;
|
||||
assert(presets.length !== 0);
|
||||
discoveryEntry.discovery_payload.preset_mode_state_topic = true;
|
||||
discoveryEntry.discovery_payload.preset_mode_command_topic = true;
|
||||
discoveryEntry.discovery_payload.preset_mode_command_topic = 'fan_mode';
|
||||
discoveryEntry.discovery_payload.preset_mode_value_template =
|
||||
`{{ value_json.${speed.property} if value_json.${speed.property} in [${presetList}]` +
|
||||
` else 'None' | default('None') }}`;
|
||||
@@ -532,20 +535,25 @@ export default class HomeAssistant extends Extension {
|
||||
const lookup: {[s: string]: KeyValue}= {
|
||||
battery_low: {entity_category: 'diagnostic', device_class: 'battery'},
|
||||
button_lock: {entity_category: 'config', icon: 'mdi:lock'},
|
||||
carbon_monoxide: {device_class: 'safety'},
|
||||
calibration: {entity_category: 'config', icon: 'mdi:progress-wrench'},
|
||||
carbon_monoxide: {device_class: 'carbon_monoxide'},
|
||||
card: {entity_category: 'config', icon: 'mdi:clipboard-check'},
|
||||
child_lock: {entity_category: 'config', icon: 'mdi:account-lock'},
|
||||
color_sync: {entity_category: 'config', icon: 'mdi:sync-circle'},
|
||||
consumer_connected: {entity_category: 'diagnostic', device_class: 'connectivity'},
|
||||
contact: {device_class: 'door'},
|
||||
garage_door_contact: {device_class: 'garage'},
|
||||
garage_door_contact: {device_class: 'garage_door', payload_on: false, payload_off: true},
|
||||
eco_mode: {entity_category: 'config', icon: 'mdi:leaf'},
|
||||
expose_pin: {entity_category: 'config', icon: 'mdi:pin'},
|
||||
flip_indicator_light: {entity_category: 'config', icon: 'mdi:arrow-left-right'},
|
||||
gas: {device_class: 'gas'},
|
||||
indicator_mode: {entity_category: 'config', icon: 'mdi:led-on'},
|
||||
invert_cover: {entity_category: 'config', icon: 'mdi:arrow-left-right'},
|
||||
led_disabled_night: {entity_category: 'config', icon: 'mdi:led-off'},
|
||||
led_indication: {entity_category: 'config', icon: 'mdi:led-on'},
|
||||
led_enable: {entity_category: 'config', icon: 'mdi:led-on'},
|
||||
legacy: {entity_category: 'config', icon: 'mdi:cog'},
|
||||
motor_reversal: {entity_category: 'config', icon: 'mdi:arrow-left-right'},
|
||||
moving: {device_class: 'moving'},
|
||||
no_position_support: {entity_category: 'config', icon: 'mdi:minus-circle-outline'},
|
||||
occupancy: {device_class: 'motion'},
|
||||
@@ -554,9 +562,12 @@ export default class HomeAssistant extends Extension {
|
||||
smoke: {device_class: 'smoke'},
|
||||
sos: {device_class: 'safety'},
|
||||
tamper: {device_class: 'tamper'},
|
||||
temperature_scale: {entity_category: 'config', icon: 'mdi:temperature-celsius'},
|
||||
test: {entity_category: 'diagnostic', icon: 'mdi:test-tube'},
|
||||
valve_state: {device_class: 'opening'},
|
||||
vibration: {device_class: 'vibration'},
|
||||
water_leak: {device_class: 'moisture'},
|
||||
window: {device_class: 'window'},
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -614,9 +625,13 @@ export default class HomeAssistant extends Extension {
|
||||
away_preset_days: {entity_category: 'config', icon: 'mdi:timer'},
|
||||
away_preset_temperature: {entity_category: 'config', icon: 'mdi:thermometer'},
|
||||
battery: {device_class: 'battery', entity_category: 'diagnostic', state_class: 'measurement'},
|
||||
battery2: {device_class: 'battery', entity_category: 'diagnostic', state_class: 'measurement'},
|
||||
battery_voltage: {device_class: 'voltage', entity_category: 'diagnostic', state_class: 'measurement'},
|
||||
boost_heating_countdown: {device_class: 'duration'},
|
||||
boost_heating_countdown_time_set: {entity_category: 'config', icon: 'mdi:timer'},
|
||||
boost_time: {entity_category: 'config', icon: 'mdi:timer'},
|
||||
calibration: {entity_category: 'config'},
|
||||
calibration: {entity_category: 'config', icon: 'mdi:wrench-clock'},
|
||||
calibration_time: {entity_category: 'config', icon: 'mdi:wrench-clock'},
|
||||
co2: {device_class: 'carbon_dioxide', state_class: 'measurement'},
|
||||
comfort_temperature: {entity_category: 'config', icon: 'mdi:thermometer'},
|
||||
cpu_temperature: {
|
||||
@@ -645,6 +660,7 @@ export default class HomeAssistant extends Extension {
|
||||
device_temperature: {
|
||||
device_class: 'temperature', entity_category: 'diagnostic', state_class: 'measurement',
|
||||
},
|
||||
duration: {entity_category: 'config', icon: 'mdi:timer'},
|
||||
eco2: {device_class: 'carbon_dioxide', state_class: 'measurement'},
|
||||
eco_temperature: {entity_category: 'config', icon: 'mdi:thermometer'},
|
||||
energy: {device_class: 'energy', state_class: 'total_increasing'},
|
||||
@@ -652,6 +668,10 @@ export default class HomeAssistant extends Extension {
|
||||
gas_density: {icon: 'mdi:google-circles-communities', state_class: 'measurement'},
|
||||
hcho: {icon: 'mdi:air-filter', state_class: 'measurement'},
|
||||
humidity: {device_class: 'humidity', state_class: 'measurement'},
|
||||
humidity_calibration: {entity_category: 'config', icon: 'mdi:wrench-clock'},
|
||||
humidity_max: {entity_category: 'config', icon: 'mdi:water-percent'},
|
||||
humidity_min: {entity_category: 'config', icon: 'mdi:water-percent'},
|
||||
illuminance_calibration: {entity_category: 'config', icon: 'mdi:wrench-clock'},
|
||||
illuminance_lux: {device_class: 'illuminance', state_class: 'measurement'},
|
||||
illuminance: {device_class: 'illuminance', enabled_by_default: false, state_class: 'measurement'},
|
||||
linkquality: {
|
||||
@@ -661,9 +681,10 @@ export default class HomeAssistant extends Extension {
|
||||
state_class: 'measurement',
|
||||
},
|
||||
local_temperature: {device_class: 'temperature', state_class: 'measurement'},
|
||||
max_temperature: {entity_category: 'config', icon: 'mdi:thermometer'},
|
||||
max_temperature_limit: {entity_category: 'config', icon: 'mdi:thermometer'},
|
||||
min_temperature: {entity_category: 'config', icon: 'mdi:thermometer'},
|
||||
max_temperature: {entity_category: 'config', icon: 'mdi:thermometer-high'},
|
||||
max_temperature_limit: {entity_category: 'config', icon: 'mdi:thermometer-high'},
|
||||
min_temperature_limit: {entity_category: 'config', icon: 'mdi:thermometer-low'},
|
||||
min_temperature: {entity_category: 'config', icon: 'mdi:thermometer-low'},
|
||||
measurement_poll_interval: {entity_category: 'config', icon: 'mdi:clock-out'},
|
||||
occupancy_timeout: {entity_category: 'config', icon: 'mdi:timer'},
|
||||
pm10: {device_class: 'pm10', state_class: 'measurement'},
|
||||
@@ -674,8 +695,9 @@ export default class HomeAssistant extends Extension {
|
||||
power_factor: {device_class: 'power_factor', enabled_by_default: false,
|
||||
entity_category: 'diagnostic', state_class: 'measurement'},
|
||||
precision: {entity_category: 'config', icon: 'mdi:decimal-comma-increase'},
|
||||
pressure: {device_class: 'pressure', state_class: 'measurement'},
|
||||
pressure: {device_class: 'atmospheric_pressure', state_class: 'measurement'},
|
||||
presence_timeout: {entity_category: 'config', icon: 'mdi:timer'},
|
||||
reporting_time: {entity_category: 'config', icon: 'mdi:clock-time-one-outline'},
|
||||
requested_brightness_level: {
|
||||
enabled_by_default: false, entity_category: 'diagnostic', icon: 'mdi:brightness-5',
|
||||
},
|
||||
@@ -685,8 +707,12 @@ export default class HomeAssistant extends Extension {
|
||||
smoke_density: {icon: 'mdi:google-circles-communities', state_class: 'measurement'},
|
||||
soil_moisture: {icon: 'mdi:water-percent', state_class: 'measurement'},
|
||||
temperature: {device_class: 'temperature', state_class: 'measurement'},
|
||||
temperature_calibration: {entity_category: 'config', icon: 'mdi:wrench-clock'},
|
||||
temperature_max: {entity_category: 'config', icon: 'mdi:thermometer-plus'},
|
||||
temperature_min: {entity_category: 'config', icon: 'mdi:thermometer-minus'},
|
||||
transition: {entity_category: 'config', icon: 'mdi:transition'},
|
||||
voc: {device_class: 'volatile_organic_compounds', state_class: 'measurement'},
|
||||
voc_index: {state_class: 'measurement'},
|
||||
vibration_timeout: {entity_category: 'config', icon: 'mdi:timer'},
|
||||
voltage: {
|
||||
device_class: 'voltage',
|
||||
@@ -706,6 +732,10 @@ export default class HomeAssistant extends Extension {
|
||||
entity_category: 'diagnostic',
|
||||
state_class: 'measurement',
|
||||
},
|
||||
water_consumed: {
|
||||
device_class: 'water',
|
||||
state_class: 'total_increasing',
|
||||
},
|
||||
x_axis: {icon: 'mdi:axis-x-arrow'},
|
||||
y_axis: {icon: 'mdi:axis-y-arrow'},
|
||||
z_axis: {icon: 'mdi:axis-z-arrow'},
|
||||
@@ -732,6 +762,20 @@ export default class HomeAssistant extends Extension {
|
||||
...extraAttrs,
|
||||
},
|
||||
};
|
||||
|
||||
// When a device_class is set, unit_of_measurement must be set, otherwise warnings are generated.
|
||||
// https://github.com/Koenkk/zigbee2mqtt/issues/15958#issuecomment-1377483202
|
||||
if (discoveryEntry.discovery_payload.device_class &&
|
||||
!discoveryEntry.discovery_payload.unit_of_measurement) {
|
||||
delete discoveryEntry.discovery_payload.device_class;
|
||||
}
|
||||
|
||||
// Home Assistant only supports µg/m³, not other units like ppb.
|
||||
// https://github.com/Koenkk/zigbee2mqtt/issues/16057
|
||||
if (firstExpose.name === 'voc' && discoveryEntry.discovery_payload.unit_of_measurement !== 'µg/m³') {
|
||||
delete discoveryEntry.discovery_payload.device_class;
|
||||
}
|
||||
|
||||
discoveryEntries.push(discoveryEntry);
|
||||
|
||||
/**
|
||||
@@ -750,10 +794,17 @@ export default class HomeAssistant extends Extension {
|
||||
command_topic_prefix: endpoint,
|
||||
command_topic_postfix: firstExpose.property,
|
||||
...(firstExpose.unit && {unit_of_measurement: firstExpose.unit}),
|
||||
...(firstExpose.value_step && {step: firstExpose.value_step}),
|
||||
...lookup[firstExpose.name],
|
||||
},
|
||||
};
|
||||
|
||||
if (lookup[firstExpose.name]?.device_class === 'temperature') {
|
||||
discoveryEntry.discovery_payload.device_class == lookup[firstExpose.name]?.device_class;
|
||||
} else {
|
||||
delete discoveryEntry.discovery_payload.device_class;
|
||||
}
|
||||
|
||||
if (firstExpose.value_min != null) discoveryEntry.discovery_payload.min = firstExpose.value_min;
|
||||
if (firstExpose.value_max != null) discoveryEntry.discovery_payload.max = firstExpose.value_max;
|
||||
|
||||
@@ -767,11 +818,15 @@ export default class HomeAssistant extends Extension {
|
||||
backlight_auto_dim: {entity_category: 'config', icon: 'mdi:brightness-auto'},
|
||||
backlight_mode: {entity_category: 'config', icon: 'mdi:lightbulb'},
|
||||
color_power_on_behavior: {entity_category: 'config', icon: 'mdi:palette'},
|
||||
control_mode: {entity_category: 'config', icon: 'mdi:tune'},
|
||||
device_mode: {entity_category: 'config', icon: 'mdi:tune'},
|
||||
effect: {enabled_by_default: false, icon: 'mdi:palette'},
|
||||
force: {enabled_by_default: false, icon: 'mdi:valve'},
|
||||
keep_time: {entity_category: 'config', icon: 'mdi:av-timer'},
|
||||
keypad_lockout: {entity_category: 'config', icon: 'mdi:lock'},
|
||||
load_detection_mode: {entity_category: 'config', icon: 'mdi:tune'},
|
||||
load_dimmable: {entity_category: 'diagnostic', icon: 'mdi:chart-bell-curve'},
|
||||
load_type: {entity_category: 'diagnostic', icon: 'mdi:led-on'},
|
||||
melody: {entity_category: 'config', icon: 'mdi:music-note'},
|
||||
mode_phase_control: {entity_category: 'config', icon: 'mdi:tune'},
|
||||
mode: {entity_category: 'config', icon: 'mdi:tune'},
|
||||
@@ -779,67 +834,93 @@ export default class HomeAssistant extends Extension {
|
||||
operation_mode: {entity_category: 'config', icon: 'mdi:tune'},
|
||||
power_on_behavior: {entity_category: 'config', icon: 'mdi:power-settings'},
|
||||
power_outage_memory: {entity_category: 'config', icon: 'mdi:power-settings'},
|
||||
power_supply_mode: {entity_category: 'diagnostic', icon: 'mdi:power-settings'},
|
||||
power_type: {entity_category: 'diagnostic', icon: 'mdi:lightning-bolt-circle'},
|
||||
sensitivity: {entity_category: 'config', icon: 'mdi:tune'},
|
||||
sensors_type: {entity_category: 'config', icon: 'mdi:tune'},
|
||||
sound_volume: {entity_category: 'config', icon: 'mdi:volume-high'},
|
||||
status: {icon: 'mdi:state-machine'},
|
||||
switch_type: {entity_category: 'config', icon: 'mdi:tune'},
|
||||
temperature_sensor_select: {entity_category: 'config', icon: 'mdi:home-thermometer'},
|
||||
thermostat_unit: {entity_category: 'config', icon: 'mdi:thermometer'},
|
||||
volume: {entity_category: 'config', icon: 'mdi: volume-high'},
|
||||
week: {entity_category: 'config', icon: 'mdi:calendar-clock'},
|
||||
};
|
||||
|
||||
const valueTemplate = firstExpose.access & ACCESS_STATE ?
|
||||
`{{ value_json.${firstExpose.property} }}` : undefined;
|
||||
|
||||
if (firstExpose.access & ACCESS_STATE) {
|
||||
discoveryEntries.push({
|
||||
type: 'sensor',
|
||||
object_id: firstExpose.property,
|
||||
mockProperties: [{property: firstExpose.property, value: null}],
|
||||
discovery_payload: {
|
||||
value_template: `{{ value_json.${firstExpose.property} }}`,
|
||||
value_template: valueTemplate,
|
||||
enabled_by_default: !(firstExpose.access & ACCESS_SET),
|
||||
...lookup[firstExpose.name],
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* If enum attribute has SET access then expose as SELECT entity too.
|
||||
* Note: currently both sensor and select are discovered, this is to avoid
|
||||
* breaking changes for sensors already existing in HA (legacy).
|
||||
*/
|
||||
if ((firstExpose.access & ACCESS_SET)) {
|
||||
discoveryEntries.push({
|
||||
type: 'select',
|
||||
object_id: firstExpose.property,
|
||||
mockProperties: [{property: firstExpose.property, value: null}],
|
||||
discovery_payload: {
|
||||
value_template: `{{ value_json.${firstExpose.property} }}`,
|
||||
state_topic: true,
|
||||
command_topic_prefix: endpoint,
|
||||
command_topic: true,
|
||||
command_topic_postfix: firstExpose.property,
|
||||
options: firstExpose.values.map((v) => v.toString()),
|
||||
...lookup[firstExpose.name],
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (firstExpose.type === 'text' || firstExpose.type === 'composite') {
|
||||
if (firstExpose.access & ACCESS_STATE) {
|
||||
const lookup: {[s: string]: KeyValue} = {
|
||||
action: {icon: 'mdi:gesture-double-tap'},
|
||||
};
|
||||
|
||||
/**
|
||||
* If enum attribute has SET access then expose as SELECT entity too.
|
||||
* Note: currently both sensor and select are discovered, this is to avoid
|
||||
* breaking changes for sensors already existing in HA (legacy).
|
||||
*/
|
||||
if ((firstExpose.access & ACCESS_SET)) {
|
||||
discoveryEntries.push({
|
||||
type: 'select',
|
||||
object_id: firstExpose.property,
|
||||
mockProperties: [], // Already mocked above in case access STATE is supported
|
||||
discovery_payload: {
|
||||
value_template: valueTemplate,
|
||||
state_topic: !!(firstExpose.access & ACCESS_STATE),
|
||||
command_topic_prefix: endpoint,
|
||||
command_topic: true,
|
||||
command_topic_postfix: firstExpose.property,
|
||||
options: firstExpose.values.map((v) => v.toString()),
|
||||
...lookup[firstExpose.name],
|
||||
},
|
||||
});
|
||||
}
|
||||
} else if (firstExpose.type === 'text' || firstExpose.type === 'composite' || firstExpose.type === 'list') {
|
||||
// Deprecated: remove text sensor
|
||||
const settableText = firstExpose.type === 'text' && firstExpose.access & ACCESS_SET;
|
||||
const lookup: {[s: string]: KeyValue} = {
|
||||
action: {icon: 'mdi:gesture-double-tap'},
|
||||
programming_mode: {icon: 'mdi:calendar-clock'},
|
||||
program: {value_template: `{{ value_json.${firstExpose.property}|default("") ` +
|
||||
`| truncate(254, True, '', 0) }}`},
|
||||
};
|
||||
if (firstExpose.access & ACCESS_STATE) {
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: 'sensor',
|
||||
object_id: firstExpose.property,
|
||||
mockProperties: [{property: firstExpose.property, value: null}],
|
||||
discovery_payload: {
|
||||
value_template: `{{ value_json.${firstExpose.property} }}`,
|
||||
enabled_by_default: !settableText,
|
||||
...lookup[firstExpose.name],
|
||||
},
|
||||
};
|
||||
discoveryEntries.push(discoveryEntry);
|
||||
}
|
||||
if (settableText) {
|
||||
discoveryEntries.push({
|
||||
type: 'text',
|
||||
object_id: firstExpose.property,
|
||||
mockProperties: [], // Already mocked above in case access STATE is supported
|
||||
discovery_payload: {
|
||||
state_topic: firstExpose.access & ACCESS_STATE,
|
||||
value_template: `{{ value_json.${firstExpose.property} }}`,
|
||||
command_topic_prefix: endpoint,
|
||||
command_topic: true,
|
||||
command_topic_postfix: firstExpose.property,
|
||||
...lookup[firstExpose.name],
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Unsupported exposes type: '${firstExpose.type}'`);
|
||||
}
|
||||
@@ -872,7 +953,11 @@ export default class HomeAssistant extends Extension {
|
||||
const entity = this.zigbee.resolveEntity(data.entity.name);
|
||||
if (entity.isDevice() && this.discovered[entity.ieeeAddr]) {
|
||||
for (const objectID of this.discovered[entity.ieeeAddr].objectIDs) {
|
||||
const match = /light_(.*)/.exec(objectID);
|
||||
const lightMatch = /^light_(.*)/.exec(objectID);
|
||||
const coverMatch = /^cover_(.*)/.exec(objectID);
|
||||
|
||||
const match = lightMatch || coverMatch;
|
||||
|
||||
if (match) {
|
||||
const endpoint = match[1];
|
||||
const endpointRegExp = new RegExp(`(.*)_${endpoint}`);
|
||||
@@ -907,7 +992,7 @@ export default class HomeAssistant extends Extension {
|
||||
* Implements the MQTT device trigger (https://www.home-assistant.io/integrations/device_trigger.mqtt/)
|
||||
* The MQTT device trigger does not support JSON parsing, so it cannot listen to zigbee2mqtt/my_device
|
||||
* Whenever a device publish an {action: *} we discover an MQTT device trigger sensor
|
||||
* and republish it to zigbee2mqtt/my_devic/action
|
||||
* and republish it to zigbee2mqtt/my_device/action
|
||||
*/
|
||||
if (entity.isDevice() && entity.definition) {
|
||||
const keys = ['action', 'click'].filter((k) => data.message[k]);
|
||||
@@ -919,7 +1004,7 @@ export default class HomeAssistant extends Extension {
|
||||
}
|
||||
}
|
||||
|
||||
@bind onEntityRenamed(data: eventdata.EntityRenamed): void {
|
||||
@bind async onEntityRenamed(data: eventdata.EntityRenamed): Promise<void> {
|
||||
logger.debug(`Refreshing Home Assistant discovery topic for '${data.entity.name}'`);
|
||||
|
||||
// Clear before rename so Home Assistant uses new friendly_name
|
||||
@@ -929,6 +1014,10 @@ export default class HomeAssistant extends Extension {
|
||||
const topic = this.getDiscoveryTopic(config, data.entity);
|
||||
this.mqtt.publish(topic, null, {retain: true, qos: 0}, this.discoveryTopic, false, false);
|
||||
}
|
||||
|
||||
// Make sure Home Assistant deletes the old entity first otherwise another one (_2) is created
|
||||
// https://github.com/Koenkk/zigbee2mqtt/issues/12610
|
||||
await utils.sleep(2);
|
||||
}
|
||||
|
||||
this.discover(data.entity, true);
|
||||
@@ -951,7 +1040,7 @@ export default class HomeAssistant extends Extension {
|
||||
if (isDevice) {
|
||||
const exposes = entity.exposes(); // avoid calling it hundred of times/s
|
||||
for (const expose of exposes) {
|
||||
configs.push(...this.exposeToConfig([expose], 'device', entity.definition, exposes));
|
||||
configs.push(...this.exposeToConfig([expose], 'device', exposes, entity.definition));
|
||||
}
|
||||
|
||||
for (const mapping of legacyMapping) {
|
||||
@@ -968,10 +1057,13 @@ export default class HomeAssistant extends Extension {
|
||||
}
|
||||
} else { // group
|
||||
const exposesByType: {[s: string]: zhc.DefinitionExpose[]} = {};
|
||||
const allExposes: zhc.DefinitionExpose[] = [];
|
||||
|
||||
entity.zh.members.map((e) => this.zigbee.resolveEntity(e.getDevice()) as Device)
|
||||
.filter((d) => d.definition).forEach((device) => {
|
||||
for (const expose of device.exposes().filter((e) => groupSupportedTypes.includes(e.type))) {
|
||||
const exposes = device.exposes();
|
||||
allExposes.push(...exposes);
|
||||
for (const expose of exposes.filter((e) => groupSupportedTypes.includes(e.type))) {
|
||||
let key = expose.type;
|
||||
if (['switch', 'lock', 'cover'].includes(expose.type) && expose.endpoint) {
|
||||
// A device can have multiple of these types which have to discovered seperately.
|
||||
@@ -986,7 +1078,7 @@ export default class HomeAssistant extends Extension {
|
||||
});
|
||||
|
||||
configs = [].concat(...Object.values(exposesByType)
|
||||
.map((exposes) => this.exposeToConfig(exposes, 'group')));
|
||||
.map((exposes) => this.exposeToConfig(exposes, 'group', allExposes)));
|
||||
}
|
||||
|
||||
if (isDevice && settings.get().advanced.last_seen !== 'disable') {
|
||||
@@ -1014,7 +1106,7 @@ export default class HomeAssistant extends Extension {
|
||||
const updateStateSensor: DiscoveryEntry = {
|
||||
type: 'sensor',
|
||||
object_id: 'update_state',
|
||||
mockProperties: [{property: 'update', value: {state: null}}],
|
||||
mockProperties: [], // update is mocked below with updateSensor
|
||||
discovery_payload: {
|
||||
icon: 'mdi:update',
|
||||
value_template: `{{ value_json['update']['state'] }}`,
|
||||
@@ -1032,12 +1124,28 @@ export default class HomeAssistant extends Extension {
|
||||
payload_on: true,
|
||||
payload_off: false,
|
||||
value_template: `{{ value_json['update']['state'] == "available" }}`,
|
||||
enabled_by_default: true,
|
||||
enabled_by_default: false,
|
||||
device_class: 'update',
|
||||
entity_category: 'diagnostic',
|
||||
},
|
||||
};
|
||||
configs.push(updateAvailableSensor);
|
||||
const updateSensor: DiscoveryEntry = {
|
||||
type: 'update',
|
||||
object_id: 'update',
|
||||
mockProperties: [{property: 'update', value: {state: null}}],
|
||||
discovery_payload: {
|
||||
entity_picture: 'https://github.com/Koenkk/zigbee2mqtt/raw/master/images/logo.png',
|
||||
latest_version_topic: true,
|
||||
state_topic: true,
|
||||
device_class: 'firmware',
|
||||
command_topic: `${settings.get().mqtt.base_topic}/bridge/request/device/ota_update/update`,
|
||||
payload_install: `{"id": "${entity.ieeeAddr}"}`,
|
||||
value_template: `{{ value_json['update']['installed_version'] }}`,
|
||||
latest_version_template: `{{ value_json['update']['latest_version'] }}`,
|
||||
},
|
||||
};
|
||||
configs.push(updateSensor);
|
||||
}
|
||||
|
||||
if (isDevice && entity.options.hasOwnProperty('legacy') && !entity.options.legacy) {
|
||||
@@ -1139,7 +1247,10 @@ export default class HomeAssistant extends Extension {
|
||||
payload.availability.push({topic: `${baseTopic}/availability`});
|
||||
}
|
||||
|
||||
if (!settings.get().advanced.legacy_availability_payload) {
|
||||
if (entity.isDevice() && entity.options.disabled) {
|
||||
// Mark disabled device always as unavailable
|
||||
payload.availability.forEach((a: KeyValue) => a.value_template = '{{ "offline" }}');
|
||||
} else if (!settings.get().advanced.legacy_availability_payload) {
|
||||
payload.availability.forEach((a: KeyValue) => a.value_template = '{{ value_json.state }}');
|
||||
}
|
||||
|
||||
@@ -1149,7 +1260,7 @@ export default class HomeAssistant extends Extension {
|
||||
delete payload.command_topic_postfix;
|
||||
const commandTopic = `${baseTopic}/${commandTopicPrefix}set${commandTopicPostfix}`;
|
||||
|
||||
if (payload.command_topic) {
|
||||
if (payload.command_topic && typeof payload.command_topic !== 'string') {
|
||||
payload.command_topic = commandTopic;
|
||||
}
|
||||
|
||||
@@ -1169,22 +1280,6 @@ export default class HomeAssistant extends Extension {
|
||||
payload.mode_command_topic = `${baseTopic}/${commandTopicPrefix}set/system_mode`;
|
||||
}
|
||||
|
||||
if (payload.hold_command_topic) {
|
||||
payload.hold_command_topic = `${baseTopic}/${commandTopicPrefix}set/preset`;
|
||||
}
|
||||
|
||||
if (payload.hold_state_topic) {
|
||||
payload.hold_state_topic = stateTopic;
|
||||
}
|
||||
|
||||
if (payload.away_mode_state_topic) {
|
||||
payload.away_mode_state_topic = stateTopic;
|
||||
}
|
||||
|
||||
if (payload.away_mode_command_topic) {
|
||||
payload.away_mode_command_topic = `${baseTopic}/${commandTopicPrefix}set/away_mode`;
|
||||
}
|
||||
|
||||
if (payload.current_temperature_topic) {
|
||||
payload.current_temperature_topic = stateTopic;
|
||||
}
|
||||
@@ -1220,10 +1315,22 @@ export default class HomeAssistant extends Extension {
|
||||
payload.fan_mode_state_topic = stateTopic;
|
||||
}
|
||||
|
||||
if (payload.latest_version_topic) {
|
||||
payload.latest_version_topic = stateTopic;
|
||||
}
|
||||
|
||||
if (payload.fan_mode_command_topic) {
|
||||
payload.fan_mode_command_topic = `${baseTopic}/${commandTopicPrefix}set/fan_mode`;
|
||||
}
|
||||
|
||||
if (payload.swing_mode_state_topic) {
|
||||
payload.swing_mode_state_topic = stateTopic;
|
||||
}
|
||||
|
||||
if (payload.swing_mode_command_topic) {
|
||||
payload.swing_mode_command_topic = `${baseTopic}/${commandTopicPrefix}set/swing_mode`;
|
||||
}
|
||||
|
||||
if (payload.percentage_state_topic) {
|
||||
payload.percentage_state_topic = stateTopic;
|
||||
}
|
||||
@@ -1237,7 +1344,8 @@ export default class HomeAssistant extends Extension {
|
||||
}
|
||||
|
||||
if (payload.preset_mode_command_topic) {
|
||||
payload.preset_mode_command_topic = `${baseTopic}/${commandTopicPrefix}set/fan_mode`;
|
||||
payload.preset_mode_command_topic = `${baseTopic}/${commandTopicPrefix}set/` +
|
||||
payload.preset_mode_command_topic;
|
||||
}
|
||||
|
||||
if (payload.action_topic) {
|
||||
@@ -1339,9 +1447,9 @@ export default class HomeAssistant extends Extension {
|
||||
data.message.toLowerCase() === 'online') {
|
||||
const timer = setTimeout(async () => {
|
||||
// Publish all device states.
|
||||
for (const device of this.zigbee.devices(false)) {
|
||||
if (this.state.exists(device)) {
|
||||
this.publishEntityState(device, this.state.get(device));
|
||||
for (const entity of [...this.zigbee.devices(false), ...this.zigbee.groups()]) {
|
||||
if (this.state.exists(entity)) {
|
||||
this.publishEntityState(entity, this.state.get(entity), 'publishCached');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1358,7 +1466,7 @@ export default class HomeAssistant extends Extension {
|
||||
const identifierPostfix = entity.isGroup() ?
|
||||
`zigbee2mqtt_${this.getEncodedBaseTopic()}` : 'zigbee2mqtt';
|
||||
|
||||
// Allow device name to be overriden by homeassistant config
|
||||
// Allow device name to be overridden by homeassistant config
|
||||
let deviceName = entity.name;
|
||||
if (typeof entity.options.homeassistant?.name === 'string') {
|
||||
deviceName = entity.options.homeassistant.name;
|
||||
@@ -1393,7 +1501,7 @@ export default class HomeAssistant extends Extension {
|
||||
}
|
||||
});
|
||||
|
||||
// Copy hue -> h, saturation -> s to make homeassitant happy
|
||||
// Copy hue -> h, saturation -> s to make homeassistant happy
|
||||
if (message.hasOwnProperty('color')) {
|
||||
if (message.color.hasOwnProperty('hue')) {
|
||||
message.color.h = message.color.hue;
|
||||
@@ -1402,6 +1510,10 @@ export default class HomeAssistant extends Extension {
|
||||
message.color.s = message.color.saturation;
|
||||
}
|
||||
}
|
||||
|
||||
if (entity.isDevice() && entity.definition?.ota && message.update?.latest_version == null) {
|
||||
message.update = {...message.update, installed_version: -1, latest_version: -1};
|
||||
}
|
||||
}
|
||||
|
||||
private getEncodedBaseTopic(): string {
|
||||
|
||||
@@ -214,7 +214,7 @@ export default class NetworkMap extends Extension {
|
||||
|
||||
async networkScan(includeRoutes: boolean): Promise<Topology> {
|
||||
logger.info(`Starting network scan (includeRoutes '${includeRoutes}')`);
|
||||
const devices = this.zigbee.devices().filter((d) => d.zh.type !== 'GreenPower');
|
||||
const devices = this.zigbee.devices().filter((d) => d.zh.type !== 'GreenPower' && !d.options.disabled);
|
||||
const lqis: Map<Device, zh.LQI> = new Map();
|
||||
const routingTables: Map<Device, zh.RoutingTable> = new Map();
|
||||
const failed: Map<Device, string[]> = new Map();
|
||||
@@ -244,6 +244,7 @@ export default class NetworkMap extends Extension {
|
||||
} catch (error) {
|
||||
failed.get(device).push('lqi');
|
||||
logger.error(`Failed to execute LQI for '${device.name}'`);
|
||||
logger.debug(error.stack);
|
||||
}
|
||||
|
||||
if (includeRoutes) {
|
||||
|
||||
@@ -40,10 +40,11 @@ export default class OnEvent extends Extension {
|
||||
}
|
||||
|
||||
private async callOnEvent(device: Device, type: string, data: KeyValue): Promise<void> {
|
||||
zhc.onEvent(type, data, device.zh);
|
||||
const state = this.state.get(device);
|
||||
zhc.onEvent(type, data, device.zh, device.options, state);
|
||||
|
||||
if (device.definition?.onEvent) {
|
||||
await device.definition.onEvent(type, data, device.zh, device.options);
|
||||
await device.definition.onEvent(type, data, device.zh, device.options, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+45
-33
@@ -2,8 +2,8 @@ import * as settings from '../util/settings';
|
||||
import logger from '../util/logger';
|
||||
import stringify from 'json-stable-stringify-without-jsonify';
|
||||
import utils from '../util/utils';
|
||||
import tradfriOTA from 'zigbee-herdsman-converters/lib/ota/tradfri';
|
||||
import zigbeeOTA from 'zigbee-herdsman-converters/lib/ota/zigbeeOTA';
|
||||
import * as tradfriOTA from 'zigbee-herdsman-converters/lib/ota/tradfri';
|
||||
import * as zigbeeOTA from 'zigbee-herdsman-converters/lib/ota/zigbeeOTA';
|
||||
import Extension from './extension';
|
||||
import bind from 'bind-decorator';
|
||||
import Device from '../model/device';
|
||||
@@ -24,8 +24,13 @@ function isValidUrl(url: string): boolean {
|
||||
|
||||
type UpdateState = 'updating' | 'idle' | 'available';
|
||||
interface UpdatePayload {
|
||||
update_available?: boolean
|
||||
// eslint-disable-next-line camelcase
|
||||
update: {progress?: number, remaining?: number, state: UpdateState}, update_available?: boolean}
|
||||
update: {
|
||||
progress?: number, remaining?: number, state: UpdateState,
|
||||
installed_version: number | null, latest_version: number | null
|
||||
}
|
||||
}
|
||||
|
||||
const legacyTopicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/ota_update/.+$`);
|
||||
const topicRegex =
|
||||
@@ -65,38 +70,39 @@ export default class OTAUpdate extends Extension {
|
||||
}
|
||||
|
||||
private removeProgressAndRemainingFromState(device: Device): void {
|
||||
delete this.state.get(device)?.update?.progress;
|
||||
delete this.state.get(device)?.update?.remaining;
|
||||
delete this.state.get(device).update?.progress;
|
||||
delete this.state.get(device).update?.remaining;
|
||||
}
|
||||
|
||||
@bind private async onZigbeeEvent(data: eventdata.DeviceMessage): Promise<void> {
|
||||
if (settings.get().ota.disable_automatic_update_check) return;
|
||||
if (data.type !== 'commandQueryNextImageRequest' || !data.device.definition) return;
|
||||
if (data.type !== 'commandQueryNextImageRequest' || !data.device.definition ||
|
||||
this.inProgress.has(data.device.ieeeAddr)) return;
|
||||
logger.debug(`Device '${data.device.name}' requested OTA`);
|
||||
|
||||
const automaticOTACheckDisabled = settings.get().ota.disable_automatic_update_check;
|
||||
let supportsOTA = data.device.definition.hasOwnProperty('ota');
|
||||
if (supportsOTA) {
|
||||
if (supportsOTA && !automaticOTACheckDisabled) {
|
||||
// When a device does a next image request, it will usually do it a few times after each other
|
||||
// with only 10 - 60 seconds inbetween. It doesn't make sense to check for a new update
|
||||
// each time, so this interval can be set by the user. The default is 1,440 minutes (one day).
|
||||
const updateCheckInterval = settings.get().ota.update_check_interval * 1000 * 60;
|
||||
const check = this.lastChecked.hasOwnProperty(data.device.ieeeAddr) ?
|
||||
(Date.now() - this.lastChecked[data.device.ieeeAddr]) > updateCheckInterval : true;
|
||||
if (!check || this.inProgress.has(data.device.ieeeAddr)) return;
|
||||
if (!check) return;
|
||||
|
||||
this.lastChecked[data.device.ieeeAddr] = Date.now();
|
||||
let available = false;
|
||||
let availableResult: zhc.OtaUpdateAvailableResult = null;
|
||||
try {
|
||||
available = await data.device.definition.ota.isUpdateAvailable(data.device.zh, logger, data.data);
|
||||
availableResult = await data.device.definition.ota.isUpdateAvailable(data.device.zh, logger, data.data);
|
||||
} catch (e) {
|
||||
supportsOTA = false;
|
||||
logger.debug(`Failed to check if update available for '${data.device.name}' (${e.message})`);
|
||||
}
|
||||
|
||||
const payload = this.getEntityPublishPayload(available ? 'available' : 'idle');
|
||||
const payload = this.getEntityPublishPayload(data.device, availableResult ?? 'idle');
|
||||
this.publishEntityState(data.device, payload);
|
||||
|
||||
if (available) {
|
||||
if (availableResult?.available) {
|
||||
const message = `Update available for '${data.device.name}'`;
|
||||
logger.info(message);
|
||||
|
||||
@@ -111,14 +117,10 @@ export default class OTAUpdate extends Extension {
|
||||
}
|
||||
}
|
||||
|
||||
// Respond to the OTA request:
|
||||
// - In case we don't support OTA: respond with NO_IMAGE_AVAILABLE (0x98) (so the client stops requesting OTAs)
|
||||
// - In case we do support OTA: respond with ABORT (0x95) as we don't want to update now.
|
||||
const endpoint = data.device.zh.endpoints.find((e) => e.supportsOutputCluster('genOta'));
|
||||
if (endpoint) {
|
||||
// Some devices send OTA requests without defining OTA cluster as input cluster.
|
||||
await endpoint.commandResponse('genOta', 'queryNextImageResponse', {status: supportsOTA ? 0x95 : 0x98});
|
||||
}
|
||||
// Respond to the OTA request: respond with NO_IMAGE_AVAILABLE (0x98) (so the client stops requesting OTAs)
|
||||
const endpoint = data.device.zh.endpoints.find((e) => e.supportsOutputCluster('genOta')) || data.endpoint;
|
||||
await endpoint.commandResponse('genOta', 'queryNextImageResponse', {status: 0x98});
|
||||
logger.debug(`Responded to OTA request of '${data.device.name}' with 'NO_IMAGE_AVAILABLE'`);
|
||||
}
|
||||
|
||||
private async readSoftwareBuildIDAndDateCode(device: Device, sendWhen: 'active' | 'immediate'):
|
||||
@@ -132,14 +134,22 @@ export default class OTAUpdate extends Extension {
|
||||
}
|
||||
}
|
||||
|
||||
private getEntityPublishPayload(state: UpdateState, progress: number=null, remaining: number=null): UpdatePayload {
|
||||
const payload: UpdatePayload = {update: {state}};
|
||||
private getEntityPublishPayload(device: Device, state: zhc.OtaUpdateAvailableResult | UpdateState,
|
||||
progress: number=null, remaining: number=null): UpdatePayload {
|
||||
const deviceUpdateState = this.state.get(device).update;
|
||||
const payload: UpdatePayload = {update: {
|
||||
state: typeof state === 'string' ? state : (state.available ? 'available' : 'idle'),
|
||||
installed_version: typeof state === 'string' ?
|
||||
deviceUpdateState?.installed_version : state.currentFileVersion,
|
||||
latest_version: typeof state === 'string' ?
|
||||
deviceUpdateState?.latest_version : state.otaFileVersion,
|
||||
}};
|
||||
if (progress !== null) payload.update.progress = progress;
|
||||
if (remaining !== null) payload.update.remaining = Math.round(remaining);
|
||||
|
||||
/* istanbul ignore else */
|
||||
if (this.legacyApi) {
|
||||
payload.update_available = state === 'available';
|
||||
payload.update_available = typeof state === 'string' ? state === 'available' : state.available;
|
||||
}
|
||||
|
||||
return payload;
|
||||
@@ -190,23 +200,24 @@ export default class OTAUpdate extends Extension {
|
||||
}
|
||||
|
||||
try {
|
||||
const available = await device.definition.ota.isUpdateAvailable(device.zh, logger);
|
||||
const msg = `${available ? 'Update' : 'No update'} available for '${device.name}'`;
|
||||
const availableResult = await device.definition.ota.isUpdateAvailable(device.zh, logger);
|
||||
const msg = `${availableResult.available ? 'Update' : 'No update'} available for '${device.name}'`;
|
||||
logger.info(msg);
|
||||
|
||||
/* istanbul ignore else */
|
||||
if (settings.get().advanced.legacy_api) {
|
||||
const meta = {status: available ? 'available' : 'not_available', device: device.name};
|
||||
const meta = {
|
||||
status: availableResult.available ? 'available' : 'not_available', device: device.name};
|
||||
this.mqtt.publish(
|
||||
'bridge/log',
|
||||
stringify({type: `ota_update`, message: msg, meta}),
|
||||
);
|
||||
}
|
||||
|
||||
const payload = this.getEntityPublishPayload(available ? 'available' : 'idle');
|
||||
const payload = this.getEntityPublishPayload(device, availableResult);
|
||||
this.publishEntityState(device, payload);
|
||||
this.lastChecked[device.ieeeAddr] = Date.now();
|
||||
responseData.updateAvailable = available;
|
||||
responseData.updateAvailable = availableResult.available;
|
||||
} catch (e) {
|
||||
error = `Failed to check if update available for '${device.name}' (${e.message})`;
|
||||
errorStack = e.stack;
|
||||
@@ -242,7 +253,7 @@ export default class OTAUpdate extends Extension {
|
||||
|
||||
logger.info(msg);
|
||||
|
||||
const payload = this.getEntityPublishPayload('updating', progress, remaining);
|
||||
const payload = this.getEntityPublishPayload(device, 'updating', progress, remaining);
|
||||
this.publishEntityState(device, payload);
|
||||
|
||||
/* istanbul ignore else */
|
||||
@@ -253,11 +264,12 @@ export default class OTAUpdate extends Extension {
|
||||
};
|
||||
|
||||
const from_ = await this.readSoftwareBuildIDAndDateCode(device, 'immediate');
|
||||
await device.definition.ota.updateToLatest(device.zh, logger, onProgress);
|
||||
const fileVersion = await device.definition.ota.updateToLatest(device.zh, logger, onProgress);
|
||||
logger.info(`Finished update of '${device.name}'`);
|
||||
this.eventBus.emitReconfigure({device});
|
||||
this.removeProgressAndRemainingFromState(device);
|
||||
const payload = this.getEntityPublishPayload('idle');
|
||||
const payload = this.getEntityPublishPayload(device,
|
||||
{available: false, currentFileVersion: fileVersion, otaFileVersion: fileVersion});
|
||||
this.publishEntityState(device, payload);
|
||||
const to = await this.readSoftwareBuildIDAndDateCode(device, 'active');
|
||||
const [fromS, toS] = [stringify(from_), stringify(to)];
|
||||
@@ -277,7 +289,7 @@ export default class OTAUpdate extends Extension {
|
||||
errorStack = e.stack;
|
||||
|
||||
this.removeProgressAndRemainingFromState(device);
|
||||
const payload = this.getEntityPublishPayload('available');
|
||||
const payload = this.getEntityPublishPayload(device, 'available');
|
||||
this.publishEntityState(device, payload);
|
||||
|
||||
/* istanbul ignore else */
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
|
||||
import * as settings from '../util/settings';
|
||||
import zigbeeHerdsmanConverters from 'zigbee-herdsman-converters';
|
||||
import * as philips from 'zigbee-herdsman-converters/lib/philips';
|
||||
import logger from '../util/logger';
|
||||
import utils from '../util/utils';
|
||||
import Extension from './extension';
|
||||
@@ -18,7 +19,7 @@ const sceneConverterKeys = ['scene_store', 'scene_add', 'scene_remove', 'scene_r
|
||||
const defaultGroupConverters = [
|
||||
zigbeeHerdsmanConverters.toZigbeeConverters.light_onoff_brightness,
|
||||
zigbeeHerdsmanConverters.toZigbeeConverters.light_color_colortemp,
|
||||
zigbeeHerdsmanConverters.toZigbeeConverters.effect,
|
||||
philips.tz.effect, // Support Hue effects for groups
|
||||
zigbeeHerdsmanConverters.toZigbeeConverters.ignore_transition,
|
||||
zigbeeHerdsmanConverters.toZigbeeConverters.cover_position_tilt,
|
||||
zigbeeHerdsmanConverters.toZigbeeConverters.thermostat_occupied_heating_setpoint,
|
||||
@@ -89,7 +90,7 @@ export default class Publish extends Extension {
|
||||
// When reporting is requested for a device (report: true in device-specific settings) we won't
|
||||
// ever issue a read here, as we assume the device will properly report changes.
|
||||
// Only do this when the retrieve_state option is enabled for this device.
|
||||
// retrieve_state == decprecated
|
||||
// retrieve_state == deprecated
|
||||
if (re instanceof Device && result && result.hasOwnProperty('readAfterWriteTime') &&
|
||||
re.options.retrieve_state
|
||||
) {
|
||||
@@ -101,7 +102,7 @@ export default class Publish extends Extension {
|
||||
/**
|
||||
* Home Assistant always publishes 'state', even when e.g. only setting
|
||||
* the color temperature. This would lead to 2 zigbee publishes, where the first one
|
||||
* (state) is probably unecessary.
|
||||
* (state) is probably unnecessary.
|
||||
*/
|
||||
if (settings.get().homeassistant) {
|
||||
const hasColorTemp = message.hasOwnProperty('color_temp');
|
||||
@@ -135,7 +136,7 @@ export default class Publish extends Extension {
|
||||
}
|
||||
const device = re instanceof Device ? re.zh : null;
|
||||
const entitySettings = re.options;
|
||||
const entityState = this.state.get(re) || {};
|
||||
const entityState = this.state.get(re);
|
||||
const membersState = re instanceof Group ?
|
||||
Object.fromEntries(re.zh.members.map((e) => [e.getDevice().ieeeAddr,
|
||||
this.state.get(this.zigbee.resolveEntity(e.getDevice().ieeeAddr))])) : null;
|
||||
@@ -143,6 +144,7 @@ export default class Publish extends Extension {
|
||||
{
|
||||
if (Array.isArray(definition)) {
|
||||
const c = new Set(definition.map((d) => d.toZigbee).flat());
|
||||
// @ts-expect-error
|
||||
if (c.size == 0) converters = defaultGroupConverters;
|
||||
else converters = Array.from(c);
|
||||
} else if (definition) {
|
||||
@@ -165,7 +167,7 @@ export default class Publish extends Extension {
|
||||
* Order state & brightness based on current bulb state
|
||||
*
|
||||
* Not all bulbs support setting the color/color_temp while it is off
|
||||
* this results in inconsistant behavior between different vendors.
|
||||
* this results in inconsistent behavior between different vendors.
|
||||
*
|
||||
* bulb on => move state & brightness to the back
|
||||
* bulb off => move state & brightness to the front
|
||||
@@ -219,7 +221,7 @@ export default class Publish extends Extension {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the endpoint_name name is a nubmer, try to map it to a friendlyName
|
||||
// If the endpoint_name name is a number, try to map it to a friendlyName
|
||||
if (!isNaN(Number(endpointName)) && re.isDevice() && utils.isEndpoint(localTarget) &&
|
||||
re.endpointName(localTarget)) {
|
||||
endpointName = re.endpointName(localTarget);
|
||||
@@ -256,7 +258,8 @@ export default class Publish extends Extension {
|
||||
}
|
||||
|
||||
// filter out attribute listed in filtered_optimistic
|
||||
entitySettings.filtered_optimistic?.forEach((a) => delete msg[a]);
|
||||
utils.filterProperties(entitySettings.filtered_optimistic, msg);
|
||||
|
||||
addToToPublish(re, msg);
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,13 @@ export default class Receive extends Extension {
|
||||
|
||||
// extend debounced payload with current
|
||||
this.debouncers[device.ieeeAddr].payload = {...this.debouncers[device.ieeeAddr].payload, ...payload};
|
||||
|
||||
// Update state cache right away. This makes sure that during debouncing cached state is always up to date.
|
||||
// ( Update right away as "lastSeenChanged" event might occur while debouncer is still active.
|
||||
// And if that happens it would cause old message to be published from cache.
|
||||
// By updating cache we make sure that state cache is always up-to-date.
|
||||
this.state.set(device, this.debouncers[device.ieeeAddr].payload);
|
||||
|
||||
this.debouncers[device.ieeeAddr].publish();
|
||||
}
|
||||
|
||||
@@ -145,9 +152,9 @@ export default class Receive extends Extension {
|
||||
if (converted) {
|
||||
payload = {...payload, ...converted};
|
||||
}
|
||||
} catch (error) {
|
||||
// istanbul ignore next
|
||||
} catch (error) /* istanbul ignore next */ {
|
||||
logger.error(`Exception while calling fromZigbee converter: ${error.message}}`);
|
||||
logger.debug(error.stack);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+34
-2
@@ -10,6 +10,10 @@ export default class MQTT {
|
||||
private connectionTimer: NodeJS.Timeout;
|
||||
private client: mqtt.MqttClient;
|
||||
private eventBus: EventBus;
|
||||
private initialConnect = true;
|
||||
private republishRetainedTimer: NodeJS.Timer;
|
||||
private retainedMessages: {[s: string]: {payload: string, options: MQTTOptions,
|
||||
skipLog: boolean, skipReceive: boolean, topic: string, base: string}} = {};
|
||||
|
||||
constructor(eventBus: EventBus) {
|
||||
this.eventBus = eventBus;
|
||||
@@ -79,7 +83,10 @@ export default class MQTT {
|
||||
resolve();
|
||||
});
|
||||
|
||||
this.client.on('error', (err) => reject(err));
|
||||
this.client.on('error', (err) => {
|
||||
logger.error(`MQTT error: ${err.message}`);
|
||||
reject(err);
|
||||
});
|
||||
this.client.on('message', this.onMessage);
|
||||
});
|
||||
}
|
||||
@@ -94,8 +101,19 @@ export default class MQTT {
|
||||
}, utils.seconds(10));
|
||||
|
||||
logger.info('Connected to MQTT server');
|
||||
this.subscribe(`${settings.get().mqtt.base_topic}/#`);
|
||||
await this.publishStateOnline();
|
||||
|
||||
if (!this.initialConnect) {
|
||||
this.republishRetainedTimer = setTimeout(() => {
|
||||
// Republish retained messages in case MQTT broker does not persist them.
|
||||
// https://github.com/Koenkk/zigbee2mqtt/issues/9629
|
||||
Object.values(this.retainedMessages).forEach((e) =>
|
||||
this.publish(e.topic, e.payload, e.options, e.base, e.skipLog, e.skipReceive));
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
this.initialConnect = false;
|
||||
this.subscribe(`${settings.get().mqtt.base_topic}/#`);
|
||||
}
|
||||
|
||||
async publishStateOnline(): Promise<void> {
|
||||
@@ -121,6 +139,11 @@ export default class MQTT {
|
||||
logger.debug(`Received MQTT message on '${topic}' with data '${message}'`);
|
||||
this.eventBus.emitMQTTMessage({topic, message: message + ''});
|
||||
}
|
||||
|
||||
if (this.republishRetainedTimer && topic === `${settings.get().mqtt.base_topic}/bridge/info`) {
|
||||
clearTimeout(this.republishRetainedTimer);
|
||||
this.republishRetainedTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
isConnected(): boolean {
|
||||
@@ -137,6 +160,15 @@ export default class MQTT {
|
||||
this.publishedTopics.add(topic);
|
||||
}
|
||||
|
||||
if (options.retain) {
|
||||
if (payload) {
|
||||
this.retainedMessages[topic] =
|
||||
{payload, options, skipReceive, skipLog, topic: topic.substring(base.length + 1), base};
|
||||
} else {
|
||||
delete this.retainedMessages[topic];
|
||||
}
|
||||
}
|
||||
|
||||
this.eventBus.emitMQTTMessagePublished({topic, payload, options: {...defaultOptions, ...options}});
|
||||
|
||||
if (!this.isConnected()) {
|
||||
|
||||
+17
-16
@@ -1,25 +1,26 @@
|
||||
import logger from './util/logger';
|
||||
import data from './util/data';
|
||||
import * as settings from './util/settings';
|
||||
import utils from './util/utils';
|
||||
import fs from 'fs';
|
||||
import objectAssignDeep from 'object-assign-deep';
|
||||
|
||||
const saveInterval = 1000 * 60 * 5; // 5 minutes
|
||||
|
||||
const dontCacheProperties = [
|
||||
'^action$', '^action_.*$', '^button$', '^button_left$', '^button_right$', '^click$', '^forgotten$', '^keyerror$',
|
||||
'^step_size$', '^transition_time$', '^group_list$', '^group_capacity$', '^no_occupancy_since$',
|
||||
'^step_mode$', '^transition_time$', '^duration$', '^elapsed$', '^from_side$', '^to_side$',
|
||||
'action', 'action_.*', 'button', 'button_left', 'button_right', 'click', 'forgotten', 'keyerror',
|
||||
'step_size', 'transition_time', 'group_list', 'group_capacity', 'no_occupancy_since',
|
||||
'step_mode', 'transition_time', 'duration', 'elapsed', 'from_side', 'to_side',
|
||||
];
|
||||
|
||||
class State {
|
||||
private state: {[s: string | number]: KeyValue} = {};
|
||||
private file = data.joinPath('state.json');
|
||||
private timer: NodeJS.Timer = null;
|
||||
private eventBus: EventBus;
|
||||
|
||||
constructor(eventBus: EventBus) {
|
||||
constructor(private readonly eventBus: EventBus, private readonly zigbee: Zigbee) {
|
||||
this.eventBus = eventBus;
|
||||
this.zigbee = zigbee;
|
||||
}
|
||||
|
||||
start(): void {
|
||||
@@ -27,11 +28,14 @@ class State {
|
||||
|
||||
// Save the state on every interval
|
||||
this.timer = setInterval(() => this.save(), saveInterval);
|
||||
this.eventBus.onDeviceLeave(this, (data) => delete this.state[data.ieeeAddr]);
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
this.eventBus.removeListeners(this);
|
||||
// Remove any invalid states (ie when the device has left the network) when the system is stopped
|
||||
Object.keys(this.state)
|
||||
.filter((k) => typeof k === 'string' && !this.zigbee.resolveEntity(k)) // string key = ieeeAddr
|
||||
.forEach((k) => delete this.state[k]);
|
||||
|
||||
clearTimeout(this.timer);
|
||||
this.save();
|
||||
}
|
||||
@@ -68,23 +72,20 @@ class State {
|
||||
}
|
||||
|
||||
get(entity: Group | Device): KeyValue {
|
||||
return this.state[entity.ID];
|
||||
return this.state[entity.ID] || {};
|
||||
}
|
||||
|
||||
set(entity: Group | Device, update: KeyValue, reason: string=null): KeyValue {
|
||||
const fromState = this.state[entity.ID] || {};
|
||||
const toState = objectAssignDeep({}, fromState, update);
|
||||
const result = {...toState};
|
||||
const newCache = {...toState};
|
||||
const entityDontCacheProperties = entity.options.filtered_cache || [];
|
||||
|
||||
for (const property of Object.keys(toState)) {
|
||||
if (dontCacheProperties.find((p) => property.match(p))) {
|
||||
delete toState[property];
|
||||
}
|
||||
}
|
||||
utils.filterProperties(dontCacheProperties.concat(entityDontCacheProperties), newCache);
|
||||
|
||||
this.state[entity.ID] = toState;
|
||||
this.state[entity.ID] = newCache;
|
||||
this.eventBus.emitStateChange({entity, from: fromState, to: toState, reason, update});
|
||||
return result;
|
||||
return toState;
|
||||
}
|
||||
|
||||
remove(ID: string | number): void {
|
||||
|
||||
Vendored
+22
-8
@@ -17,6 +17,10 @@ import type {
|
||||
Cluster as ZHCluster,
|
||||
} from 'zigbee-herdsman/dist/zcl/tstype';
|
||||
|
||||
import type {
|
||||
FrameControl as ZHFrameControl,
|
||||
} from 'zigbee-herdsman/dist/zcl';
|
||||
|
||||
import type * as ZHEvents from 'zigbee-herdsman/dist/controller/events';
|
||||
|
||||
import type TypeEventBus from 'lib/eventBus';
|
||||
@@ -42,7 +46,7 @@ declare global {
|
||||
// Types
|
||||
interface MQTTResponse {data: KeyValue, status: 'error' | 'ok', error?: string, transaction?: string}
|
||||
interface MQTTOptions {qos?: mqtt.QoS, retain?: boolean, properties?: {messageExpiryInterval: number}}
|
||||
type StateChangeReason = 'publishDebounce' | 'groupOptimistic' | 'lastSeenChanged';
|
||||
type StateChangeReason = 'publishDebounce' | 'groupOptimistic' | 'lastSeenChanged' | 'publishCached';
|
||||
type PublishEntityState = (entity: Device | Group, payload: KeyValue,
|
||||
stateChangeReason?: StateChangeReason) => Promise<void>;
|
||||
type RecursivePartial<T> = {[P in keyof T]?: RecursivePartial<T[P]>;};
|
||||
@@ -100,10 +104,12 @@ declare global {
|
||||
|
||||
interface DefinitionExpose {
|
||||
type: string, name?: string, features?: DefinitionExposeFeature[],
|
||||
endpoint?: string, values?: string[], value_off?: string, value_on?: string,
|
||||
endpoint?: string, values?: string[], value_off?: string, value_on?: string, value_step?: number,
|
||||
access: number, property: string, unit?: string,
|
||||
value_min?: number, value_max?: number}
|
||||
|
||||
interface OtaUpdateAvailableResult {available: boolean, currentFileVersion: number, otaFileVersion: number}
|
||||
|
||||
interface Definition {
|
||||
model: string,
|
||||
zigbeeModel: string[],
|
||||
@@ -117,11 +123,13 @@ declare global {
|
||||
exposes: DefinitionExpose[] | ((device: zh.Device, options: KeyValue) => DefinitionExpose[])
|
||||
configure?: (device: zh.Device, coordinatorEndpoint: zh.Endpoint, logger: Logger,
|
||||
options?: DeviceOptions) => Promise<void>;
|
||||
onEvent?: (type: string, data: KeyValue, device: zh.Device, settings: KeyValue) => Promise<void>;
|
||||
onEvent?: (type: string, data: KeyValue, device: zh.Device,
|
||||
settings: KeyValue, state: KeyValue) => Promise<void>;
|
||||
ota?: {
|
||||
isUpdateAvailable: (device: zh.Device, logger: Logger, data?: KeyValue) => Promise<boolean>;
|
||||
isUpdateAvailable: (device: zh.Device, logger: Logger, data?: KeyValue)
|
||||
=> Promise<OtaUpdateAvailableResult>;
|
||||
updateToLatest: (device: zh.Device, logger: Logger,
|
||||
onProgress: (progress: number, remaining: number) => void) => Promise<void>;
|
||||
onProgress: (progress: number, remaining: number) => void) => Promise<number>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +166,7 @@ declare global {
|
||||
groupID: number;
|
||||
cluster: string | number;
|
||||
data: KeyValue | Array<string | number>;
|
||||
meta: {zclTransactionSequenceNumber?: number;};
|
||||
meta: {zclTransactionSequenceNumber?: number; manufacturerCode?: number; frameControl?: ZHFrameControl;};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -232,6 +240,8 @@ declare global {
|
||||
host?: string,
|
||||
port?: number,
|
||||
url?: string,
|
||||
ssl_cert?: string,
|
||||
ssl_key?: string,
|
||||
},
|
||||
devices?: {[s: string]: DeviceOptions},
|
||||
groups?: {[s: string]: GroupOptions},
|
||||
@@ -273,17 +283,19 @@ declare global {
|
||||
|
||||
interface DeviceOptions {
|
||||
ID?: string,
|
||||
disabled?: boolean,
|
||||
retention?: number,
|
||||
availability?: boolean | {timeout: number},
|
||||
optimistic?: boolean,
|
||||
retrieve_state?: boolean,
|
||||
debounce?: number,
|
||||
debounce_ignore?: string[],
|
||||
filtered_attributes?: string[],
|
||||
filtered_cache?: string[],
|
||||
filtered_optimistic?: string[],
|
||||
icon?: string,
|
||||
homeassistant?: KeyValue,
|
||||
legacy?: boolean,
|
||||
filtered_attributes?: string[],
|
||||
friendly_name: string,
|
||||
description?: string,
|
||||
qos?: 0 | 1 | 2,
|
||||
@@ -293,10 +305,12 @@ declare global {
|
||||
devices?: string[],
|
||||
ID?: number,
|
||||
optimistic?: boolean,
|
||||
off_state?: 'all_members_off' | 'last_member_state'
|
||||
filtered_attributes?: string[],
|
||||
filtered_cache?: string[],
|
||||
filtered_optimistic?: string[],
|
||||
retrieve_state?: boolean,
|
||||
homeassistant?: KeyValue,
|
||||
filtered_attributes?: string[],
|
||||
friendly_name: string,
|
||||
description?: string,
|
||||
qos?: 0 | 1 | 2,
|
||||
|
||||
+2
-10
@@ -4,14 +4,6 @@ declare module 'zigbee-herdsman-converters' {
|
||||
export const toZigbeeConverters: {[s: string]: zhc.ToZigbeeConverter};
|
||||
export const definitions: zhc.Definition[];
|
||||
export function addDeviceDefinition(definition: zhc.Definition): Promise<void>;
|
||||
export function onEvent(type: string, data: KeyValue, device: zh.Device): Promise<void>;
|
||||
}
|
||||
|
||||
declare module 'zigbee-herdsman-converters/lib/ota/tradfri' {
|
||||
export function useTestURL(): void;
|
||||
}
|
||||
|
||||
declare module 'zigbee-herdsman-converters/lib/ota/zigbeeOTA' {
|
||||
export function useIndexOverride(indexFileName: string): void;
|
||||
export function setDataDir(dataDir: string): void;
|
||||
export function onEvent(type: string, data: KeyValue, device: zh.Device, settings: KeyValue,
|
||||
state: KeyValue): Promise<void>;
|
||||
}
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ function load(): void {
|
||||
load();
|
||||
|
||||
function joinPath(file: string): string {
|
||||
return path.join(dataPath, file);
|
||||
return path.resolve(dataPath, file);
|
||||
}
|
||||
|
||||
function getPath(): string {
|
||||
|
||||
+101
-91
@@ -4,45 +4,17 @@ import * as settings from './settings';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import fx from 'mkdir-recursive';
|
||||
import rimraf from 'rimraf';
|
||||
import {rimrafSync} from 'rimraf';
|
||||
import assert from 'assert';
|
||||
|
||||
const colorizer = winston.format.colorize();
|
||||
|
||||
// What transports to enable
|
||||
const output = settings.get().advanced.log_output;
|
||||
|
||||
// Directory to log to
|
||||
const timestamp = moment(Date.now()).format('YYYY-MM-DD.HH-mm-ss');
|
||||
const directory = settings.get().advanced.log_directory.replace('%TIMESTAMP%', timestamp);
|
||||
const logFilename = settings.get().advanced.log_file.replace('%TIMESTAMP%', timestamp);
|
||||
|
||||
// Make sure that log directoy exsists when not logging to stdout only
|
||||
if (output.includes('file')) {
|
||||
fx.mkdirSync(directory);
|
||||
|
||||
if (settings.get().advanced.log_symlink_current) {
|
||||
const current = settings.get().advanced.log_directory.replace('%TIMESTAMP%', 'current');
|
||||
const actual = './' + timestamp;
|
||||
if (fs.existsSync(current)) {
|
||||
fs.unlinkSync(current);
|
||||
}
|
||||
fs.symlinkSync(actual, current);
|
||||
}
|
||||
}
|
||||
|
||||
type Z2MLogLevel = 'warn' | 'debug' | 'info' | 'error';
|
||||
type WinstonLogLevel = 'warning' | 'debug' | 'info' | 'error';
|
||||
|
||||
const z2mToWinstonLevel = (level: Z2MLogLevel): WinstonLogLevel => level === 'warn' ? 'warning' : level;
|
||||
const winstonToZ2mLevel = (level: WinstonLogLevel): Z2MLogLevel => level === 'warning' ? 'warn' : level;
|
||||
|
||||
// Determine the log level.
|
||||
const z2mLevel = settings.get().advanced.log_level;
|
||||
const validLevels = ['info', 'error', 'warn', 'debug'];
|
||||
assert(validLevels.includes(z2mLevel), `'${z2mLevel}' is not valid log_level, use one of '${validLevels.join(', ')}'`);
|
||||
const level = z2mToWinstonLevel(z2mLevel);
|
||||
|
||||
const levelWithCompensatedLength: {[s: string]: string} = {
|
||||
'info': 'info ',
|
||||
'error': 'error',
|
||||
@@ -50,76 +22,113 @@ const levelWithCompensatedLength: {[s: string]: string} = {
|
||||
'debug': 'debug',
|
||||
};
|
||||
|
||||
const timestampFormat = (): string => moment().format(settings.get().advanced.timestamp_format);
|
||||
let logger: winston.Logger;
|
||||
let fileTransport : winston.transport;
|
||||
let output: string[];
|
||||
let directory: string;
|
||||
let logFilename: string;
|
||||
let transportsToUse: winston.transport[];
|
||||
|
||||
// Setup default console logger
|
||||
const transportsToUse: winston.transport[] = [
|
||||
new winston.transports.Console({
|
||||
function init(): void {
|
||||
// What transports to enable
|
||||
output = settings.get().advanced.log_output;
|
||||
|
||||
// Directory to log to
|
||||
const timestamp = moment(Date.now()).format('YYYY-MM-DD.HH-mm-ss');
|
||||
directory = settings.get().advanced.log_directory.replace('%TIMESTAMP%', timestamp);
|
||||
logFilename = settings.get().advanced.log_file.replace('%TIMESTAMP%', timestamp);
|
||||
|
||||
// Make sure that log directory exists when not logging to stdout only
|
||||
if (output.includes('file')) {
|
||||
fx.mkdirSync(directory);
|
||||
|
||||
if (settings.get().advanced.log_symlink_current) {
|
||||
const current = settings.get().advanced.log_directory.replace('%TIMESTAMP%', 'current');
|
||||
const actual = './' + timestamp;
|
||||
/* istanbul ignore next */
|
||||
if (fs.existsSync(current)) {
|
||||
fs.unlinkSync(current);
|
||||
}
|
||||
fs.symlinkSync(actual, current);
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the log level.
|
||||
const z2mLevel = settings.get().advanced.log_level;
|
||||
const validLevels = ['info', 'error', 'warn', 'debug'];
|
||||
assert(validLevels.includes(z2mLevel),
|
||||
`'${z2mLevel}' is not valid log_level, use one of '${validLevels.join(', ')}'`);
|
||||
const level = z2mToWinstonLevel(z2mLevel);
|
||||
|
||||
const timestampFormat = (): string => moment().format(settings.get().advanced.timestamp_format);
|
||||
|
||||
// Setup default console logger
|
||||
transportsToUse = [
|
||||
new winston.transports.Console({
|
||||
level,
|
||||
silent: !output.includes('console'),
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp({format: timestampFormat}),
|
||||
winston.format.printf(/* istanbul ignore next */(info) => {
|
||||
const {timestamp, level, message} = info;
|
||||
const l = winstonToZ2mLevel(level as WinstonLogLevel);
|
||||
|
||||
const plainPrefix = `Zigbee2MQTT:${levelWithCompensatedLength[l]}`;
|
||||
let prefix = plainPrefix;
|
||||
if (process.stdout.isTTY) {
|
||||
prefix = colorizer.colorize(l, plainPrefix);
|
||||
}
|
||||
return `${prefix} ${timestamp.split('.')[0]}: ${message}`;
|
||||
}),
|
||||
),
|
||||
}),
|
||||
];
|
||||
|
||||
// Add file logger when enabled
|
||||
// NOTE: the initiation of the logger, even when not added as transport tries to create the logging directory
|
||||
const transportFileOptions: KeyValue = {
|
||||
filename: path.join(directory, logFilename),
|
||||
json: false,
|
||||
level,
|
||||
silent: !output.includes('console'),
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp({format: timestampFormat}),
|
||||
winston.format.printf(/* istanbul ignore next */(info) => {
|
||||
const {timestamp, level, message} = info;
|
||||
const l = winstonToZ2mLevel(level as WinstonLogLevel);
|
||||
|
||||
const plainPrefix = `Zigbee2MQTT:${levelWithCompensatedLength[l]}`;
|
||||
let prefix = plainPrefix;
|
||||
if (process.stdout.isTTY) {
|
||||
prefix = colorizer.colorize(l, plainPrefix);
|
||||
}
|
||||
return `${prefix} ${timestamp.split('.')[0]}: ${message}`;
|
||||
return `${levelWithCompensatedLength[l]} ${timestamp.split('.')[0]}: ${message}`;
|
||||
}),
|
||||
),
|
||||
}),
|
||||
];
|
||||
|
||||
// Add file logger when enabled
|
||||
// NOTE: the initiation of the logger, even when not added as transport tries to create the logging directory
|
||||
const transportFileOptions: KeyValue = {
|
||||
filename: path.join(directory, logFilename),
|
||||
json: false,
|
||||
level,
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp({format: timestampFormat}),
|
||||
winston.format.printf(/* istanbul ignore next */(info) => {
|
||||
const {timestamp, level, message} = info;
|
||||
const l = winstonToZ2mLevel(level as WinstonLogLevel);
|
||||
return `${levelWithCompensatedLength[l]} ${timestamp.split('.')[0]}: ${message}`;
|
||||
}),
|
||||
),
|
||||
};
|
||||
|
||||
if (settings.get().advanced.log_rotation) {
|
||||
transportFileOptions.tailable = true;
|
||||
transportFileOptions.maxFiles = 3; // Keep last 3 files
|
||||
transportFileOptions.maxsize = 10000000; // 10MB
|
||||
}
|
||||
|
||||
let fileTransport : winston.transport;
|
||||
if (output.includes('file')) {
|
||||
fileTransport = new winston.transports.File(transportFileOptions);
|
||||
transportsToUse.push(fileTransport);
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
if (output.includes('syslog')) {
|
||||
// eslint-disable-next-line
|
||||
require('winston-syslog').Syslog;
|
||||
const options: KeyValue = {
|
||||
app_name: 'Zigbee2MQTT',
|
||||
format: winston.format.printf(/* istanbul ignore next */(info) => {
|
||||
return `${info.message}`;
|
||||
}),
|
||||
...settings.get().advanced.log_syslog,
|
||||
};
|
||||
if (options.hasOwnProperty('type')) options.type = options.type.toString();
|
||||
// @ts-ignore
|
||||
transportsToUse.push(new winston.transports.Syslog(options));
|
||||
}
|
||||
|
||||
// Create logger
|
||||
const logger = winston.createLogger({transports: transportsToUse, levels: winston.config.syslog.levels});
|
||||
if (settings.get().advanced.log_rotation) {
|
||||
transportFileOptions.tailable = true;
|
||||
transportFileOptions.maxFiles = 3; // Keep last 3 files
|
||||
transportFileOptions.maxsize = 10000000; // 10MB
|
||||
}
|
||||
|
||||
if (output.includes('file')) {
|
||||
fileTransport = new winston.transports.File(transportFileOptions);
|
||||
transportsToUse.push(fileTransport);
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
if (output.includes('syslog')) {
|
||||
// eslint-disable-next-line
|
||||
require('winston-syslog').Syslog;
|
||||
const options: KeyValue = {
|
||||
app_name: 'Zigbee2MQTT',
|
||||
format: winston.format.printf(/* istanbul ignore next */(info) => {
|
||||
return `${info.message}`;
|
||||
}),
|
||||
...settings.get().advanced.log_syslog,
|
||||
};
|
||||
if (options.hasOwnProperty('type')) options.type = options.type.toString();
|
||||
// @ts-ignore
|
||||
transportsToUse.push(new winston.transports.Syslog(options));
|
||||
}
|
||||
|
||||
logger = winston.createLogger({transports: transportsToUse, levels: winston.config.syslog.levels});
|
||||
}
|
||||
|
||||
// Cleanup any old log directory.
|
||||
function cleanup(): void {
|
||||
@@ -135,7 +144,7 @@ function cleanup(): void {
|
||||
directories = directories.slice(10, directories.length);
|
||||
directories.forEach((dir) => {
|
||||
logger.debug(`Removing old log directory '${dir.path}'`);
|
||||
rimraf.sync(dir.path);
|
||||
rimrafSync(dir.path);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -168,7 +177,7 @@ function setLevel(level: Z2MLogLevel): void {
|
||||
}
|
||||
|
||||
function warn(message: string): void {
|
||||
// winston.config.syslog.levels doesnt have warn, but is required for syslog.
|
||||
// winston.config.syslog.levels doesn't have warn, but is required for syslog.
|
||||
logger.warning(message);
|
||||
}
|
||||
|
||||
@@ -211,5 +220,6 @@ async function end(): Promise<void> {
|
||||
}
|
||||
|
||||
export default {
|
||||
logOutput, warn, warning, error, info, debug, setLevel, getLevel, cleanup, addTransport, end, winston: logger,
|
||||
init, logOutput, warn, warning, error, info, debug, setLevel, getLevel, cleanup, addTransport, end,
|
||||
winston: (): winston.Logger => logger,
|
||||
};
|
||||
|
||||
@@ -366,14 +366,15 @@
|
||||
"port": {
|
||||
"type": "number",
|
||||
"title": "Port",
|
||||
"description": "Frontend binding port",
|
||||
"description": "Frontend binding port. Ignored when using a unix domain socket",
|
||||
"default": 8080,
|
||||
"requiresRestart": true
|
||||
},
|
||||
"host": {
|
||||
"type": "string",
|
||||
"title": "Bind host",
|
||||
"description": "Frontend binding host",
|
||||
"description": "Frontend binding host. Binds to a unix socket when an absolute path is given instead.",
|
||||
"examples": ["127.0.0.1", "/run/zigbee2mqtt/zigbee2mqtt.sock"],
|
||||
"default": "0.0.0.0",
|
||||
"requiresRestart": true
|
||||
},
|
||||
@@ -388,6 +389,18 @@
|
||||
"title": "URL",
|
||||
"description": "URL on which the frontend can be reached, currently only used for the Home Assistant device configuration page",
|
||||
"requiresRestart": true
|
||||
},
|
||||
"ssl_cert" : {
|
||||
"type": ["string", "null"],
|
||||
"title": "Certificate file path",
|
||||
"description": "SSL Certificate file path for exposing HTTPS. The sibling property 'ssl_key' must be set for HTTPS to be activated.",
|
||||
"requiresRestart": true
|
||||
},
|
||||
"ssl_key" : {
|
||||
"type": ["string", "null"],
|
||||
"title": "key file path",
|
||||
"description": "SSL key file path for exposing HTTPS. The sibling property 'ssl_cert' must be set for HTTPS to be activated.",
|
||||
"requiresRestart": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -499,13 +512,13 @@
|
||||
"type": "number",
|
||||
"title": "Port",
|
||||
"description": "The port on the host that syslog is running on, defaults to syslogd's default port.",
|
||||
"default": 123
|
||||
"default": 514
|
||||
},
|
||||
"protocol": {
|
||||
"type": "string",
|
||||
"title": "Protocol",
|
||||
"description": "The network protocol to log over (e.g. tcp4, udp4, tls4, unix, unix-connect, etc).",
|
||||
"default": "tcp4",
|
||||
"default": "udp4",
|
||||
"examples": [
|
||||
"udp4",
|
||||
"tls4",
|
||||
@@ -782,6 +795,12 @@
|
||||
"title": "Retain",
|
||||
"description": "Retain MQTT messages of this device"
|
||||
},
|
||||
"disabled": {
|
||||
"type": "boolean",
|
||||
"title": "Disabled",
|
||||
"description": "Disables the device (excludes device from network scans, availability and group state updates)",
|
||||
"requiresRestart": true
|
||||
},
|
||||
"retention": {
|
||||
"type": "number",
|
||||
"title": "Retention",
|
||||
@@ -812,28 +831,48 @@
|
||||
"description": "Publish optimistic state after set",
|
||||
"default": true
|
||||
},
|
||||
"filtered_optimistic": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"examples": ["color_mode", "color_temp", "color"],
|
||||
"title": "Filtered optimistic attributes",
|
||||
"description": "Filter attributes from optimistic publish payload when calling /set. (This has no effect if optimistic is set to false)."
|
||||
},
|
||||
"filtered_attributes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"examples": ["temperature", "battery", "action"],
|
||||
"examples": ["^temperature$", "^battery$", "^action$"],
|
||||
"title": "Filtered publish attributes",
|
||||
"description": "Filter attributes from publish payload."
|
||||
"description": "Filter attributes with regex from published payload."
|
||||
},
|
||||
"filtered_cache": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"examples": ["^input_actions$"],
|
||||
"title": "Filtered attributes from cache",
|
||||
"description": "Filter attributes with regex from being added to the cache, this prevents the attribute from being in the published payload when the value didn't change."
|
||||
},
|
||||
"filtered_optimistic": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"examples": ["^color_(mode|temp)$", "color"],
|
||||
"title": "Filtered optimistic attributes",
|
||||
"description": "Filter attributes with regex from optimistic publish payload when calling /set. (This has no effect if optimistic is set to false)."
|
||||
},
|
||||
"icon": {
|
||||
"type": "string",
|
||||
"title": "Icon",
|
||||
"description": "The user-defined device icon for the frontend. It can be a link to an image (not a path to a file) or base64 encoded data URL like: image/svg+xml;base64,PHN2ZyB3aW....R0aD"
|
||||
"description": "The user-defined device icon for the frontend. It can be a full URL link to an image (e.g. https://SOME.SITE/MODEL123.jpg) (you cannot use a path to a local file) or base64 encoded data URL (e.g. image/svg+xml;base64,PHN2ZyB3aW....R0aD)"
|
||||
},
|
||||
"homeassistant": {
|
||||
"type": ["object", "null"],
|
||||
"title": "Home Assistant",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"title": "Home Assistant name",
|
||||
"description": "Name of the device in Home Assistant"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["friendly_name"]
|
||||
@@ -859,6 +898,14 @@
|
||||
"qos": {
|
||||
"type": "number"
|
||||
},
|
||||
"off_state": {
|
||||
"type": ["string"],
|
||||
"enum": ["all_members_off", "last_member_state"],
|
||||
"title": "Group off state",
|
||||
"default": "auto",
|
||||
"requiresRestart": true,
|
||||
"description": "Control when to publish state OFF for a group. 'all_members_off': only publish state OFF when all group memebers are in state OFF, 'last_member_state': publish state OFF whenever one of its members changes to OFF"
|
||||
},
|
||||
"filtered_attributes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
||||
+57
-25
@@ -3,7 +3,7 @@ import utils from './utils';
|
||||
import objectAssignDeep from 'object-assign-deep';
|
||||
import path from 'path';
|
||||
import yaml from './yaml';
|
||||
import Ajv from 'ajv';
|
||||
import Ajv, {ValidateFunction} from 'ajv';
|
||||
import schemaJson from './settings.schema.json';
|
||||
export let schema = schemaJson;
|
||||
// @ts-ignore
|
||||
@@ -31,7 +31,10 @@ const file = process.env.ZIGBEE2MQTT_CONFIG ?? data.joinPath('configuration.yaml
|
||||
const ajvSetting = new Ajv({allErrors: true}).addKeyword('requiresRestart').compile(schemaJson);
|
||||
const ajvRestartRequired = new Ajv({allErrors: true})
|
||||
.addKeyword({keyword: 'requiresRestart', validate: (s: unknown) => !s}).compile(schemaJson);
|
||||
|
||||
const ajvRestartRequiredDeviceOptions = new Ajv({allErrors: true})
|
||||
.addKeyword({keyword: 'requiresRestart', validate: (s: unknown) => !s}).compile(schemaJson.definitions.device);
|
||||
const ajvRestartRequiredGroupOptions = new Ajv({allErrors: true})
|
||||
.addKeyword({keyword: 'requiresRestart', validate: (s: unknown) => !s}).compile(schemaJson.definitions.group);
|
||||
const defaults: RecursivePartial<Settings> = {
|
||||
permit_join: false,
|
||||
external_converters: [],
|
||||
@@ -187,6 +190,20 @@ function loadSettingsWithDefaults(): void {
|
||||
_settingsWithDefaults.whitelist && _settingsWithDefaults.passlist.push(..._settingsWithDefaults.whitelist);
|
||||
}
|
||||
|
||||
function parseValueRef(text: string): {filename: string, key: string} | null {
|
||||
const match = /!(.*) (.*)/g.exec(text);
|
||||
if (match) {
|
||||
let filename = match[1];
|
||||
// This is mainly for backward compatibility.
|
||||
if (!filename.endsWith('.yaml') && !filename.endsWith('.yml')) {
|
||||
filename += '.yaml';
|
||||
}
|
||||
return {filename, key: match[2]};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function write(): void {
|
||||
const settings = getInternalSettings();
|
||||
const toWrite: KeyValue = objectAssignDeep({}, settings);
|
||||
@@ -194,17 +211,18 @@ function write(): void {
|
||||
// Read settings to check if we have to split devices/groups into separate file.
|
||||
const actual = yaml.read(file);
|
||||
|
||||
// In case the setting is defined in a separte file (e.g. !secret network_key) update it there.
|
||||
// In case the setting is defined in a separate file (e.g. !secret network_key) update it there.
|
||||
for (const path of [
|
||||
['mqtt', 'server'],
|
||||
['mqtt', 'user'],
|
||||
['mqtt', 'password'],
|
||||
['advanced', 'network_key'],
|
||||
['frontend', 'auth_token'],
|
||||
]) {
|
||||
if (actual[path[0]] && actual[path[0]][path[1]]) {
|
||||
const match = /!(.*) (.*)/g.exec(actual[path[0]][path[1]]);
|
||||
if (match) {
|
||||
yaml.updateIfChanged(data.joinPath(`${match[1]}.yaml`), match[2], toWrite[path[0]][path[1]]);
|
||||
const ref = parseValueRef(actual[path[0]][path[1]]);
|
||||
if (ref) {
|
||||
yaml.updateIfChanged(data.joinPath(ref.filename), ref.key, toWrite[path[0]][path[1]]);
|
||||
toWrite[path[0]][path[1]] = actual[path[0]][path[1]];
|
||||
}
|
||||
}
|
||||
@@ -212,7 +230,7 @@ function write(): void {
|
||||
|
||||
// Write devices/groups to separate file if required.
|
||||
const writeDevicesOrGroups = (type: 'devices' | 'groups'): void => {
|
||||
if (typeof actual[type] === 'string' || Array.isArray(actual[type])) {
|
||||
if (typeof actual[type] === 'string' || (Array.isArray(actual[type]) && actual[type].length > 0)) {
|
||||
const fileToWrite = Array.isArray(actual[type]) ? actual[type][0] : actual[type];
|
||||
const content = objectAssignDeep({}, settings[type]);
|
||||
|
||||
@@ -308,37 +326,42 @@ export function validate(): string[] {
|
||||
|
||||
function read(): Settings {
|
||||
const s = yaml.read(file) as Settings;
|
||||
applyEnvironmentVariables(s);
|
||||
|
||||
// Read !secret MQTT username and password if set
|
||||
// eslint-disable-next-line
|
||||
const interpetValue = (value: any): any => {
|
||||
const re = /!(.*) (.*)/g;
|
||||
const match = re.exec(value);
|
||||
if (match) {
|
||||
const file = data.joinPath(`${match[1]}.yaml`);
|
||||
const key = match[2];
|
||||
return yaml.read(file)[key];
|
||||
const interpretValue = (value: any): any => {
|
||||
const ref = parseValueRef(value);
|
||||
if (ref) {
|
||||
return yaml.read(data.joinPath(ref.filename))[ref.key];
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
if (s.mqtt?.user && s.mqtt?.password) {
|
||||
s.mqtt.user = interpetValue(s.mqtt.user);
|
||||
s.mqtt.password = interpetValue(s.mqtt.password);
|
||||
if (s.mqtt?.user) {
|
||||
s.mqtt.user = interpretValue(s.mqtt.user);
|
||||
}
|
||||
|
||||
if (s.mqtt?.password) {
|
||||
s.mqtt.password = interpretValue(s.mqtt.password);
|
||||
}
|
||||
|
||||
if (s.mqtt?.server) {
|
||||
s.mqtt.server = interpretValue(s.mqtt.server);
|
||||
}
|
||||
|
||||
if (s.advanced?.network_key) {
|
||||
s.advanced.network_key = interpetValue(s.advanced.network_key);
|
||||
s.advanced.network_key = interpretValue(s.advanced.network_key);
|
||||
}
|
||||
|
||||
if (s.frontend?.auth_token) {
|
||||
s.frontend.auth_token = interpetValue(s.frontend.auth_token);
|
||||
s.frontend.auth_token = interpretValue(s.frontend.auth_token);
|
||||
}
|
||||
|
||||
// Read devices/groups configuration from separate file if specified.
|
||||
const readDevicesOrGroups = (type: 'devices' | 'groups'): void => {
|
||||
if (typeof s[type] === 'string' || Array.isArray(s[type])) {
|
||||
if (typeof s[type] === 'string' || (Array.isArray(s[type]) && Array(s[type]).length > 0)) {
|
||||
/* eslint-disable-line */ // @ts-ignore
|
||||
const files: string[] = Array.isArray(s[type]) ? s[type] : [s[type]];
|
||||
s[type] = {};
|
||||
@@ -373,7 +396,11 @@ function applyEnvironmentVariables(settings: Partial<Settings>): void {
|
||||
}, settings);
|
||||
|
||||
if (type.indexOf('object') >= 0 || type.indexOf('array') >= 0) {
|
||||
setting[key] = JSON.parse(process.env[envVariableName]);
|
||||
try {
|
||||
setting[key] = JSON.parse(process.env[envVariableName]);
|
||||
} catch (error) {
|
||||
setting[key] = process.env[envVariableName];
|
||||
}
|
||||
} else if (type.indexOf('number') >= 0) {
|
||||
/* eslint-disable-line */ // @ts-ignore
|
||||
setting[key] = process.env[envVariableName] * 1;
|
||||
@@ -390,7 +417,7 @@ function applyEnvironmentVariables(settings: Partial<Settings>): void {
|
||||
|
||||
if (typeof obj[key] === 'object' && obj[key]) {
|
||||
const newPath = [...path];
|
||||
if (key !== 'properties') {
|
||||
if (key !== 'properties' && key !== 'oneOf' && !Number.isInteger(Number(key))) {
|
||||
newPath.push(key);
|
||||
}
|
||||
iterate(obj[key], newPath);
|
||||
@@ -404,7 +431,6 @@ function applyEnvironmentVariables(settings: Partial<Settings>): void {
|
||||
function getInternalSettings(): Partial<Settings> {
|
||||
if (!_settings) {
|
||||
_settings = read();
|
||||
applyEnvironmentVariables(_settings);
|
||||
}
|
||||
|
||||
return _settings;
|
||||
@@ -446,7 +472,7 @@ export function apply(newSettings: Record<string, unknown>): boolean {
|
||||
throw new Error(`${error.instancePath.substring(1)} ${error.message}`);
|
||||
}
|
||||
|
||||
getInternalSettings(); // Ensure _settings is intialized.
|
||||
getInternalSettings(); // Ensure _settings is initialized.
|
||||
/* eslint-disable-line */ // @ts-ignore
|
||||
_settings = objectAssignDeep.noMutate(_settings, newSettings);
|
||||
write();
|
||||
@@ -644,21 +670,27 @@ export function removeGroup(IDorName: string | number): void {
|
||||
write();
|
||||
}
|
||||
|
||||
export function changeEntityOptions(IDorName: string, newOptions: KeyValue): void {
|
||||
export function changeEntityOptions(IDorName: string, newOptions: KeyValue): boolean {
|
||||
const settings = getInternalSettings();
|
||||
delete newOptions.friendly_name;
|
||||
delete newOptions.devices;
|
||||
let validator: ValidateFunction;
|
||||
if (getDevice(IDorName)) {
|
||||
objectAssignDeep(settings.devices[getDevice(IDorName).ID], newOptions);
|
||||
utils.removeNullPropertiesFromObject(settings.devices[getDevice(IDorName).ID]);
|
||||
validator = ajvRestartRequiredDeviceOptions;
|
||||
} else if (getGroup(IDorName)) {
|
||||
objectAssignDeep(settings.groups[getGroup(IDorName).ID], newOptions);
|
||||
utils.removeNullPropertiesFromObject(settings.groups[getGroup(IDorName).ID]);
|
||||
validator = ajvRestartRequiredGroupOptions;
|
||||
} else {
|
||||
throw new Error(`Device or group '${IDorName}' does not exist`);
|
||||
}
|
||||
|
||||
write();
|
||||
validator(newOptions);
|
||||
const restartRequired = validator.errors && !!validator.errors.find((e) => e.keyword === 'requiresRestart');
|
||||
return restartRequired;
|
||||
}
|
||||
|
||||
export function changeFriendlyName(IDorName: string, newName: string): void {
|
||||
|
||||
+102
-4
@@ -4,6 +4,8 @@ import data from './data';
|
||||
import vm from 'vm';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import {detailedDiff} from 'deep-object-diff';
|
||||
import objectAssignDeep from 'object-assign-deep';
|
||||
|
||||
// construct a local ISO8601 string (instead of UTC-based)
|
||||
// Example:
|
||||
@@ -33,6 +35,8 @@ const endpointNames = [
|
||||
'ep1', 'ep2', 'row_1', 'row_2', 'row_3', 'row_4', 'relay', 'usb',
|
||||
'l1', 'l2', 'l3', 'l4', 'l5', 'l6', 'l7', 'l8',
|
||||
'l9', 'l10', 'l11', 'l12', 'l13', 'l14', 'l15', 'l16',
|
||||
'l17', 'l18', 'l19', 'l20', 'l21', 'l22', 'l23', 'l24',
|
||||
'th1', 'th2', 'th3', 'th4', 'th5', 'th6', 'th7', 'th8', 'th9', 'th10',
|
||||
'button_1', 'button_2', 'button_3', 'button_4', 'button_5',
|
||||
'button_6', 'button_7', 'button_8', 'button_9', 'button_10',
|
||||
'button_11', 'button_12', 'button_13', 'button_14', 'button_15',
|
||||
@@ -77,7 +81,7 @@ async function getZigbee2MQTTVersion(includeCommitHash=true): Promise<{commitHas
|
||||
}
|
||||
|
||||
async function getDependencyVersion(depend: string): Promise<{version: string}> {
|
||||
const packageJSON = await import(path.join(__dirname, '..', '..', 'node_modules', depend, 'package.json'));
|
||||
const packageJSON = await import(path.join(require.resolve(depend), '..', '..', 'package.json'));
|
||||
const version = packageJSON.version;
|
||||
return {version};
|
||||
}
|
||||
@@ -132,8 +136,8 @@ function parseJSON(value: string, fallback: string): KeyValue | string {
|
||||
}
|
||||
}
|
||||
|
||||
function loadModuleFromText(moduleCode: string): unknown {
|
||||
const moduleFakePath = path.join(__dirname, 'externally-loaded.js');
|
||||
function loadModuleFromText(moduleCode: string, name?: string): unknown {
|
||||
const moduleFakePath = path.join(__dirname, '..', '..', 'data', 'extension', name || 'externally-loaded.js');
|
||||
const sandbox = {
|
||||
require: require,
|
||||
module: {},
|
||||
@@ -210,6 +214,43 @@ function toSnakeCase(value: string | KeyValue): any {
|
||||
}
|
||||
}
|
||||
|
||||
function charRange(start: string, stop: string): number[] {
|
||||
const result = [];
|
||||
for (let idx=start.charCodeAt(0), end=stop.charCodeAt(0); idx <=end; ++idx) {
|
||||
result.push(idx);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const controlCharacters = [
|
||||
...charRange('\u0000', '\u001F'),
|
||||
...charRange('\u007f', '\u009F'),
|
||||
...charRange('\ufdd0', '\ufdef'),
|
||||
];
|
||||
|
||||
function containsControlCharacter(str: string): boolean {
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const ch = str.charCodeAt(i);
|
||||
if (controlCharacters.includes(ch) || [0xFFFE, 0xFFFF].includes(ch & 0xFFFF)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function getAllFiles(path_: string): string[] {
|
||||
const result = [];
|
||||
for (let item of fs.readdirSync(path_)) {
|
||||
item = path.join(path_, item);
|
||||
if (fs.lstatSync(item).isFile()) {
|
||||
result.push(item);
|
||||
} else {
|
||||
result.push(...getAllFiles(item));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function validateFriendlyName(name: string, throwFirstError=false): string[] {
|
||||
const errors = [];
|
||||
for (const endpointName of endpointNames) {
|
||||
@@ -220,7 +261,7 @@ function validateFriendlyName(name: string, throwFirstError=false): string[] {
|
||||
|
||||
if (name.length === 0) errors.push(`friendly_name must be at least 1 char long`);
|
||||
if (name.endsWith('/') || name.startsWith('/')) errors.push(`friendly_name is not allowed to end or start with /`);
|
||||
if (name.endsWith(String.fromCharCode(0))) errors.push(`friendly_name is not allowed to contain null char`);
|
||||
if (containsControlCharacter(name)) errors.push(`friendly_name is not allowed to contain control char`);
|
||||
if (endpointNames.includes(name)) errors.push(`Following friendly_name are not allowed: '${endpointNames}'`);
|
||||
if (name.match(/.*\/\d*$/)) errors.push(`Friendly name cannot end with a "/DIGIT" ('${name}')`);
|
||||
if (name.includes('#') || name.includes('+')) {
|
||||
@@ -258,6 +299,8 @@ function isAvailabilityEnabledForEntity(entity: Device | Group, settings: Settin
|
||||
const enabledGlobal = settings.advanced.availability_timeout || settings.availability;
|
||||
if (!enabledGlobal) return false;
|
||||
|
||||
if (entity.isDevice() && entity.options.disabled) return false;
|
||||
|
||||
const passlist = settings.advanced.availability_passlist.concat(settings.advanced.availability_whitelist);
|
||||
if (passlist.length > 0) {
|
||||
return passlist.includes(entity.name) || passlist.includes(entity.ieeeAddr);
|
||||
@@ -277,6 +320,14 @@ function isEndpoint(obj: unknown): obj is zh.Endpoint {
|
||||
return obj.constructor.name.toLowerCase() === 'endpoint';
|
||||
}
|
||||
|
||||
function flatten<Type>(arr: Type[][]): Type[] {
|
||||
return [].concat(...arr);
|
||||
}
|
||||
|
||||
function arrayUnique<Type>(arr: Type[]): Type[] {
|
||||
return [...new Set(arr)];
|
||||
}
|
||||
|
||||
function isZHGroup(obj: unknown): obj is zh.Group {
|
||||
return obj.constructor.name.toLowerCase() === 'group';
|
||||
}
|
||||
@@ -304,6 +355,52 @@ function publishLastSeen(data: eventdata.LastSeenChanged, settings: Settings, al
|
||||
}
|
||||
}
|
||||
|
||||
function filterProperties(filter: string[], data: KeyValue): void {
|
||||
if (filter) {
|
||||
for (const property of Object.keys(data)) {
|
||||
if (filter.find((p) => property.match(`^${p}$`))) {
|
||||
delete data[property];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clone(obj: KeyValue): KeyValue {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
function computeSettingsToChange(current: KeyValue, new_: KeyValue): KeyValue {
|
||||
const diff: KeyValue = detailedDiff(current, new_);
|
||||
|
||||
// Remove any settings that are in the deleted.diff but not in the passed options
|
||||
const cleanupDeleted = (options: KeyValue, deleted: KeyValue): void => {
|
||||
for (const key of Object.keys(deleted)) {
|
||||
if (!(key in options)) {
|
||||
delete deleted[key];
|
||||
} else if (!Array.isArray(options[key])) {
|
||||
cleanupDeleted(options[key], deleted[key]);
|
||||
}
|
||||
}
|
||||
};
|
||||
cleanupDeleted(new_, diff.deleted);
|
||||
|
||||
// objectAssignDeep requires object prototype which is missing from detailedDiff, therefore clone
|
||||
const newSettings = objectAssignDeep({}, clone(diff.added), clone(diff.updated), clone(diff.deleted));
|
||||
|
||||
// deep-object-diff converts arrays to objects, set original array back here
|
||||
const convertBackArray = (before: KeyValue, after: KeyValue): void => {
|
||||
for (const [key, afterValue] of Object.entries(after)) {
|
||||
const beforeValue = before[key];
|
||||
if (Array.isArray(beforeValue)) {
|
||||
after[key] = beforeValue;
|
||||
} else if (afterValue && typeof beforeValue === 'object') {
|
||||
convertBackArray(beforeValue, afterValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
convertBackArray(new_, newSettings);
|
||||
return newSettings;
|
||||
}
|
||||
|
||||
export default {
|
||||
endpointNames, capitalize, getZigbee2MQTTVersion, getDependencyVersion, formatDate, objectHasProperties,
|
||||
@@ -311,4 +408,5 @@ export default {
|
||||
getExternalConvertersDefinitions, removeNullPropertiesFromObject, toNetworkAddressHex, toSnakeCase,
|
||||
parseEntityID, isEndpoint, isZHGroup, hours, minutes, seconds, validateFriendlyName, sleep,
|
||||
sanitizeImageParameter, isAvailabilityEnabledForEntity, publishLastSeen, availabilityPayload,
|
||||
getAllFiles, filterProperties, flatten, arrayUnique, clone, computeSettingsToChange,
|
||||
};
|
||||
|
||||
+11
-2
@@ -9,6 +9,7 @@ import Device from './model/device';
|
||||
import Group from './model/group';
|
||||
import * as ZHEvents from 'zigbee-herdsman/dist/controller/events';
|
||||
import bind from 'bind-decorator';
|
||||
import {randomInt} from 'crypto';
|
||||
|
||||
export default class Zigbee {
|
||||
private herdsman: Controller;
|
||||
@@ -164,13 +165,13 @@ export default class Zigbee {
|
||||
}
|
||||
|
||||
private generateNetworkKey(): number[] {
|
||||
const key = Array.from({length: 16}, () => Math.floor(Math.random() * 255));
|
||||
const key = Array.from({length: 16}, () => randomInt(256));
|
||||
settings.set(['advanced', 'network_key'], key);
|
||||
return key;
|
||||
}
|
||||
|
||||
private generatePanID(): number {
|
||||
const panID = Math.floor(Math.random() * (0xFFFF - 2)) + 1;
|
||||
const panID = randomInt(1, 0xFFFF - 1);
|
||||
settings.set(['advanced', 'pan_id'], panID);
|
||||
return panID;
|
||||
}
|
||||
@@ -183,6 +184,10 @@ export default class Zigbee {
|
||||
return this.herdsman.isStopping();
|
||||
}
|
||||
|
||||
async backup(): Promise<void> {
|
||||
return this.herdsman.backup();
|
||||
}
|
||||
|
||||
async getNetworkParameters(): Promise<zh.NetworkParameters> {
|
||||
return this.herdsman.getNetworkParameters();
|
||||
}
|
||||
@@ -306,6 +311,10 @@ export default class Zigbee {
|
||||
return this.herdsman.touchlinkFactoryReset(ieeeAddr, channel);
|
||||
}
|
||||
|
||||
async addInstallCode(installCode: string): Promise<void> {
|
||||
await this.herdsman.addInstallCode(installCode);
|
||||
}
|
||||
|
||||
async touchlinkIdentify(ieeeAddr: string, channel: number): Promise<void> {
|
||||
await this.herdsman.touchlinkIdentify(ieeeAddr, channel);
|
||||
}
|
||||
|
||||
Generated
-22883
File diff suppressed because it is too large
Load Diff
Generated
+16839
File diff suppressed because it is too large
Load Diff
+31
-30
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "zigbee2mqtt",
|
||||
"version": "1.25.0",
|
||||
"version": "1.32.2",
|
||||
"description": "Zigbee to MQTT bridge using Zigbee-herdsman",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
@@ -8,7 +8,7 @@
|
||||
"url": "git+https://github.com/Koenkk/zigbee2mqtt.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^14 || ^15 || ^16"
|
||||
"node": "^14 || ^16 || ^18 || ^19 || ^20"
|
||||
},
|
||||
"keywords": [
|
||||
"xiaomi",
|
||||
@@ -35,56 +35,57 @@
|
||||
},
|
||||
"homepage": "https://koenkk.github.io/zigbee2mqtt",
|
||||
"dependencies": {
|
||||
"ajv": "^8.11.0",
|
||||
"ajv": "^8.12.0",
|
||||
"bind-decorator": "^1.0.11",
|
||||
"connect-gzip-static": "^2.1.1",
|
||||
"core-js": "^3.21.1",
|
||||
"connect-gzip-static": "2.1.1",
|
||||
"debounce": "^1.2.1",
|
||||
"deep-object-diff": "^1.1.7",
|
||||
"deep-object-diff": "^1.1.9",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"finalhandler": "^1.2.0",
|
||||
"git-last-commit": "^1.0.1",
|
||||
"humanize-duration": "^3.27.1",
|
||||
"humanize-duration": "^3.29.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"json-stable-stringify-without-jsonify": "^1.0.1",
|
||||
"jszip": "^3.10.1",
|
||||
"mkdir-recursive": "^0.4.0",
|
||||
"moment": "^2.29.1",
|
||||
"moment": "^2.29.4",
|
||||
"mqtt": "4.3.7",
|
||||
"object-assign-deep": "^0.4.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"semver": "^7.3.5",
|
||||
"rimraf": "^5.0.1",
|
||||
"semver": "^7.5.4",
|
||||
"source-map-support": "^0.5.21",
|
||||
"uri-js": "^4.4.1",
|
||||
"winston": "^3.6.0",
|
||||
"winston-syslog": "^2.5.0",
|
||||
"winston": "^3.10.0",
|
||||
"winston-syslog": "^2.7.0",
|
||||
"winston-transport": "^4.5.0",
|
||||
"ws": "^8.5.0",
|
||||
"zigbee-herdsman": "0.14.20",
|
||||
"zigbee-herdsman-converters": "14.0.467",
|
||||
"zigbee2mqtt-frontend": "0.6.80"
|
||||
"ws": "^8.13.0",
|
||||
"zigbee-herdsman": "0.17.2",
|
||||
"zigbee-herdsman-converters": "15.50.1",
|
||||
"zigbee2mqtt-frontend": "0.6.133"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.17.8",
|
||||
"@babel/plugin-proposal-decorators": "^7.17.8",
|
||||
"@babel/preset-env": "^7.16.11",
|
||||
"@babel/preset-typescript": "^7.16.7",
|
||||
"@babel/core": "^7.22.9",
|
||||
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
||||
"@babel/plugin-proposal-decorators": "^7.22.7",
|
||||
"@babel/preset-env": "^7.22.9",
|
||||
"@babel/preset-typescript": "^7.22.5",
|
||||
"@types/debounce": "^1.2.1",
|
||||
"@types/finalhandler": "^1.1.1",
|
||||
"@types/finalhandler": "^1.2.0",
|
||||
"@types/humanize-duration": "^3.27.1",
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/jest": "^29.5.3",
|
||||
"@types/js-yaml": "^4.0.5",
|
||||
"@types/object-assign-deep": "^0.4.0",
|
||||
"@types/rimraf": "^3.0.2",
|
||||
"@types/ws": "^8.5.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.16.0",
|
||||
"@typescript-eslint/parser": "^5.16.0",
|
||||
"babel-jest": "^27.5.1",
|
||||
"eslint": "^8.12.0",
|
||||
"@types/ws": "8.5.4",
|
||||
"@typescript-eslint/eslint-plugin": "^6.1.0",
|
||||
"@typescript-eslint/parser": "^6.1.0",
|
||||
"babel-jest": "^29.6.1",
|
||||
"eslint": "^8.45.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-plugin-jest": "^26.1.3",
|
||||
"jest": "^27.5.1",
|
||||
"eslint-plugin-jest": "^27.2.3",
|
||||
"jest": "^29.6.1",
|
||||
"tmp": "^0.2.1",
|
||||
"typescript": "^4.6.3"
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"jest": {
|
||||
"coverageThreshold": {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"packages": {
|
||||
".": {
|
||||
"changelog-path": "CHANGELOG.md",
|
||||
"release-type": "node",
|
||||
"bump-minor-pre-major": false,
|
||||
"bump-patch-for-minor-pre-major": false,
|
||||
"draft": false,
|
||||
"include-v-in-tag": false,
|
||||
"prerelease": false,
|
||||
"skip-github-release": true
|
||||
}
|
||||
},
|
||||
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json"
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/* eslint max-len: 0 */
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const process = require('process');
|
||||
const {execSync} = require('child_process');
|
||||
const zhc = require('zigbee-herdsman-converters');
|
||||
|
||||
const z2mTillVersion = process.argv[2];
|
||||
const zhcTillVersion = process.argv[3];
|
||||
const zhTillVersion = process.argv[4];
|
||||
|
||||
const changelogs = [
|
||||
{tillVersion: z2mTillVersion, project: 'koenkk/zigbee2mqtt',
|
||||
contents: fs.readFileSync(path.join(__dirname, '..', 'CHANGELOG.md'), 'utf-8').split('\n')},
|
||||
{tillVersion: zhcTillVersion, project: 'koenkk/zigbee-herdsman-converters',
|
||||
contents: fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'zigbee-herdsman-converters', 'CHANGELOG.md'), 'utf-8').split('\n')},
|
||||
{tillVersion: zhTillVersion, project: 'koenkk/zigbee-herdsman',
|
||||
contents: fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'zigbee-herdsman', 'CHANGELOG.md'), 'utf-8').split('\n')},
|
||||
];
|
||||
|
||||
const releaseRe = /## \[(.+)\]/;
|
||||
const changes = {features: [], fixes: [], detect: [], add: [], error: []};
|
||||
let context = null;
|
||||
const changeRe = [
|
||||
/^\* (\*\*(.+):\*\*)?(.+)\((\[#\d+\]\(.+\))\) \(\[.+\]\(https:.+\/(.+)\)\)$/,
|
||||
/^\* (\*\*(.+):\*\*)?(.+)(https:\/\/github\.com.+) \(\[.+\]\(https:.+\/(.+)\)\)$/,
|
||||
/^\* (\*\*(.+):\*\*)?(.+)() \(\[.+\]\(https:.+\/(.+)\)\)$/,
|
||||
];
|
||||
|
||||
let commitUserLookup = {};
|
||||
const commitUserFile = path.join(__dirname, 'commit-user-lookup.json');
|
||||
if (fs.existsSync(commitUserFile)) {
|
||||
commitUserLookup = JSON.parse(fs.readFileSync(commitUserFile, 'utf8'));
|
||||
}
|
||||
|
||||
for (const changelog of changelogs) {
|
||||
for (const line of changelog.contents) {
|
||||
const releaseMatch = line.match(releaseRe);
|
||||
const changeMatch = changeRe.map((re) => line.match(re)).find((e) => e);
|
||||
if (releaseMatch) {
|
||||
if (releaseMatch[1] === changelog.tillVersion) {
|
||||
break;
|
||||
}
|
||||
} else if (line === '### Features') {
|
||||
context = 'features';
|
||||
} else if (line === '### Bug Fixes') {
|
||||
context = 'fixes';
|
||||
} else if (line.startsWith('* **ignore:**')) {
|
||||
continue;
|
||||
} else if (changeMatch) {
|
||||
let localContext = changeMatch[2] ? changeMatch[2] : context;
|
||||
if (!changes[localContext]) throw new Error(`Unknown context: ${localContext}`);
|
||||
|
||||
const commitUserKey = `${changelog.project}-${changeMatch[5]} `;
|
||||
let user = commitUserKey in commitUserLookup ? commitUserLookup[commitUserKey] :
|
||||
execSync(`curl -s https://api.github.com/repos/${changelog.project}/commits/${changeMatch[5]} | jq -r '.author.login'`).toString().trim();
|
||||
if (user !== 'null') commitUserLookup[commitUserKey] = user;
|
||||
const messages = [];
|
||||
let message = changeMatch[3].trim();
|
||||
if (message.endsWith('.')) message = message.substring(0, message.length - 1);
|
||||
|
||||
const otherUser = message.match(/\[@(.+)\]\(https:\/\/github.com\/.+\)/) || message.match(/@(.+)/);
|
||||
if (otherUser) {
|
||||
user = otherUser[1];
|
||||
message = message.replace(otherUser[0], '');
|
||||
}
|
||||
|
||||
if (localContext === 'add') {
|
||||
for (const model of message.split(',')) {
|
||||
const definition = zhc.definitions.find((d) => d.model === model.trim());
|
||||
if (definition) {
|
||||
messages.push(`\`${definition.model}\` ${definition.vendor} ${definition.description}`);
|
||||
} else {
|
||||
changes['error'].push(`${line} (model '${model}' does not exist)`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
messages.push(message);
|
||||
}
|
||||
|
||||
let issue = changeMatch[4].trim();
|
||||
if (issue && !issue.startsWith('[#')) issue = `[#${issue.split('/').pop()}](${issue})`;
|
||||
if (!issue) {
|
||||
issue = '_NO_ISSUE_';
|
||||
localContext = 'error';
|
||||
}
|
||||
|
||||
messages.forEach((m) => changes[localContext].push(`- ${issue} ${m} (@${user})`));
|
||||
} else if (line === '# Changelog' || !line) {
|
||||
continue;
|
||||
} else {
|
||||
throw new Error(`Unmatched line: ${line}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result = '';
|
||||
const names = [
|
||||
['features', 'Improvements'],
|
||||
['fixes', 'Fixes'],
|
||||
['add', 'New supported devices'],
|
||||
['detect', 'Fixed device detections'],
|
||||
['error', 'Changelog generator error'],
|
||||
];
|
||||
for (const name of names) {
|
||||
result += `# ${name[1]}\n`;
|
||||
if (name[0] === 'add') {
|
||||
result += `This release adds support for ${changes['add'].length} devices: \n`;
|
||||
}
|
||||
changes[name[0]].forEach((e) => result += `${e}\n`);
|
||||
result += '\n';
|
||||
}
|
||||
|
||||
fs.writeFileSync(commitUserFile, JSON.stringify(commitUserLookup), 'utf-8');
|
||||
|
||||
console.log(result.trim());
|
||||
@@ -5,7 +5,8 @@ const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
|
||||
|
||||
|
||||
for (const line of text.split('\n')) {
|
||||
const model = zigbeeHerdsmanConverters.devices.find((d) => d.model === line);
|
||||
const [modelNumber, user] = line.split(',').map((t) => t.trim());
|
||||
const model = zigbeeHerdsmanConverters.devices.find((d) => d.model === modelNumber);
|
||||
if (!model) throw new Error(`${line} does not exist`);
|
||||
console.log(`- \`${line}\` ${model.vendor} ${model.description}`);
|
||||
console.log(`- \`${modelNumber}\` ${model.vendor} ${model.description} (${user})`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
const assert = require('assert');
|
||||
const vm = require('vm');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const filename = process.argv[2];
|
||||
const moduleCode = fs.readFileSync(filename);
|
||||
const moduleFakePath = path.join(__dirname, 'externally-loaded.js');
|
||||
const sandbox = {
|
||||
require: require,
|
||||
module: {},
|
||||
console,
|
||||
setTimeout,
|
||||
clearTimeout,
|
||||
setInterval,
|
||||
clearInterval,
|
||||
setImmediate,
|
||||
clearImmediate,
|
||||
};
|
||||
vm.runInNewContext(moduleCode, sandbox, moduleFakePath);
|
||||
const converter = sandbox.module.exports;
|
||||
assert(!converter.toZigbee.includes(undefined));
|
||||
assert(!converter.fromZigbee.includes(undefined));
|
||||
@@ -0,0 +1,31 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFazCCA1OgAwIBAgIUU70Q1tbGP071yeQ8EcHd/0rxtnwwDQYJKoZIhvcNAQEL
|
||||
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
|
||||
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMjEyMDMwMDA1MTBaFw0yMzAx
|
||||
MDIwMDA1MTBaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
|
||||
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggIiMA0GCSqGSIb3DQEB
|
||||
AQUAA4ICDwAwggIKAoICAQDR6bYQPgkfWlGQq19U2nMsAo0RZR7wSUuFS0MFpbQS
|
||||
2Rr5x/4FwwKhTMOlfr6H2Xo/5pQvGOlhFE69j1HxQRooWDAswnscMv/9pb+BUqoE
|
||||
7J29SEndIpmCaI0c3ejZsu4Nq8DxGod/RsQhJdbDU+hkw8qARJ0JjnTb+aN1KH8g
|
||||
J3KeSMTOL3guBVZQujO0Unbjv47yBIFwi0IihR18aoRaxinb94Aqf92HcjZN++WU
|
||||
gq7/2Qlm3tF5uJwuNtG/DmzReSblRcUgzPri7qIhb9z51gFNdNX6cTH3yNoF/KtT
|
||||
XwEArvvsAyMCYuC6w8KI7WNmPI6yxSksgHdcNPlUVZYquLD+z2kYAdIByiyHmIpm
|
||||
OH3jVN7lRJ6xITOGmyNktfRuSUi2GzECAOWIGpDD/5/gK1pacbOXpJWqVrTjUIAN
|
||||
BrQf496CbabVgwaQEvq1QJJbuMbpma21/IlS6NpZS7vm/q1ojNsbEB6PYkSsBTzP
|
||||
iZiiaomjhV2w5yIp5Zn8xyMFVKLb+SEDQBtZZ6CErmbgqp4B3U2b4k5LippLmZZK
|
||||
pSk7BOuMpsZK8FIGS4KlANFfGVs79y6N+Jxue2IO9pRcpcpbqOXty4662BsPKInw
|
||||
dKxKTr6TcTe0gEq1fpf1npYwcZ9t36LMJ7L51TbaaJ5sEsXe5QNOOOUgSwPqWmrE
|
||||
lwIDAQABo1MwUTAdBgNVHQ4EFgQUYD/x9cCuGJgLUPkYLIVa8uvseBkwHwYDVR0j
|
||||
BBgwFoAUYD/x9cCuGJgLUPkYLIVa8uvseBkwDwYDVR0TAQH/BAUwAwEB/zANBgkq
|
||||
hkiG9w0BAQsFAAOCAgEAI+gp+8qqO4anj8NHgjJvXhrETLPJIeLuBp/cjF9HA2IL
|
||||
oAd5rXWPnQ6qgh3q/HazILgIHwI0wSrhVtRlprgGIxNa59cD3RcZCtqFSWIqhAqZ
|
||||
JnAye3bnGU/KahazQX3oP+y564b/QZvwpJDKC1EoMv1wdpXND2NVn4HwaBc7Tu/d
|
||||
SX/Tr6k+e32K8mwE+00/yf2rLrmWmNjWtP0f7zRmH3j1+ovktk7UoVhjKXfhqMUE
|
||||
pWdhPZIXQJ65JNMGKdeFI2ULQY9IbXHeWQUgBCyhTciMvm2XZzopk7rxrh9NZopS
|
||||
pAlCrWBkOZrs6hfZrus9FOzwSkeIEY7NnhNk05kslERJxjkoFytp7mDd4mTKCVW0
|
||||
dKc7G1hZWYfO3oe0Zjxi0ragyReNKtyL1BqMbDKJmxHl91iPbdueWix1xxhNFakH
|
||||
l3eOzfa/DEixvzxy3hzCcvagNb5RsXFVMQXQTgnIW/5GcG5VVB2/o7KidPL3XtqR
|
||||
Hc/0ebQvloGXgqJbyTXG9EInkwB35wXDQuHgwt1PhwzP1yfp2BCvDsQWrR3IxnBT
|
||||
DO1o2RpsurgCgUpd0vcW17DePLNfprrhhU2uTPn2bNcWdT/rZmEImkNaA29Rku+3
|
||||
SZQJZWIx4s2FMdVdAv6ZauROStTY/WayudfcFfP+hYvJ5/ccQ9Gkb7vrGUssFRw=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,54 @@
|
||||
-----BEGIN ENCRYPTED PRIVATE KEY-----
|
||||
MIIJnDBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQIrNH7wPLdpbUCAggA
|
||||
MAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECN97bYsRcF6xBIIJSLVWpqTzdlNO
|
||||
5J7kOrNcdbbg+SSEYbtT+G9gtwtkI8KO+xzYQZ/fdqmXgNqf9AQmikwDWQkoS4gC
|
||||
qq6S+iKYn7Y8ZNZ/xu7gWW6L6uNZ3LTe/vkpenU6kmZrDysFTW5b7iH2VmMc/2h/
|
||||
J3o3unxeMLMVSaoDG3MG08qj53yTmetTkZZZc4fgf6vK9/4fTJhdXu6n+1COBLBI
|
||||
KbqbTRKnai3CNiYzz3UHyp9CGx1nX06beTJW8bYJUld9cJz+ozLNSV78oCkcUfwG
|
||||
yR7p/6+86cWor5SwYG1wEm6w2bXrIJILTUdT92gXC8fhc/B6Id8hcuLIHcvFQ/CP
|
||||
31y4Qwea3vY6b1TeRbqS4/fYOYajsiZi9ps0Meg55qMJj/UuNW2iLlR7PVsDXmPO
|
||||
33X6cpEVyIlNCJzRREotzSoGmcXjQ5Dgd1LkxZPb163NGkRoJT4GcKCtQxYIyIzy
|
||||
qyU0YF7I5LPKtNt1IPURHHerW42jM2jFlISxUixgl5Pj3WrmXeEmXq7htItn6X/q
|
||||
tVEVUf6srs3aos2Ohx7Ji9PVDNuMGGGKxleCM6fU9ax/gYuk5r6PMYGPsD26xuZ0
|
||||
A9WbFFFI6N1V+M3YsJJdxevZRV1NB3RymYGwFoZFDOKLb5DYJmPP3VVN/DsMNPkw
|
||||
qaue7rinxBdLi8J6Itusiscg5pCyYQaujXtkK/bi6c1HCld4RvWCDgeU52SWRdJ9
|
||||
2Np2629AeBvBfHP1/DsaUsyQPJAgDJk0HXoJyqstoALkRZ6ICoi1833LW/JGIWOv
|
||||
xnl3JZ2q12aqb2OWxiBUKspnt2y5C2KpQJdmuEL68lAKLg+fzLi/jhJG1bwzWo79
|
||||
I5G+QeV9+kI3vAIYHu331a+Yevs2p84sIumDGFSCSHAaWMmsrizJ4eKl5NnwZHS+
|
||||
xUvcW+ISRKNRm4DWAL/nlwBJWMhCz+nsTKYvE1JO/IH2P5xxtD6bTjJ3YkVAHkBZ
|
||||
wfApCBxdiQurEiabj06C4OCcNRkqJ/GwLrdxiwTCAt2hd7Dn+ttLo3Fi6iPRae8c
|
||||
ZtWsCWaFObZ1QBeu7lKm+ev/Ab2SHtp6QMfrArNsrdIxSH1+Sm4DgfHmTmte2Tse
|
||||
KFHy5Rwokst+Y18zjp3auJ3+NTTr9D1x2JCdQ6wkh66Pyd2FBLqRrMI1fHq9t9P+
|
||||
r4DQm9I6FbgALm9yJ2QxV4ytwFGsowAye3vF+hfO5OjAScrG5DrxASRXlnp59zpd
|
||||
+vMLk6rzcfiHzaJzYc/SOU9ubKN93aJmlL0Zgf1gg54oGnmPbEJeqQzeKVBtisro
|
||||
FrNUPEFx5QOaqjVipPOpZfpTb0oaUpBdp4RpeI7W3Gb/dsmsRj7ItxvM6QxFdd5e
|
||||
XfDZEYaUJRwSG3lHg9URWwSmfRWV81BhEuwvji4rNIEQ32VMsi2WWWPLOYYKGIw/
|
||||
yE56xWBw7K78XwVx/b5yoNHBNjVNyjpNZ7jXkx6zdOjbqEpXJP+WfgVkYwlhlvAq
|
||||
cCDbrF7jVWFqTV9tZNxlbaFlS8f17vld8DmmJ29Lo+WecsOo3M9n2Z6i79u7Xg2O
|
||||
ip0V2Textq8rrKOwC1TLCRoQLypENO/cwdaF5amVkoXYDlY0rQtxf65Xe9bnJLrM
|
||||
1G+tG8NZQu/bvW0mgrm0XHt2i6yfjn6sXEOY5VEC2+gRUriQ+GCETgzMe6ukb26/
|
||||
VT38Va10SIu+xXwf/GXTwLskOO0AyWLBopTpL5q6faM4o/USX+Xl98sQVNQurHMS
|
||||
xSQtpL6YPhQrmW7q5Q57OVWy70kEU8RpOJx6I7uyoUDz2g3bjmpIPpQX55vhofXK
|
||||
3Xn76Ce9TyiVol71mPtBT1tYzi3ytZh/EYm2Wq3lEhbMT7zjw3T1Ypb4+q12VdCy
|
||||
0Y9swKsQdhRCT6FwcCFSpCc2TzCCJCR6mChTe+z6rNm5BS2M6BaYcrX8PP82rZ8k
|
||||
L0l3ymSLqSJ3J6Q+fdKhRq8EbCq6ngi31Q/l7fPHkW5VAJyowPRayD7UM09n6ou+
|
||||
sg/kfQB8nh46uVxRRDpyUsrfhtIEQec2xrU17NmCr2VlAeOjWBn8lE/J1Gfn8WyV
|
||||
05mY1aDGcRk/x02+74UujyuozYMfIVDXHpxp8YteTnBfiKVrRq+Aa9zVX9bkyagp
|
||||
SJUTpF7lqMmiuvyZnG2xfLDfILR/zTRIRrm+ZIJLHeVQJtibOJ1llxE6kRvKx6zp
|
||||
La47YCHKH+rLDTlNgrE5K/JtQgOKc+7bGQ6SCszf8OZWi5B2+Z+Ht36c3xQk9IOW
|
||||
cjNpvjZxpiw6L1MhlQPE/wxz2JkOlKjJN0MmLscIyu15X+W/BvHwB8d11CRKUUiE
|
||||
pV7OUoW2Ol1IgFHrEY33IhxwtyKtAKfQCiORarnwk/GvcD3TqmnzUeQdscdww4BI
|
||||
wsO3c3aGr+wvU3P/8oFAoffD1KRihtoZ6CB347JcnaeEPlOS13a28JsJCOtjMxy9
|
||||
wBH6UgzTwo04lDAStST+aIT4TaU4dlWmsAEQTpE8sa4DjpFtzK/ll+Wg2sA/SYaA
|
||||
uAugsAOnTb1pkgk43GoYfc2fZCu1g9b/L3mZrE62W8HDfm67PWT65XMJziy/hcCy
|
||||
DbXMAWfohLdePKDG7J0QQI8xcTN9TT5q3ZCglI9Xf7PE/qrHROdRgF8c8zyvB9dG
|
||||
AVx96Z7akBoveqlFS44MnxIOGI0AzV8MrVbN51W6RHNyg/6UTL50/8zHmRWZj+iO
|
||||
tfqiW+CtSHacYPy+Tcuft30PWZZkQDGphbRxaBRB00Fb+Bo57epDw4JS+4wRru33
|
||||
2RHaVzQmnXwR6kP85x4uAR53UdgMgfK4cyylnFqGDlLFrHZfauFHg3kMIwqgVIVA
|
||||
uMRrWC9XXU11j6NnP9J6Y64uSrjsh6eZOrtJIwA16U2r3PMMbn1hIdADrKu1MsPt
|
||||
bkNsnPQUKJyX8DbRX45kje5ssUerbkxhvewvHQJlTyDxtgk/pGHOyKXoT+5vQEj9
|
||||
pwHOrXi+QWidzi2vfRoNQJ0wXJr39dDTn0hVdsgFo2PsTh98W9cocVQfJnCL4Djs
|
||||
Zlsp/BJOwlb2e3Q28E6pNyT+N63ZYEO9zLV/WmCB/eV2uMVoKibX3KszYkn0jAs0
|
||||
RBytgnB8YW3n9RyorShchCKJzdjzR7nw79/lSLgDFE6W6Xh3VaGM07hz0vYWfaui
|
||||
yvytkM9LPan2/GJM4zLFeA==
|
||||
-----END ENCRYPTED PRIVATE KEY-----
|
||||
@@ -179,6 +179,15 @@ describe('Availability', () => {
|
||||
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('Should not ping disabled devices', async () => {
|
||||
settings.set(['devices', devices.bulb_color.ieeeAddr, 'disabled'], true);
|
||||
await resetExtension();
|
||||
MQTT.publish.mockClear();
|
||||
|
||||
await advancedTime(utils.minutes(15));
|
||||
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('Should allow to change availability timeout via avaiability options', async () => {
|
||||
settings.set(['availability'], {active: {timeout: 30}});
|
||||
await resetExtension();
|
||||
|
||||
+2
-1
@@ -588,6 +588,7 @@ describe('Bind', () => {
|
||||
it('Should poll bounded Hue bulb when receiving message from scene controller', async () => {
|
||||
const remote = zigbeeHerdsman.devices.bj_scene_switch;
|
||||
const data = {"action": "recall_2_row_1"};
|
||||
zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read.mockImplementationOnce(() => {throw new Error('failed')});
|
||||
const payload = {data, cluster: 'genScenes', device: remote, endpoint: remote.getEndpoint(10), type: 'commandRecall', linkquality: 10, groupID: 0};
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
@@ -595,7 +596,7 @@ describe('Bind', () => {
|
||||
expect(debounce).toHaveBeenCalledTimes(3);
|
||||
expect(zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read).toHaveBeenCalledWith("genOnOff", ["onOff"]);
|
||||
expect(zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read).toHaveBeenCalledWith("genLevelCtrl", ["currentLevel"]);
|
||||
expect(zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read).toHaveBeenCalledWith("lightingColorCtrl", ["currentX", "currentY", "colorTemperature"]);
|
||||
expect(zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read).toHaveBeenCalledWith("lightingColorCtrl", ["currentX", "currentY", "colorTemperature"]);
|
||||
});
|
||||
|
||||
it('Should poll grouped Hue bulb when receiving message from TRADFRI remote', async () => {
|
||||
|
||||
+115
-7
File diff suppressed because one or more lines are too long
+14
-1
@@ -65,6 +65,7 @@ describe('Configure', () => {
|
||||
jest.useFakeTimers();
|
||||
controller = new Controller(jest.fn(), jest.fn());
|
||||
await controller.start();
|
||||
await jest.runOnlyPendingTimers();
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
@@ -72,8 +73,9 @@ describe('Configure', () => {
|
||||
data.writeDefaultConfiguration();
|
||||
settings.reRead();
|
||||
mocksClear.forEach((m) => m.mockClear());
|
||||
coordinatorEndpoint = zigbeeHerdsman.devices.coordinator.getEndpoint(1);
|
||||
coordinatorEndpoint = zigbeeHerdsman.devices.coordinator.getEndpoint(1);
|
||||
await resetExtension();
|
||||
await jest.runOnlyPendingTimers();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -100,6 +102,17 @@ describe('Configure', () => {
|
||||
expectBulbConfigured();
|
||||
});
|
||||
|
||||
it('Should not re-configure disabled devices', async () => {
|
||||
expectBulbConfigured();
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
await flushPromises();
|
||||
mockClear(device);
|
||||
settings.set(['devices', device.ieeeAddr, 'disabled'], true);
|
||||
zigbeeHerdsman.events.deviceJoined({device});
|
||||
await flushPromises();
|
||||
expectBulbNotConfigured();
|
||||
});
|
||||
|
||||
it('Should reconfigure reporting on reconfigure event', async () => {
|
||||
expectBulbConfigured();
|
||||
const device = controller.zigbee.resolveEntity(zigbeeHerdsman.devices.bulb);
|
||||
|
||||
+80
-9
@@ -120,6 +120,7 @@ describe('Controller', () => {
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({"state":"ON","brightness":50,"color_temp":370,"linkquality":99}), {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/remote", stringify({"brightness":255}), {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({"state":'ON'}), {"qos": 0, "retain":false}, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Start controller should not publish cached states when disabled', async () => {
|
||||
@@ -146,7 +147,6 @@ describe('Controller', () => {
|
||||
logger.error.mockClear();
|
||||
controller.mqtt.client.reconnecting = true;
|
||||
jest.advanceTimersByTime(11 * 1000);
|
||||
await flushPromises();
|
||||
expect(logger.error).toHaveBeenCalledWith("Not connected to MQTT server!");
|
||||
controller.mqtt.client.reconnecting = false;
|
||||
});
|
||||
@@ -201,9 +201,10 @@ describe('Controller', () => {
|
||||
});
|
||||
await controller.start();
|
||||
await flushPromises();
|
||||
expect(logger.error).toHaveBeenCalledWith('MQTT failed to connect: addr not found');
|
||||
expect(logger.error).toHaveBeenCalledWith('MQTT error: addr not found');
|
||||
expect(logger.error).toHaveBeenCalledWith('MQTT failed to connect, exiting...');
|
||||
expect(mockExit).toHaveBeenCalledTimes(1);
|
||||
expect(mockExit).toHaveBeenCalledWith(1);
|
||||
expect(mockExit).toHaveBeenCalledWith(1, false);
|
||||
});
|
||||
|
||||
it('Start controller with permit join true', async () => {
|
||||
@@ -220,13 +221,13 @@ describe('Controller', () => {
|
||||
expect(zigbeeHerdsman.setTransmitPower).toHaveBeenCalledWith(14);
|
||||
});
|
||||
|
||||
it('Start controller and stop', async () => {
|
||||
it('Start controller and stop with restart', async () => {
|
||||
await controller.start();
|
||||
await controller.stop();
|
||||
await controller.stop(true);
|
||||
expect(MQTT.end).toHaveBeenCalledTimes(1);
|
||||
expect(zigbeeHerdsman.stop).toHaveBeenCalledTimes(1);
|
||||
expect(mockExit).toHaveBeenCalledTimes(1);
|
||||
expect(mockExit).toHaveBeenCalledWith(0);
|
||||
expect(mockExit).toHaveBeenCalledWith(0, true);
|
||||
});
|
||||
|
||||
it('Start controller and stop', async () => {
|
||||
@@ -236,7 +237,7 @@ describe('Controller', () => {
|
||||
expect(MQTT.end).toHaveBeenCalledTimes(1);
|
||||
expect(zigbeeHerdsman.stop).toHaveBeenCalledTimes(1);
|
||||
expect(mockExit).toHaveBeenCalledTimes(1);
|
||||
expect(mockExit).toHaveBeenCalledWith(1);
|
||||
expect(mockExit).toHaveBeenCalledWith(1, false);
|
||||
});
|
||||
|
||||
it('Start controller adapter disconnects', async () => {
|
||||
@@ -247,7 +248,7 @@ describe('Controller', () => {
|
||||
expect(MQTT.end).toHaveBeenCalledTimes(1);
|
||||
expect(zigbeeHerdsman.stop).toHaveBeenCalledTimes(1);
|
||||
expect(mockExit).toHaveBeenCalledTimes(1);
|
||||
expect(mockExit).toHaveBeenCalledWith(1);
|
||||
expect(mockExit).toHaveBeenCalledWith(1, false);
|
||||
});
|
||||
|
||||
it('Handle mqtt message', async () => {
|
||||
@@ -489,6 +490,46 @@ describe('Controller', () => {
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({"state":"ON","brightness":200}), {"qos": 0, "retain": true}, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Publish entity state attribute_json output filtered cache', async () => {
|
||||
await controller.start();
|
||||
settings.set(['advanced', 'output'], 'attribute_and_json');
|
||||
settings.set(['devices', zigbeeHerdsman.devices.bulb.ieeeAddr, 'filtered_cache'], ['linkquality']);
|
||||
MQTT.publish.mockClear();
|
||||
|
||||
const device = controller.zigbee.resolveEntity('bulb');
|
||||
expect(controller.state.state[device.ieeeAddr]).toStrictEqual({"brightness":50,"color_temp":370,"linkquality":99,"state":"ON"});
|
||||
|
||||
await controller.publishEntityState(device, {state: 'ON', brightness: 200, color_temp: 370, linkquality: 87});
|
||||
await flushPromises();
|
||||
|
||||
expect(controller.state.state[device.ieeeAddr]).toStrictEqual({"brightness":200,"color_temp":370,"state":"ON"});
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(5);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/state", "ON", {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/brightness", "200", {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/linkquality", "87", {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({"state":"ON","brightness":200,"color_temp":370,"linkquality":87}), {"qos": 0, "retain": true}, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Publish entity state attribute_json output filtered cache (device_options)', async () => {
|
||||
await controller.start();
|
||||
settings.set(['advanced', 'output'], 'attribute_and_json');
|
||||
settings.set(['device_options', 'filtered_cache'], ['linkquality']);
|
||||
MQTT.publish.mockClear();
|
||||
|
||||
const device = controller.zigbee.resolveEntity('bulb');
|
||||
expect(controller.state.state[device.ieeeAddr]).toStrictEqual({"brightness":50,"color_temp":370,"linkquality":99,"state":"ON"});
|
||||
|
||||
await controller.publishEntityState(device, {state: 'ON', brightness: 200, color_temp: 370, linkquality: 87});
|
||||
await flushPromises();
|
||||
|
||||
expect(controller.state.state[device.ieeeAddr]).toStrictEqual({"brightness":200,"color_temp":370,"state":"ON"});
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(5);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/state", "ON", {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/brightness", "200", {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/linkquality", "87", {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({"state":"ON","brightness":200,"color_temp":370,"linkquality":87}), {"qos": 0, "retain": true}, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Publish entity state with device information', async () => {
|
||||
await controller.start();
|
||||
settings.set(['mqtt', 'include_device_information'], true);
|
||||
@@ -569,7 +610,7 @@ describe('Controller', () => {
|
||||
logger.error.mockClear();
|
||||
controller.state.file = "/";
|
||||
await controller.state.save();
|
||||
expect(logger.error).toHaveBeenCalledWith(`Failed to write state to '/' (EISDIR: illegal operation on a directory, open '/')`);
|
||||
expect(logger.error).toHaveBeenCalledWith(expect.stringMatching(/Failed to write state to \'\/\'/));
|
||||
});
|
||||
|
||||
it('Publish should not cache when set', async () => {
|
||||
@@ -604,6 +645,27 @@ describe('Controller', () => {
|
||||
expect(MQTT.connect).toHaveBeenCalledWith("mqtt://localhost", expected);
|
||||
});
|
||||
|
||||
it('Should republish retained messages on MQTT reconnect', async () => {
|
||||
await controller.start();
|
||||
MQTT.publish.mockClear();
|
||||
MQTT.events['connect']();
|
||||
await flushPromises();
|
||||
jest.runOnlyPendingTimers();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(13);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), { retain: true, qos: 0 }, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Should not republish retained messages on MQTT reconnect when retained message are sent', async () => {
|
||||
await controller.start();
|
||||
MQTT.publish.mockClear();
|
||||
MQTT.events['connect']();
|
||||
await flushPromises();
|
||||
await MQTT.events.message('zigbee2mqtt/bridge/info', 'dummy');
|
||||
jest.runOnlyPendingTimers();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/state', expect.any(String), { retain: true, qos: 0 }, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Should prevent any message being published with retain flag when force_disable_retain is set', async () => {
|
||||
settings.set(['mqtt', 'force_disable_retain'], true);
|
||||
await controller.mqtt.connect()
|
||||
@@ -656,4 +718,13 @@ describe('Controller', () => {
|
||||
await flushPromises();
|
||||
expect(logger.debug).toHaveBeenCalledWith(`Received Zigbee message from 'Coordinator', type 'attributeReport', cluster 'genBasic', data '{"modelId":null}' from endpoint 1, ignoring since it is from coordinator`);
|
||||
});
|
||||
|
||||
it('Should remove state of removed device when stopped', async () => {
|
||||
await controller.start();
|
||||
const device = controller.zigbee.resolveEntity('bulb');
|
||||
expect(controller.state.state[device.ieeeAddr]).toStrictEqual({"brightness":50,"color_temp":370,"linkquality":99,"state":"ON"});
|
||||
device.zh.isDeleted = true;
|
||||
await controller.stop();
|
||||
expect(controller.state.state[device.ieeeAddr]).toStrictEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ describe('Data', () => {
|
||||
const actual = data.getPath();
|
||||
expect(actual).toBe(expected);
|
||||
expect(data.joinPath('test')).toStrictEqual(path.join(expected, 'test'));
|
||||
expect(data.joinPath('/test')).toStrictEqual(path.resolve(expected, '/test'));
|
||||
delete process.env.ZIGBEE2MQTT_DATA;
|
||||
data.testingOnlyReload();
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ const logger = require('./stub/logger');
|
||||
const zigbeeHerdsman = require('./stub/zigbeeHerdsman');
|
||||
const MQTT = require('./stub/mqtt');
|
||||
const path = require('path');
|
||||
const rimraf = require('rimraf');
|
||||
const {rimrafSync} = require('rimraf');
|
||||
const settings = require('../lib/util/settings');
|
||||
const Controller = require('../lib/controller');
|
||||
const stringify = require('json-stable-stringify-without-jsonify');
|
||||
@@ -45,7 +45,7 @@ describe('User extensions', () => {
|
||||
});
|
||||
afterEach(() => {
|
||||
const extensionPath = path.join(data.mockDir, 'extension');
|
||||
rimraf.sync(extensionPath);
|
||||
rimrafSync(extensionPath);
|
||||
})
|
||||
|
||||
it('Load user extension', async () => {
|
||||
@@ -84,7 +84,12 @@ describe('User extensions', () => {
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/extension/save', stringify({"name": "foo.js", "code": extensionCode}));
|
||||
await flushPromises();
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/response/extension/save', stringify({"data":{},"error":"Unexpected identifier","status":"error"}), { retain: false, qos: 0 }, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/response/extension/save', expect.any(String), { retain: false, qos: 0 }, expect.any(Function));
|
||||
const payload = JSON.parse(MQTT.publish.mock.calls[0][1]);
|
||||
expect(payload).toEqual(
|
||||
expect.objectContaining({"data":{},"status":"error"})
|
||||
);
|
||||
expect(payload.error).toMatch("Unexpected identifier");
|
||||
});
|
||||
|
||||
it('Removes user extension', async () => {
|
||||
|
||||
+93
-5
@@ -1,5 +1,5 @@
|
||||
const data = require('./stub/data');
|
||||
require('./stub/logger');
|
||||
const logger = require('./stub/logger');
|
||||
require('./stub/zigbeeHerdsman');
|
||||
const MQTT = require('./stub/mqtt');
|
||||
const settings = require('../lib/util/settings');
|
||||
@@ -7,6 +7,7 @@ const Controller = require('../lib/controller');
|
||||
const stringify = require('json-stable-stringify-without-jsonify');
|
||||
const flushPromises = require('./lib/flushPromises');
|
||||
const zigbeeHerdsman = require('./stub/zigbeeHerdsman');
|
||||
const path = require("path");
|
||||
jest.spyOn(process, 'exit').mockImplementation(() => {});
|
||||
|
||||
const mockHTTP = {
|
||||
@@ -19,6 +20,16 @@ const mockHTTP = {
|
||||
events: {},
|
||||
};
|
||||
|
||||
const mockHTTPS = {
|
||||
implementation: {
|
||||
listen: jest.fn(),
|
||||
on: (event, handler) => {mockHTTPS.events[event] = handler},
|
||||
close: jest.fn().mockImplementation((cb) => cb()),
|
||||
},
|
||||
variables: {},
|
||||
events: {},
|
||||
};
|
||||
|
||||
const mockWSocket = {
|
||||
close: jest.fn(),
|
||||
};
|
||||
@@ -48,6 +59,14 @@ jest.mock('http', () => ({
|
||||
mockHTTP.variables.onRequest = onRequest;
|
||||
return mockHTTP.implementation;
|
||||
}),
|
||||
Agent: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('https', () => ({
|
||||
createServer: jest.fn().mockImplementation((onRequest) => {
|
||||
mockHTTPS.variables.onRequest = onRequest;
|
||||
return mockHTTPS.implementation;
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock("connect-gzip-static", () =>
|
||||
@@ -98,7 +117,6 @@ describe('Frontend', () => {
|
||||
await controller.start();
|
||||
expect(mockNodeStatic.variables.path).toBe("my/dummy/path");
|
||||
expect(mockHTTP.implementation.listen).toHaveBeenCalledWith(8081, "127.0.0.1");
|
||||
|
||||
const mockWSClient = {
|
||||
implementation: {
|
||||
terminate: jest.fn(),
|
||||
@@ -111,9 +129,75 @@ describe('Frontend', () => {
|
||||
expect(mockWSClient.implementation.terminate).toHaveBeenCalledTimes(1);
|
||||
expect(mockHTTP.implementation.close).toHaveBeenCalledTimes(1);
|
||||
expect(mockWS.implementation.close).toHaveBeenCalledTimes(1);
|
||||
mockWS.implementation.close.mockClear();
|
||||
mockHTTP.implementation.close.mockClear();
|
||||
mockHTTP.implementation.listen.mockClear();
|
||||
mockHTTPS.implementation.listen.mockClear();
|
||||
});
|
||||
|
||||
it('Websocket interaction', async () => {
|
||||
it('Start/stop unix socket', async () => {
|
||||
settings.set(['frontend'], {host: "/tmp/zigbee2mqtt.sock"});
|
||||
controller = new Controller(jest.fn(), jest.fn());
|
||||
await controller.start();
|
||||
expect(mockNodeStatic.variables.path).toBe("my/dummy/path");
|
||||
expect(mockHTTP.implementation.listen).toHaveBeenCalledWith("/tmp/zigbee2mqtt.sock");
|
||||
const mockWSClient = {
|
||||
implementation: {
|
||||
terminate: jest.fn(),
|
||||
send: jest.fn(),
|
||||
},
|
||||
events: {},
|
||||
};
|
||||
mockWS.implementation.clients.push(mockWSClient.implementation);
|
||||
await controller.stop();
|
||||
expect(mockWSClient.implementation.terminate).toHaveBeenCalledTimes(1);
|
||||
expect(mockHTTP.implementation.close).toHaveBeenCalledTimes(1);
|
||||
expect(mockWS.implementation.close).toHaveBeenCalledTimes(1);
|
||||
mockWS.implementation.close.mockClear();
|
||||
mockHTTP.implementation.close.mockClear();
|
||||
mockHTTP.implementation.listen.mockClear();
|
||||
mockHTTPS.implementation.listen.mockClear();
|
||||
});
|
||||
|
||||
|
||||
it('Start/stop HTTPS valid', async () => {
|
||||
settings.set(['frontend','ssl_cert'], path.join(__dirname,'assets','certs','dummy.crt'));
|
||||
settings.set(['frontend','ssl_key'], path.join(__dirname,'assets','certs','dummy.key'));
|
||||
controller = new Controller(jest.fn(), jest.fn());
|
||||
await controller.start();
|
||||
expect(mockHTTP.implementation.listen).not.toHaveBeenCalledWith(8081, "127.0.0.1");
|
||||
expect(mockHTTPS.implementation.listen).toHaveBeenCalledWith(8081, "127.0.0.1");
|
||||
await controller.stop();
|
||||
mockHTTP.implementation.listen.mockClear();
|
||||
mockHTTPS.implementation.listen.mockClear();
|
||||
});
|
||||
|
||||
it('Start/stop HTTPS invalid : missing config', async () => {
|
||||
settings.set(['frontend','ssl_cert'], path.join(__dirname,'assets','certs','dummy.crt'));
|
||||
controller = new Controller(jest.fn(), jest.fn());
|
||||
await controller.start();
|
||||
expect(mockHTTP.implementation.listen).toHaveBeenCalledWith(8081, "127.0.0.1");
|
||||
expect(mockHTTPS.implementation.listen).not.toHaveBeenCalledWith(8081, "127.0.0.1");
|
||||
await controller.stop();
|
||||
mockHTTP.implementation.listen.mockClear();
|
||||
mockHTTPS.implementation.listen.mockClear();
|
||||
|
||||
});
|
||||
|
||||
it('Start/stop HTTPS invalid : missing file', async () => {
|
||||
settings.set(['frontend','ssl_cert'], 'filesNotExists.crt');
|
||||
settings.set(['frontend','ssl_key'], path.join(__dirname,'assets','certs','dummy.key'));
|
||||
controller = new Controller(jest.fn(), jest.fn());
|
||||
await controller.start();
|
||||
expect(mockHTTP.implementation.listen).toHaveBeenCalledWith(8081, "127.0.0.1");
|
||||
expect(mockHTTPS.implementation.listen).not.toHaveBeenCalledWith(8081, "127.0.0.1");
|
||||
await controller.stop();
|
||||
mockHTTP.implementation.listen.mockClear();
|
||||
mockHTTPS.implementation.listen.mockClear();
|
||||
|
||||
});
|
||||
|
||||
it('onlythis Websocket interaction', async () => {
|
||||
controller = new Controller(jest.fn(), jest.fn());
|
||||
await controller.start();
|
||||
|
||||
@@ -140,7 +224,7 @@ describe('Frontend', () => {
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb_color',
|
||||
stringify({state: 'ON', linkquality: null, update_available: null, update: {state: null}}),
|
||||
stringify({state: 'ON', power_on_behavior:null, linkquality: null, update_available: null, update: {state: null, installed_version: -1, latest_version: -1}}),
|
||||
{ retain: false, qos: 0 },
|
||||
expect.any(Function)
|
||||
);
|
||||
@@ -149,9 +233,13 @@ describe('Frontend', () => {
|
||||
mockWSClient.events.message(null, false);
|
||||
await flushPromises();
|
||||
|
||||
// Error
|
||||
mockWSClient.events.error(new Error('This is an error'));
|
||||
expect(logger.error).toHaveBeenCalledWith('WebSocket error: This is an error');
|
||||
|
||||
// Received message on socket
|
||||
expect(mockWSClient.implementation.send).toHaveBeenCalledTimes(1);
|
||||
expect(mockWSClient.implementation.send).toHaveBeenCalledWith(stringify({topic: 'bulb_color', payload: {state: 'ON', linkquality: null, update_available: null, update: {state: null}}}));
|
||||
expect(mockWSClient.implementation.send).toHaveBeenCalledWith(stringify({topic: 'bulb_color', payload: {state: 'ON', power_on_behavior:null, linkquality: null, update_available: null, update: {state: null, installed_version: -1, latest_version: -1}}}));
|
||||
|
||||
// Shouldnt set when not ready
|
||||
mockWSClient.implementation.send.mockClear();
|
||||
|
||||
@@ -334,6 +334,22 @@ describe('Groups', () => {
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Should not publish state change when group changes state and device is disabled', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb_color;
|
||||
const endpoint = device.getEndpoint(1);
|
||||
const group = zigbeeHerdsman.groups.group_1;
|
||||
group.members.push(endpoint);
|
||||
settings.set(['devices', device.ieeeAddr, 'disabled'], true);
|
||||
settings.set(['groups'], {'1': {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]}});
|
||||
await resetExtension();
|
||||
|
||||
MQTT.publish.mockClear();
|
||||
await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'}));
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({"state":"ON"}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Should publish state change for group when members state change', async () => {
|
||||
// Created for https://github.com/Koenkk/zigbee2mqtt/issues/5725
|
||||
const device = zigbeeHerdsman.devices.bulb_color;
|
||||
@@ -469,6 +485,30 @@ describe('Groups', () => {
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({state:"OFF"}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Should publish state change off if any lights within are still on when changed via device when off_state: last_member_state is used', async () => {
|
||||
const device_1 = zigbeeHerdsman.devices.bulb_color;
|
||||
const device_2 = zigbeeHerdsman.devices.bulb;
|
||||
const endpoint_1 = device_1.getEndpoint(1);
|
||||
const endpoint_2 = device_2.getEndpoint(1);
|
||||
const group = zigbeeHerdsman.groups.group_1;
|
||||
group.members.push(endpoint_1);
|
||||
group.members.push(endpoint_2);
|
||||
settings.set(['groups'], {
|
||||
'1': {friendly_name: 'group_1', devices: [device_1.ieeeAddr, device_2.ieeeAddr], retain: false, off_state: 'last_member_state'}
|
||||
});
|
||||
await resetExtension();
|
||||
|
||||
await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'}));
|
||||
await flushPromises();
|
||||
MQTT.publish.mockClear();
|
||||
|
||||
await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'}));
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(2);
|
||||
expect(MQTT.publish).toHaveBeenNthCalledWith(1, "zigbee2mqtt/group_1", stringify({state:"OFF"}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenNthCalledWith(2, "zigbee2mqtt/bulb_color", stringify({state:"OFF"}), {"retain": false, qos: 0}, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Should not publish state change off if any lights within are still on when changed via shared group', async () => {
|
||||
const device_1 = zigbeeHerdsman.devices.bulb_color;
|
||||
const device_2 = zigbeeHerdsman.devices.bulb;
|
||||
|
||||
+180
-27
@@ -98,6 +98,19 @@ describe('HomeAssistant extension', () => {
|
||||
"xy",
|
||||
"color_temp"
|
||||
],
|
||||
"effect":true,
|
||||
"effect_list":[
|
||||
"blink",
|
||||
"breathe",
|
||||
"okay",
|
||||
"channel_change",
|
||||
"candle",
|
||||
"fireplace",
|
||||
"colorloop",
|
||||
"finish_effect",
|
||||
"stop_effect",
|
||||
"stop_hue_effect",
|
||||
],
|
||||
"unique_id":"9_light_zigbee2mqtt"
|
||||
};
|
||||
|
||||
@@ -188,7 +201,7 @@ describe('HomeAssistant extension', () => {
|
||||
|
||||
payload = {
|
||||
'unit_of_measurement': 'hPa',
|
||||
'device_class': 'pressure',
|
||||
'device_class': 'atmospheric_pressure',
|
||||
'state_class': 'measurement',
|
||||
'value_template': '{{ value_json.pressure }}',
|
||||
'state_topic': 'zigbee2mqtt/weather_sensor',
|
||||
@@ -437,7 +450,7 @@ describe('HomeAssistant extension', () => {
|
||||
|
||||
payload = {
|
||||
'unit_of_measurement': 'hPa',
|
||||
'device_class': 'pressure',
|
||||
'device_class': 'atmospheric_pressure',
|
||||
'state_class': 'measurement',
|
||||
'value_template': '{{ value_json.pressure }}',
|
||||
'state_topic': 'zigbee2mqtt/weather_sensor',
|
||||
@@ -752,9 +765,6 @@ describe('HomeAssistant extension', () => {
|
||||
"topic":"zigbee2mqtt/bridge/state"
|
||||
}
|
||||
],
|
||||
"away_mode_command_topic":"zigbee2mqtt/TS0601_thermostat/set/away_mode",
|
||||
"away_mode_state_template":"{{ value_json.away_mode }}",
|
||||
"away_mode_state_topic":"zigbee2mqtt/TS0601_thermostat",
|
||||
"current_temperature_template":"{{ value_json.local_temperature }}",
|
||||
"current_temperature_topic":"zigbee2mqtt/TS0601_thermostat",
|
||||
"device":{
|
||||
@@ -766,17 +776,18 @@ describe('HomeAssistant extension', () => {
|
||||
"name":"TS0601_thermostat",
|
||||
"sw_version":null
|
||||
},
|
||||
"hold_command_topic":"zigbee2mqtt/TS0601_thermostat/set/preset",
|
||||
"hold_modes":[
|
||||
"preset_mode_command_topic":"zigbee2mqtt/TS0601_thermostat/set/preset",
|
||||
"preset_modes":[
|
||||
"schedule",
|
||||
"manual",
|
||||
"boost",
|
||||
"complex",
|
||||
"comfort",
|
||||
"eco"
|
||||
"eco",
|
||||
"away"
|
||||
],
|
||||
"hold_state_template":"{{ value_json.preset }}",
|
||||
"hold_state_topic":"zigbee2mqtt/TS0601_thermostat",
|
||||
"preset_mode_value_template":"{{ value_json.preset }}",
|
||||
"preset_mode_state_topic":"zigbee2mqtt/TS0601_thermostat",
|
||||
"json_attributes_topic":"zigbee2mqtt/TS0601_thermostat",
|
||||
"max_temp":"35",
|
||||
"min_temp":"5",
|
||||
@@ -814,6 +825,11 @@ describe('HomeAssistant extension', () => {
|
||||
set_position_topic: 'zigbee2mqtt/smart vent/set',
|
||||
set_position_template: '{ "position": {{ position }} }',
|
||||
position_template: '{{ value_json.position }}',
|
||||
state_topic: 'zigbee2mqtt/smart vent',
|
||||
value_template: `{{ value_json.state }}`,
|
||||
state_open: 'OPEN',
|
||||
state_closed: 'CLOSE',
|
||||
state_stopped: 'STOP',
|
||||
json_attributes_topic: 'zigbee2mqtt/smart vent',
|
||||
name: 'smart vent',
|
||||
unique_id: '0x0017880104e45551_cover_zigbee2mqtt',
|
||||
@@ -834,6 +850,40 @@ describe('HomeAssistant extension', () => {
|
||||
{ retain: true, qos: 0 },
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
payload = {
|
||||
"availability": [{ "topic": "zigbee2mqtt/bridge/state" }],
|
||||
"command_topic": "zigbee2mqtt/zigfred_plus/l6/set",
|
||||
"device": {
|
||||
"identifiers": ["zigbee2mqtt_0xf4ce368a38be56a1"],
|
||||
"manufacturer": "Siglis",
|
||||
"model": "zigfred plus smart in-wall switch (ZFP-1A-CH)",
|
||||
"name": "zigfred_plus",
|
||||
"sw_version": null
|
||||
},
|
||||
"json_attributes_topic": "zigbee2mqtt/zigfred_plus/l6",
|
||||
"name": "zigfred_plus_l6",
|
||||
"position_template": "{{ value_json.position }}",
|
||||
"position_topic": "zigbee2mqtt/zigfred_plus/l6",
|
||||
"set_position_template": "{ \"position_l6\": {{ position }} }",
|
||||
"set_position_topic": "zigbee2mqtt/zigfred_plus/l6/set",
|
||||
"state_stopped": 'STOP',
|
||||
"state_closed": "CLOSE",
|
||||
"state_open": "OPEN",
|
||||
"state_topic": "zigbee2mqtt/zigfred_plus/l6",
|
||||
"tilt_command_topic": "zigbee2mqtt/zigfred_plus/l6/set/tilt",
|
||||
"tilt_status_template": "{{ value_json.tilt }}",
|
||||
"tilt_status_topic": "zigbee2mqtt/zigfred_plus/l6",
|
||||
"unique_id": "0xf4ce368a38be56a1_cover_l6_zigbee2mqtt",
|
||||
"value_template": "{{ value_json.state }}"
|
||||
}
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'homeassistant/cover/0xf4ce368a38be56a1/cover_l6/config',
|
||||
stringify(payload),
|
||||
{ retain: true, qos: 0 },
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('Should discover devices with custom homeassistant_discovery_topic', async () => {
|
||||
@@ -912,7 +962,7 @@ describe('HomeAssistant extension', () => {
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb_color',
|
||||
stringify({"color":{"hue": 0, "saturation": 100, "h": 0, "s": 100}, "color_mode": "hs", "linkquality": null, "state": null, "update_available": null, "update": {"state": null}}),
|
||||
stringify({"color":{"hue": 0, "saturation": 100, "h": 0, "s": 100}, "color_mode": "hs", "linkquality": null, "state": null, "update_available": null, "power_on_behavior":null, "update": {"state": null, "installed_version": -1, "latest_version": -1}}),
|
||||
{ retain: false, qos: 0 },
|
||||
expect.any(Function),
|
||||
);
|
||||
@@ -928,7 +978,7 @@ describe('HomeAssistant extension', () => {
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb_color',
|
||||
stringify({"color": {"x": 0.4576,"y": 0.41}, "color_mode": "xy", "linkquality": null,"state": null, "update_available": null, "update": {"state": null}}),
|
||||
stringify({"color": {"x": 0.4576,"y": 0.41}, "color_mode": "xy", "linkquality": null,"state": null, "update_available": null, "power_on_behavior":null, "update": {"state": null, "installed_version": -1, "latest_version": -1}}),
|
||||
{ retain: false, qos: 0 },
|
||||
expect.any(Function),
|
||||
);
|
||||
@@ -944,7 +994,7 @@ describe('HomeAssistant extension', () => {
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb_color',
|
||||
stringify({"linkquality": null,"state": "ON", "update_available": null, "update": {"state": null}}),
|
||||
stringify({"linkquality": null,"state": "ON", "update_available": null, "power_on_behavior": null, "update": {"state": null, "installed_version": -1, "latest_version": -1}}),
|
||||
{ retain: false, qos: 0 },
|
||||
expect.any(Function),
|
||||
);
|
||||
@@ -1032,16 +1082,22 @@ describe('HomeAssistant extension', () => {
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb',
|
||||
stringify({"state":"ON","brightness":50,"color_temp":370,"linkquality":99,"power_on_behavior":null, "update_available": null, "update": {"state": null}}),
|
||||
stringify({"state":"ON","brightness":50,"color_temp":370,"linkquality":99,"power_on_behavior":null, "update_available": null, "update": {"state": null, "installed_version": -1, "latest_version": -1}}),
|
||||
{ retain: true, qos: 0 },
|
||||
expect.any(Function)
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/remote',
|
||||
stringify({"action":null,"action_duration":null,"battery":null,"brightness":255,"linkquality":null, "update_available": null, "update": {"state": null}}),
|
||||
stringify({"action":null,"action_duration":null,"battery":null,"brightness":255,"linkquality":null, "update_available": null, "update": {"state": null, "installed_version": -1, "latest_version": -1}}),
|
||||
{ retain: true, qos: 0 },
|
||||
expect.any(Function)
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/group_1',
|
||||
stringify({"state":'ON'}),
|
||||
{ retain: false, qos: 0 },
|
||||
expect.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it('Should send all status when home assistant comes online', async () => {
|
||||
@@ -1056,13 +1112,13 @@ describe('HomeAssistant extension', () => {
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb',
|
||||
stringify({"state":"ON","brightness":50,"color_temp":370,"linkquality":99,"power_on_behavior":null, "update_available": null, "update": {"state": null}}),
|
||||
stringify({"state":"ON","brightness":50,"color_temp":370,"linkquality":99,"power_on_behavior":null, "update_available": null, "update": {"state": null, "installed_version": -1, "latest_version": -1}}),
|
||||
{ retain: true, qos: 0 },
|
||||
expect.any(Function)
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/remote',
|
||||
stringify({"action":null,"action_duration":null,"battery":null,"brightness":255,"linkquality":null, "update_available": null, "update": {"state": null}}),
|
||||
stringify({"action":null,"action_duration":null,"battery":null,"brightness":255,"linkquality":null, "update_available": null, "update": {"state": null, "installed_version": -1, "latest_version": -1}}),
|
||||
{ retain: true, qos: 0 },
|
||||
expect.any(Function)
|
||||
);
|
||||
@@ -1178,6 +1234,8 @@ describe('HomeAssistant extension', () => {
|
||||
MQTT.publish.mockClear();
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/rename', stringify({"from": "weather_sensor", "to": "weather_sensor_renamed","homeassistant_rename":true}));
|
||||
await flushPromises();
|
||||
jest.runOnlyPendingTimers();
|
||||
await flushPromises();
|
||||
|
||||
const payload = {
|
||||
'unit_of_measurement': '°C',
|
||||
@@ -1240,6 +1298,8 @@ describe('HomeAssistant extension', () => {
|
||||
MQTT.publish.mockClear();
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/group/rename', stringify({"from": "ha_discovery_group", "to": "ha_discovery_group_new","homeassistant_rename":true}));
|
||||
await flushPromises();
|
||||
jest.runOnlyPendingTimers();
|
||||
await flushPromises();
|
||||
|
||||
const payload = {
|
||||
"availability":[{"topic":"zigbee2mqtt/bridge/state"}],
|
||||
@@ -1262,6 +1322,19 @@ describe('HomeAssistant extension', () => {
|
||||
"xy",
|
||||
"color_temp"
|
||||
],
|
||||
"effect":true,
|
||||
"effect_list":[
|
||||
"blink",
|
||||
"breathe",
|
||||
"okay",
|
||||
"channel_change",
|
||||
"candle",
|
||||
"fireplace",
|
||||
"colorloop",
|
||||
"finish_effect",
|
||||
"stop_effect",
|
||||
"stop_hue_effect",
|
||||
],
|
||||
"unique_id":"9_light_zigbee2mqtt"
|
||||
};
|
||||
|
||||
@@ -1325,7 +1398,7 @@ describe('HomeAssistant extension', () => {
|
||||
"payload_on":true,
|
||||
"payload_off":false,
|
||||
"value_template":`{{ value_json['update']['state'] == "available" }}`,
|
||||
"enabled_by_default": true,
|
||||
"enabled_by_default": false,
|
||||
"state_topic":"zigbee2mqtt/bulb",
|
||||
"json_attributes_topic":"zigbee2mqtt/bulb",
|
||||
"name":"bulb update available",
|
||||
@@ -1354,7 +1427,7 @@ describe('HomeAssistant extension', () => {
|
||||
|
||||
it('Should discover trigger when click is published', async () => {
|
||||
const discovered = MQTT.publish.mock.calls.filter((c) => c[0].includes('0x0017880104e45520')).map((c) => c[0]);
|
||||
expect(discovered.length).toBe(5);
|
||||
expect(discovered.length).toBe(7);
|
||||
expect(discovered).toContain('homeassistant/sensor/0x0017880104e45520/click/config');
|
||||
expect(discovered).toContain('homeassistant/sensor/0x0017880104e45520/action/config');
|
||||
|
||||
@@ -1429,21 +1502,21 @@ describe('HomeAssistant extension', () => {
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/button',
|
||||
stringify({action: "single", click: "single", battery: null, linkquality: null, voltage: null}),
|
||||
stringify({action: "single", click: "single", battery: null, linkquality: null, voltage: null, power_outage_count: null, device_temperature: null}),
|
||||
{ retain: false, qos: 0 },
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/button',
|
||||
stringify({action: "", battery: null, linkquality: null, voltage: null, click: null}),
|
||||
stringify({action: "", battery: null, linkquality: null, voltage: null, click: null, power_outage_count: null, device_temperature: null}),
|
||||
{ retain: false, qos: 0 },
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/button',
|
||||
stringify({click: "", action: null, battery: null, linkquality: null, voltage: null}),
|
||||
stringify({click: "", action: null, battery: null, linkquality: null, voltage: null, power_outage_count: null, device_temperature: null}),
|
||||
{ retain: false, qos: 0 },
|
||||
expect.any(Function),
|
||||
);
|
||||
@@ -1530,7 +1603,7 @@ describe('HomeAssistant extension', () => {
|
||||
await resetExtension();
|
||||
|
||||
const discovered = MQTT.publish.mock.calls.filter((c) => c[0].includes('0x0017880104e45520')).map((c) => c[0]);
|
||||
expect(discovered.length).toBe(4);
|
||||
expect(discovered.length).toBe(6);
|
||||
expect(discovered).toContain('homeassistant/sensor/0x0017880104e45520/action/config');
|
||||
expect(discovered).toContain('homeassistant/sensor/0x0017880104e45520/battery/config');
|
||||
expect(discovered).toContain('homeassistant/sensor/0x0017880104e45520/linkquality/config');
|
||||
@@ -1541,7 +1614,7 @@ describe('HomeAssistant extension', () => {
|
||||
await resetExtension();
|
||||
|
||||
const discovered = MQTT.publish.mock.calls.filter((c) => c[0].includes('0x0017880104e45520')).map((c) => c[0]);
|
||||
expect(discovered.length).toBe(3);
|
||||
expect(discovered.length).toBe(5);
|
||||
expect(discovered).not.toContain('homeassistant/sensor/0x0017880104e45520/click/config');
|
||||
expect(discovered).not.toContain('homeassistant/sensor/0x0017880104e45520/action/config');
|
||||
|
||||
@@ -1586,7 +1659,7 @@ describe('HomeAssistant extension', () => {
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/button',
|
||||
stringify({action: "single", "battery":null,"linkquality":null,"voltage":null}),
|
||||
stringify({action: "single", "battery":null,"linkquality":null,"voltage":null, "power_outage_count": null, "device_temperature": null}),
|
||||
{ retain: false, qos: 0 },
|
||||
expect.any(Function),
|
||||
);
|
||||
@@ -1623,10 +1696,10 @@ describe('HomeAssistant extension', () => {
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(4);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({action: 'single', click: null, battery: null, linkquality: null, voltage: null});
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({action: 'single', click: null, battery: null, linkquality: null, voltage: null, power_outage_count: null, device_temperature: null});
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
|
||||
expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/button');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({action: '', click: null, battery: null, linkquality: null, voltage: null});
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({action: '', click: null, battery: null, linkquality: null, voltage: null, power_outage_count: null, device_temperature: null});
|
||||
expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({"qos": 0, "retain": false});
|
||||
expect(MQTT.publish.mock.calls[2][0]).toStrictEqual('homeassistant/device_automation/0x0017880104e45520/action_single/config');
|
||||
expect(MQTT.publish.mock.calls[3][0]).toStrictEqual('zigbee2mqtt/button/action');
|
||||
@@ -1778,6 +1851,19 @@ describe('HomeAssistant extension', () => {
|
||||
"xy",
|
||||
"color_temp"
|
||||
],
|
||||
"effect":true,
|
||||
"effect_list":[
|
||||
"blink",
|
||||
"breathe",
|
||||
"okay",
|
||||
"channel_change",
|
||||
"candle",
|
||||
"fireplace",
|
||||
"colorloop",
|
||||
"finish_effect",
|
||||
"stop_effect",
|
||||
"stop_hue_effect",
|
||||
],
|
||||
"unique_id":"9_light_zigbee2mqtt"
|
||||
};
|
||||
|
||||
@@ -1814,6 +1900,19 @@ describe('HomeAssistant extension', () => {
|
||||
"xy",
|
||||
"color_temp"
|
||||
],
|
||||
"effect":true,
|
||||
"effect_list":[
|
||||
"blink",
|
||||
"breathe",
|
||||
"okay",
|
||||
"channel_change",
|
||||
"candle",
|
||||
"fireplace",
|
||||
"colorloop",
|
||||
"finish_effect",
|
||||
"stop_effect",
|
||||
"stop_hue_effect",
|
||||
],
|
||||
"unique_id":"9_light_zigbee2mqtt"
|
||||
};
|
||||
|
||||
@@ -1825,6 +1924,60 @@ describe('HomeAssistant extension', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('Should discover with availability offline when device is disabled', async () => {
|
||||
settings.set(['devices', '0x000b57fffec6a5b2', 'disabled'], true);
|
||||
|
||||
await resetExtension();
|
||||
|
||||
const payload = {
|
||||
"availability":[
|
||||
{
|
||||
"topic":"zigbee2mqtt/bridge/state",
|
||||
"value_template": `{{ "offline" }}`,
|
||||
}
|
||||
],
|
||||
"brightness":true,
|
||||
"brightness_scale":254,
|
||||
"color_mode":true,
|
||||
"command_topic":"zigbee2mqtt/bulb/set",
|
||||
"device":{
|
||||
"identifiers":[
|
||||
"zigbee2mqtt_0x000b57fffec6a5b2"
|
||||
],
|
||||
"manufacturer":"IKEA",
|
||||
"model":"TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white (LED1545G12)",
|
||||
"name":"bulb",
|
||||
"sw_version":null
|
||||
},
|
||||
"effect":true,
|
||||
"effect_list":[
|
||||
"blink",
|
||||
"breathe",
|
||||
"okay",
|
||||
"channel_change",
|
||||
"finish_effect",
|
||||
"stop_effect"
|
||||
],
|
||||
"json_attributes_topic":"zigbee2mqtt/bulb",
|
||||
"max_mireds":454,
|
||||
"min_mireds":250,
|
||||
"name":"bulb",
|
||||
"schema":"json",
|
||||
"state_topic":"zigbee2mqtt/bulb",
|
||||
"supported_color_modes":[
|
||||
"color_temp"
|
||||
],
|
||||
"unique_id":"0x000b57fffec6a5b2_light_zigbee2mqtt"
|
||||
};
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'homeassistant/light/0x000b57fffec6a5b2/light/config',
|
||||
stringify(payload),
|
||||
{ retain: true, qos: 0 },
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('Should discover last_seen when enabled', async () => {
|
||||
settings.set(['advanced', 'last_seen'], 'ISO_8601');
|
||||
await resetExtension();
|
||||
|
||||
+28
-15
@@ -5,7 +5,7 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const data = require('./stub/data');
|
||||
let stdOutWriteOriginal;
|
||||
const rimraf = require('rimraf');
|
||||
const {rimrafSync} = require('rimraf');
|
||||
const Transport = require('winston-transport');
|
||||
|
||||
describe('Logger', () => {
|
||||
@@ -25,6 +25,7 @@ describe('Logger', () => {
|
||||
|
||||
it('Create log directory', () => {
|
||||
const logger = require('../lib/util/logger').default;
|
||||
logger.init();
|
||||
logger.logOutput();
|
||||
const dirs = fs.readdirSync(dir.name);
|
||||
expect(dirs.length).toBe(1);
|
||||
@@ -32,10 +33,11 @@ describe('Logger', () => {
|
||||
|
||||
it('Should cleanup', () => {
|
||||
const logger = require('../lib/util/logger').default;
|
||||
logger.init();
|
||||
logger.logOutput();
|
||||
|
||||
for (const d of fs.readdirSync(dir.name)) {
|
||||
rimraf.sync(path.join(dir.name, d));
|
||||
rimrafSync(path.join(dir.name, d));
|
||||
}
|
||||
|
||||
for (let i = 0; i < 21; i++) {
|
||||
@@ -49,6 +51,7 @@ describe('Logger', () => {
|
||||
|
||||
it('Should not cleanup when there is no timestamp set', () => {
|
||||
const logger = require('../lib/util/logger').default;
|
||||
logger.init();
|
||||
logger.logOutput();
|
||||
for (let i = 30; i < 40; i++) {
|
||||
fs.mkdirSync(path.join(dir.name, `log_${i}`));
|
||||
@@ -62,6 +65,7 @@ describe('Logger', () => {
|
||||
|
||||
it('Set and get log level', () => {
|
||||
const logger = require('../lib/util/logger').default;
|
||||
logger.init();
|
||||
logger.logOutput();
|
||||
logger.setLevel('debug');
|
||||
expect(logger.getLevel()).toBe('debug');
|
||||
@@ -74,23 +78,26 @@ describe('Logger', () => {
|
||||
}
|
||||
|
||||
const logger = require('../lib/util/logger').default;
|
||||
expect(logger.winston.transports.length).toBe(2);
|
||||
logger.init();
|
||||
expect(logger.winston().transports.length).toBe(2);
|
||||
logger.addTransport(new DummyTransport());
|
||||
expect(logger.winston.transports.length).toBe(3);
|
||||
expect(logger.winston().transports.length).toBe(3);
|
||||
});
|
||||
|
||||
it('Set and get log level warn <-> warning', () => {
|
||||
const logger = require('../lib/util/logger').default;
|
||||
logger.init();
|
||||
logger.logOutput();
|
||||
logger.setLevel('warn');
|
||||
expect(logger.winston.transports[0].level).toBe('warning');
|
||||
expect(logger.winston().transports[0].level).toBe('warning');
|
||||
expect(logger.getLevel()).toBe('warn');
|
||||
});
|
||||
|
||||
it('Logger should be console and file by default', () => {
|
||||
const logger = require('../lib/util/logger').default;
|
||||
logger.init();
|
||||
logger.logOutput();
|
||||
const pipes = logger.winston._readableState.pipes;
|
||||
const pipes = logger.winston()._readableState.pipes;
|
||||
expect(pipes.length).toBe(2);
|
||||
expect(pipes[0].constructor.name).toBe('Console');
|
||||
expect(pipes[0].silent).toBe(false);
|
||||
@@ -101,8 +108,9 @@ describe('Logger', () => {
|
||||
it('Logger can be file only', () => {
|
||||
settings.set(['advanced', 'log_output'], ['file']);
|
||||
const logger = require('../lib/util/logger').default;
|
||||
logger.init();
|
||||
logger.logOutput();
|
||||
const pipes = logger.winston._readableState.pipes;
|
||||
const pipes = logger.winston()._readableState.pipes;
|
||||
expect(pipes.length).toBe(2);
|
||||
expect(pipes[0].constructor.name).toBe('Console');
|
||||
expect(pipes[0].silent).toBe(true);
|
||||
@@ -113,8 +121,9 @@ describe('Logger', () => {
|
||||
it('Logger can be console only', () => {
|
||||
settings.set(['advanced', 'log_output'], ['console']);
|
||||
const logger = require('../lib/util/logger').default;
|
||||
logger.init();
|
||||
logger.logOutput();
|
||||
const pipes = logger.winston._readableState.pipes;
|
||||
const pipes = logger.winston()._readableState.pipes;
|
||||
expect(pipes.constructor.name).toBe('Console');
|
||||
expect(pipes.silent).toBe(false);
|
||||
});
|
||||
@@ -122,8 +131,9 @@ describe('Logger', () => {
|
||||
it('Logger can be nothing', () => {
|
||||
settings.set(['advanced', 'log_output'], []);
|
||||
const logger = require('../lib/util/logger').default;
|
||||
logger.init();
|
||||
logger.logOutput();
|
||||
const pipes = logger.winston._readableState.pipes;
|
||||
const pipes = logger.winston()._readableState.pipes;
|
||||
expect(pipes.constructor.name).toBe('Console');
|
||||
expect(pipes.silent).toBe(true);
|
||||
});
|
||||
@@ -131,8 +141,9 @@ describe('Logger', () => {
|
||||
it('Should allow to disable log rotation', () => {
|
||||
settings.set(['advanced', 'log_rotation'], false);
|
||||
const logger = require('../lib/util/logger').default;
|
||||
logger.init();
|
||||
logger.logOutput();
|
||||
const pipes = logger.winston._readableState.pipes;
|
||||
const pipes = logger.winston()._readableState.pipes;
|
||||
expect(pipes[1].constructor.name).toBe('File');
|
||||
expect(pipes[1].maxFiles).toBeNull();
|
||||
expect(pipes[1].tailable).toBeFalsy();
|
||||
@@ -142,6 +153,7 @@ describe('Logger', () => {
|
||||
it('Should allow to symlink logs to current directory', () => {
|
||||
settings.set(['advanced', 'log_symlink_current'], true);
|
||||
let logger = require('../lib/util/logger').default;
|
||||
logger.init();
|
||||
logger.logOutput();
|
||||
expect(fs.readdirSync(dir.name).includes('current')).toBeTruthy()
|
||||
|
||||
@@ -151,23 +163,24 @@ describe('Logger', () => {
|
||||
|
||||
it('Log', () => {
|
||||
const logger = require('../lib/util/logger').default;
|
||||
const warn = jest.spyOn(logger.winston, 'warning');
|
||||
logger.init();
|
||||
const warn = jest.spyOn(logger.winston(), 'warning');
|
||||
logger.warn('warn');
|
||||
expect(warn).toHaveBeenCalledWith('warn');
|
||||
|
||||
const debug = jest.spyOn(logger.winston, 'debug');
|
||||
const debug = jest.spyOn(logger.winston(), 'debug');
|
||||
logger.debug('debug');
|
||||
expect(debug).toHaveBeenCalledWith('debug');
|
||||
|
||||
const warning = jest.spyOn(logger.winston, 'warning');
|
||||
const warning = jest.spyOn(logger.winston(), 'warning');
|
||||
logger.warning('warning');
|
||||
expect(warning).toHaveBeenCalledWith('warning');
|
||||
|
||||
const info = jest.spyOn(logger.winston, 'info');
|
||||
const info = jest.spyOn(logger.winston(), 'info');
|
||||
logger.info('info');
|
||||
expect(info).toHaveBeenCalledWith('info');
|
||||
|
||||
const error = jest.spyOn(logger.winston, 'error');
|
||||
const error = jest.spyOn(logger.winston(), 'error');
|
||||
logger.error('error');
|
||||
expect(error).toHaveBeenCalledWith('error');
|
||||
});
|
||||
|
||||
+17
-2
@@ -102,7 +102,7 @@ describe('Networkmap', () => {
|
||||
let call = MQTT.publish.mock.calls[0];
|
||||
expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/networkmap/raw');
|
||||
|
||||
const expected = {"links":[{"depth":1,"linkquality":120,"lqi":120,"relationship":2,"routes":[],"source":{"ieeeAddr":"0x000b57fffec6a5b3","networkAddress":40399},"sourceIeeeAddr":"0x000b57fffec6a5b3","sourceNwkAddr":40399,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":1,"linkquality":92,"lqi":92,"relationship":2,"routes":[{"destinationAddress":6540,"nextHop":40369,"status":"ACTIVE"}],"source":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"sourceIeeeAddr":"0x000b57fffec6a5b2","sourceNwkAddr":40369,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":1,"linkquality":92,"lqi":92,"relationship":2,"routes":[],"source":{"ieeeAddr":"0x0017880104e45511","networkAddress":1114},"sourceIeeeAddr":"0x0017880104e45511","sourceNwkAddr":1114,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":2,"linkquality":110,"lqi":110,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x000b57fffec6a5b3","networkAddress":40399},"sourceIeeeAddr":"0x000b57fffec6a5b3","sourceNwkAddr":40399,"target":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"targetIeeeAddr":"0x000b57fffec6a5b2"},{"depth":2,"linkquality":100,"lqi":100,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x0017880104e45559","networkAddress":6540},"sourceIeeeAddr":"0x0017880104e45559","sourceNwkAddr":6540,"target":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"targetIeeeAddr":"0x000b57fffec6a5b2"},{"depth":2,"linkquality":130,"lqi":130,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x0017880104e45521","networkAddress":6538},"sourceIeeeAddr":"0x0017880104e45521","sourceNwkAddr":6538,"target":{"ieeeAddr":"0x0017880104e45559","networkAddress":6540},"targetIeeeAddr":"0x0017880104e45559"}],"nodes":[{"definition":null,"failed":[],"friendlyName":"Coordinator","ieeeAddr":"0x00124b00120144ae","lastSeen":1000,"modelID":null,"networkAddress":0,"type":"Coordinator"},{"definition":{"description":"TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white","model":"LED1545G12","supports":"light (state, brightness, color_temp, color_temp_startup), effect, power_on_behavior, linkquality","vendor":"IKEA"},"failed":[],"friendlyName":"bulb","ieeeAddr":"0x000b57fffec6a5b2","lastSeen":1000,"modelID":"TRADFRI bulb E27 WS opal 980lm","networkAddress":40369,"type":"Router"},{"definition":{"description":"Hue Go","model":"7146060PH","supports":"light (state, brightness, color_temp, color_temp_startup, color_xy, color_hs), effect, linkquality","vendor":"Philips"},"failed":[],"friendlyName":"bulb_color","ieeeAddr":"0x000b57fffec6a5b3","lastSeen":1000,"modelID":"LLC020","networkAddress":40399,"type":"Router"},{"definition":{"description":"Aqara double key wireless wall switch (2016 model)","model":"WXKG02LM_rev1","supports":"battery, action, voltage, linkquality","vendor":"Xiaomi"},"friendlyName":"button_double_key","ieeeAddr":"0x0017880104e45521","lastSeen":1000,"modelID":"lumi.sensor_86sw2.es1","networkAddress":6538,"type":"EndDevice"},{"definition":null,"failed":["lqi","routingTable"],"friendlyName":"0x0017880104e45525","ieeeAddr":"0x0017880104e45525","lastSeen":1000,"manufacturerName":"Boef","modelID":"notSupportedModelID","networkAddress":6536,"type":"Router"},{"definition":{"description":"[CC2530 router](http://ptvo.info/cc2530-based-zigbee-coordinator-and-router-112/)","model":"CC2530.ROUTER","supports":"led, linkquality","vendor":"Custom devices (DiY)"},"failed":[],"friendlyName":"cc2530_router","ieeeAddr":"0x0017880104e45559","lastSeen":1000,"modelID":"lumi.router","networkAddress":6540,"type":"Router"},{"definition":{"description": "external","model":"external_converter_device","supports":"linkquality","vendor":"external"},"friendlyName":"0x0017880104e45511","ieeeAddr":"0x0017880104e45511","lastSeen":1000,"modelID":"external_converter_device","networkAddress":1114,"type":"EndDevice"}]};
|
||||
const expected = {"links":[{"depth":1,"linkquality":120,"lqi":120,"relationship":2,"routes":[],"source":{"ieeeAddr":"0x000b57fffec6a5b3","networkAddress":40399},"sourceIeeeAddr":"0x000b57fffec6a5b3","sourceNwkAddr":40399,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":1,"linkquality":92,"lqi":92,"relationship":2,"routes":[{"destinationAddress":6540,"nextHop":40369,"status":"ACTIVE"}],"source":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"sourceIeeeAddr":"0x000b57fffec6a5b2","sourceNwkAddr":40369,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":1,"linkquality":92,"lqi":92,"relationship":2,"routes":[],"source":{"ieeeAddr":"0x0017880104e45511","networkAddress":1114},"sourceIeeeAddr":"0x0017880104e45511","sourceNwkAddr":1114,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":2,"linkquality":110,"lqi":110,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x000b57fffec6a5b3","networkAddress":40399},"sourceIeeeAddr":"0x000b57fffec6a5b3","sourceNwkAddr":40399,"target":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"targetIeeeAddr":"0x000b57fffec6a5b2"},{"depth":2,"linkquality":100,"lqi":100,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x0017880104e45559","networkAddress":6540},"sourceIeeeAddr":"0x0017880104e45559","sourceNwkAddr":6540,"target":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"targetIeeeAddr":"0x000b57fffec6a5b2"},{"depth":2,"linkquality":130,"lqi":130,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x0017880104e45521","networkAddress":6538},"sourceIeeeAddr":"0x0017880104e45521","sourceNwkAddr":6538,"target":{"ieeeAddr":"0x0017880104e45559","networkAddress":6540},"targetIeeeAddr":"0x0017880104e45559"}],"nodes":[{"definition":null,"failed":[],"friendlyName":"Coordinator","ieeeAddr":"0x00124b00120144ae","lastSeen":1000,"modelID":null,"networkAddress":0,"type":"Coordinator"},{"definition":{"description":"TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white","model":"LED1545G12","supports":"light (state, brightness, color_temp, color_temp_startup), effect, power_on_behavior, linkquality","vendor":"IKEA"},"failed":[],"friendlyName":"bulb","ieeeAddr":"0x000b57fffec6a5b2","lastSeen":1000,"modelID":"TRADFRI bulb E27 WS opal 980lm","networkAddress":40369,"type":"Router"},{"definition":{"description":"Hue Go","model":"7146060PH","supports":"light (state, brightness, color_temp, color_temp_startup, color_xy, color_hs), power_on_behavior, effect, linkquality","vendor":"Philips"},"failed":[],"friendlyName":"bulb_color","ieeeAddr":"0x000b57fffec6a5b3","lastSeen":1000,"modelID":"LLC020","networkAddress":40399,"type":"Router"},{"definition":{"description":"Aqara double key wireless wall switch (2016 model)","model":"WXKG02LM_rev1","supports":"battery, action, voltage, power_outage_count, linkquality","vendor":"Xiaomi"},"friendlyName":"button_double_key","ieeeAddr":"0x0017880104e45521","lastSeen":1000,"modelID":"lumi.sensor_86sw2.es1","networkAddress":6538,"type":"EndDevice"},{"definition":null,"failed":["lqi","routingTable"],"friendlyName":"0x0017880104e45525","ieeeAddr":"0x0017880104e45525","lastSeen":1000,"manufacturerName":"Boef","modelID":"notSupportedModelID","networkAddress":6536,"type":"Router"},{"definition":{"description":"[CC2530 router](http://ptvo.info/cc2530-based-zigbee-coordinator-and-router-112/)","model":"CC2530.ROUTER","supports":"led, linkquality","vendor":"Custom devices (DiY)"},"failed":[],"friendlyName":"cc2530_router","ieeeAddr":"0x0017880104e45559","lastSeen":1000,"modelID":"lumi.router","networkAddress":6540,"type":"Router"},{"definition":{"description": "external","model":"external_converter_device","supports":"linkquality","vendor":"external"},"friendlyName":"0x0017880104e45511","ieeeAddr":"0x0017880104e45511","lastSeen":1000,"modelID":"external_converter_device","networkAddress":1114,"type":"EndDevice"}]};
|
||||
expect(JSON.parse(call[1])).toStrictEqual(expected);
|
||||
|
||||
/**
|
||||
@@ -276,7 +276,7 @@ describe('Networkmap', () => {
|
||||
let call = MQTT.publish.mock.calls[0];
|
||||
expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap');
|
||||
|
||||
const expected = {"data":{"routes":true,"type":"raw","value":{"links":[{"depth":1,"linkquality":120,"lqi":120,"relationship":2,"routes":[],"source":{"ieeeAddr":"0x000b57fffec6a5b3","networkAddress":40399},"sourceIeeeAddr":"0x000b57fffec6a5b3","sourceNwkAddr":40399,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":1,"linkquality":92,"lqi":92,"relationship":2,"routes":[{"destinationAddress":6540,"nextHop":40369,"status":"ACTIVE"}],"source":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"sourceIeeeAddr":"0x000b57fffec6a5b2","sourceNwkAddr":40369,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":1,"linkquality":92,"lqi":92,"relationship":2,"routes":[],"source":{"ieeeAddr":"0x0017880104e45511","networkAddress":1114},"sourceIeeeAddr":"0x0017880104e45511","sourceNwkAddr":1114,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":2,"linkquality":110,"lqi":110,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x000b57fffec6a5b3","networkAddress":40399},"sourceIeeeAddr":"0x000b57fffec6a5b3","sourceNwkAddr":40399,"target":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"targetIeeeAddr":"0x000b57fffec6a5b2"},{"depth":2,"linkquality":100,"lqi":100,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x0017880104e45559","networkAddress":6540},"sourceIeeeAddr":"0x0017880104e45559","sourceNwkAddr":6540,"target":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"targetIeeeAddr":"0x000b57fffec6a5b2"},{"depth":2,"linkquality":130,"lqi":130,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x0017880104e45521","networkAddress":6538},"sourceIeeeAddr":"0x0017880104e45521","sourceNwkAddr":6538,"target":{"ieeeAddr":"0x0017880104e45559","networkAddress":6540},"targetIeeeAddr":"0x0017880104e45559"}],"nodes":[{"definition":null,"failed":[],"friendlyName":"Coordinator","ieeeAddr":"0x00124b00120144ae","lastSeen":1000,"modelID":null,"networkAddress":0,"type":"Coordinator"},{"definition":{"description":"TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white","model":"LED1545G12","supports":"light (state, brightness, color_temp, color_temp_startup), effect, power_on_behavior, linkquality","vendor":"IKEA"},"failed":[],"friendlyName":"bulb","ieeeAddr":"0x000b57fffec6a5b2","lastSeen":1000,"modelID":"TRADFRI bulb E27 WS opal 980lm","networkAddress":40369,"type":"Router"},{"definition":{"description":"Hue Go","model":"7146060PH","supports":"light (state, brightness, color_temp, color_temp_startup, color_xy, color_hs), effect, linkquality","vendor":"Philips"},"failed":[],"friendlyName":"bulb_color","ieeeAddr":"0x000b57fffec6a5b3","lastSeen":1000,"modelID":"LLC020","networkAddress":40399,"type":"Router"},{"definition":{"description":"Aqara double key wireless wall switch (2016 model)","model":"WXKG02LM_rev1","supports":"battery, action, voltage, linkquality","vendor":"Xiaomi"},"friendlyName":"button_double_key","ieeeAddr":"0x0017880104e45521","lastSeen":1000,"modelID":"lumi.sensor_86sw2.es1","networkAddress":6538,"type":"EndDevice"},{"definition":null,"failed":["lqi","routingTable"],"friendlyName":"0x0017880104e45525","ieeeAddr":"0x0017880104e45525","lastSeen":1000,"manufacturerName":"Boef","modelID":"notSupportedModelID","networkAddress":6536,"type":"Router"},{"definition":{"description":"[CC2530 router](http://ptvo.info/cc2530-based-zigbee-coordinator-and-router-112/)","model":"CC2530.ROUTER","supports":"led, linkquality","vendor":"Custom devices (DiY)"},"failed":[],"friendlyName":"cc2530_router","ieeeAddr":"0x0017880104e45559","lastSeen":1000,"modelID":"lumi.router","networkAddress":6540,"type":"Router"},{"definition":{"description":"external","model":"external_converter_device","supports":"linkquality","vendor":"external"},"friendlyName":"0x0017880104e45511","ieeeAddr":"0x0017880104e45511","lastSeen":1000,"modelID":"external_converter_device","networkAddress":1114,"type":"EndDevice"}]}},"status":"ok"};
|
||||
const expected = {"data":{"routes":true,"type":"raw","value":{"links":[{"depth":1,"linkquality":120,"lqi":120,"relationship":2,"routes":[],"source":{"ieeeAddr":"0x000b57fffec6a5b3","networkAddress":40399},"sourceIeeeAddr":"0x000b57fffec6a5b3","sourceNwkAddr":40399,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":1,"linkquality":92,"lqi":92,"relationship":2,"routes":[{"destinationAddress":6540,"nextHop":40369,"status":"ACTIVE"}],"source":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"sourceIeeeAddr":"0x000b57fffec6a5b2","sourceNwkAddr":40369,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":1,"linkquality":92,"lqi":92,"relationship":2,"routes":[],"source":{"ieeeAddr":"0x0017880104e45511","networkAddress":1114},"sourceIeeeAddr":"0x0017880104e45511","sourceNwkAddr":1114,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":2,"linkquality":110,"lqi":110,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x000b57fffec6a5b3","networkAddress":40399},"sourceIeeeAddr":"0x000b57fffec6a5b3","sourceNwkAddr":40399,"target":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"targetIeeeAddr":"0x000b57fffec6a5b2"},{"depth":2,"linkquality":100,"lqi":100,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x0017880104e45559","networkAddress":6540},"sourceIeeeAddr":"0x0017880104e45559","sourceNwkAddr":6540,"target":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"targetIeeeAddr":"0x000b57fffec6a5b2"},{"depth":2,"linkquality":130,"lqi":130,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x0017880104e45521","networkAddress":6538},"sourceIeeeAddr":"0x0017880104e45521","sourceNwkAddr":6538,"target":{"ieeeAddr":"0x0017880104e45559","networkAddress":6540},"targetIeeeAddr":"0x0017880104e45559"}],"nodes":[{"definition":null,"failed":[],"friendlyName":"Coordinator","ieeeAddr":"0x00124b00120144ae","lastSeen":1000,"modelID":null,"networkAddress":0,"type":"Coordinator"},{"definition":{"description":"TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white","model":"LED1545G12","supports":"light (state, brightness, color_temp, color_temp_startup), effect, power_on_behavior, linkquality","vendor":"IKEA"},"failed":[],"friendlyName":"bulb","ieeeAddr":"0x000b57fffec6a5b2","lastSeen":1000,"modelID":"TRADFRI bulb E27 WS opal 980lm","networkAddress":40369,"type":"Router"},{"definition":{"description":"Hue Go","model":"7146060PH","supports":"light (state, brightness, color_temp, color_temp_startup, color_xy, color_hs), power_on_behavior, effect, linkquality","vendor":"Philips"},"failed":[],"friendlyName":"bulb_color","ieeeAddr":"0x000b57fffec6a5b3","lastSeen":1000,"modelID":"LLC020","networkAddress":40399,"type":"Router"},{"definition":{"description":"Aqara double key wireless wall switch (2016 model)","model":"WXKG02LM_rev1","supports":"battery, action, voltage, power_outage_count, linkquality","vendor":"Xiaomi"},"friendlyName":"button_double_key","ieeeAddr":"0x0017880104e45521","lastSeen":1000,"modelID":"lumi.sensor_86sw2.es1","networkAddress":6538,"type":"EndDevice"},{"definition":null,"failed":["lqi","routingTable"],"friendlyName":"0x0017880104e45525","ieeeAddr":"0x0017880104e45525","lastSeen":1000,"manufacturerName":"Boef","modelID":"notSupportedModelID","networkAddress":6536,"type":"Router"},{"definition":{"description":"[CC2530 router](http://ptvo.info/cc2530-based-zigbee-coordinator-and-router-112/)","model":"CC2530.ROUTER","supports":"led, linkquality","vendor":"Custom devices (DiY)"},"failed":[],"friendlyName":"cc2530_router","ieeeAddr":"0x0017880104e45559","lastSeen":1000,"modelID":"lumi.router","networkAddress":6540,"type":"Router"},{"definition":{"description":"external","model":"external_converter_device","supports":"linkquality","vendor":"external"},"friendlyName":"0x0017880104e45511","ieeeAddr":"0x0017880104e45511","lastSeen":1000,"modelID":"external_converter_device","networkAddress":1114,"type":"EndDevice"}]}},"status":"ok"};
|
||||
const actual = JSON.parse(call[1]);
|
||||
expect(actual).toStrictEqual(expected);
|
||||
});
|
||||
@@ -292,4 +292,19 @@ describe('Networkmap', () => {
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it('Should exclude disabled devices from networkmap', async () => {
|
||||
settings.set(['devices', '0x000b57fffec6a5b2', 'disabled'], true);
|
||||
mock();
|
||||
MQTT.publish.mockClear();
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/networkmap', stringify({type: 'raw', routes: true}));
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
let call = MQTT.publish.mock.calls[0];
|
||||
expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap');
|
||||
|
||||
const expected = {"data":{"routes":true,"type":"raw","value":{"links":[{"depth":1,"linkquality":120,"lqi":120,"relationship":2,"routes":[],"source":{"ieeeAddr":"0x000b57fffec6a5b3","networkAddress":40399},"sourceIeeeAddr":"0x000b57fffec6a5b3","sourceNwkAddr":40399,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":1,"linkquality":92,"lqi":92,"relationship":2,"routes":[{"destinationAddress":6540,"nextHop":40369,"status":"ACTIVE"}],"source":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"sourceIeeeAddr":"0x000b57fffec6a5b2","sourceNwkAddr":40369,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":1,"linkquality":92,"lqi":92,"relationship":2,"routes":[],"source":{"ieeeAddr":"0x0017880104e45511","networkAddress":1114},"sourceIeeeAddr":"0x0017880104e45511","sourceNwkAddr":1114,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":2,"linkquality":130,"lqi":130,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x0017880104e45521","networkAddress":6538},"sourceIeeeAddr":"0x0017880104e45521","sourceNwkAddr":6538,"target":{"ieeeAddr":"0x0017880104e45559","networkAddress":6540},"targetIeeeAddr":"0x0017880104e45559"}],"nodes":[{"definition":null,"failed":[],"friendlyName":"Coordinator","ieeeAddr":"0x00124b00120144ae","lastSeen":1000,"modelID":null,"networkAddress":0,"type":"Coordinator"},{"definition":{"description":"Hue Go","model":"7146060PH","supports":"light (state, brightness, color_temp, color_temp_startup, color_xy, color_hs), power_on_behavior, effect, linkquality","vendor":"Philips"},"failed":[],"friendlyName":"bulb_color","ieeeAddr":"0x000b57fffec6a5b3","lastSeen":1000,"modelID":"LLC020","networkAddress":40399,"type":"Router"},{"definition":{"description":"Aqara double key wireless wall switch (2016 model)","model":"WXKG02LM_rev1","supports":"battery, action, voltage, power_outage_count, linkquality","vendor":"Xiaomi"},"friendlyName":"button_double_key","ieeeAddr":"0x0017880104e45521","lastSeen":1000,"modelID":"lumi.sensor_86sw2.es1","networkAddress":6538,"type":"EndDevice"},{"definition":null,"failed":["lqi","routingTable"],"friendlyName":"0x0017880104e45525","ieeeAddr":"0x0017880104e45525","lastSeen":1000,"manufacturerName":"Boef","modelID":"notSupportedModelID","networkAddress":6536,"type":"Router"},{"definition":{"description":"[CC2530 router](http://ptvo.info/cc2530-based-zigbee-coordinator-and-router-112/)","model":"CC2530.ROUTER","supports":"led, linkquality","vendor":"Custom devices (DiY)"},"failed":[],"friendlyName":"cc2530_router","ieeeAddr":"0x0017880104e45559","lastSeen":1000,"modelID":"lumi.router","networkAddress":6540,"type":"Router"},{"definition":{"description":"external","model":"external_converter_device","supports":"linkquality","vendor":"external"},"friendlyName":"0x0017880104e45511","ieeeAddr":"0x0017880104e45511","lastSeen":1000,"modelID":"external_converter_device","networkAddress":1114,"type":"EndDevice"}]}},"status":"ok"};
|
||||
const actual = JSON.parse(call[1]);
|
||||
expect(actual).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,6 +48,8 @@ describe('On event', () => {
|
||||
expect(call[0]).toBe('start')
|
||||
expect(call[1]).toStrictEqual({})
|
||||
expect(call[2]).toBe(device);
|
||||
expect(call[3]).toStrictEqual(settings.getDevice(device.ieeeAddr));
|
||||
expect(call[4]).toStrictEqual({});
|
||||
});
|
||||
|
||||
it('Should call with stop event', async () => {
|
||||
@@ -66,7 +68,7 @@ describe('On event', () => {
|
||||
await zigbeeHerdsman.events.deviceAnnounce({device});
|
||||
await flushPromises();
|
||||
expect(mockOnEvent).toHaveBeenCalledTimes(1);
|
||||
expect(mockOnEvent).toHaveBeenCalledWith('deviceAnnounce', {device}, device, settings.getDevice(device.ieeeAddr));
|
||||
expect(mockOnEvent).toHaveBeenCalledWith('deviceAnnounce', {device}, device, settings.getDevice(device.ieeeAddr), {});
|
||||
});
|
||||
|
||||
it('Should call index onEvent with zigbee event', async () => {
|
||||
@@ -74,6 +76,6 @@ describe('On event', () => {
|
||||
await zigbeeHerdsman.events.deviceAnnounce({device});
|
||||
await flushPromises();
|
||||
expect(zigbeeHerdsmanConverters.onEvent).toHaveBeenCalledTimes(1);
|
||||
expect(zigbeeHerdsmanConverters.onEvent).toHaveBeenCalledWith('deviceAnnounce', {device}, device);
|
||||
expect(zigbeeHerdsmanConverters.onEvent).toHaveBeenCalledWith('deviceAnnounce', {device}, device, settings.getDevice(device.ieeeAddr), {});
|
||||
});
|
||||
});
|
||||
|
||||
+23
-17
@@ -1,3 +1,5 @@
|
||||
const path = require('path');
|
||||
|
||||
const data = require('./stub/data');
|
||||
const logger = require('./stub/logger');
|
||||
const zigbeeHerdsman = require('./stub/zigbeeHerdsman');
|
||||
@@ -11,6 +13,7 @@ const zigbeeOTA = require('zigbee-herdsman-converters/lib/ota/zigbeeOTA');
|
||||
|
||||
const spyUseIndexOverride = jest.spyOn(zigbeeOTA, 'useIndexOverride');
|
||||
|
||||
|
||||
describe('OTA update', () => {
|
||||
let controller;
|
||||
|
||||
@@ -33,6 +36,7 @@ describe('OTA update', () => {
|
||||
jest.useFakeTimers();
|
||||
controller = new Controller(jest.fn(), jest.fn());
|
||||
await controller.start();
|
||||
await jest.runOnlyPendingTimers();
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
@@ -68,6 +72,7 @@ describe('OTA update', () => {
|
||||
mapped.ota.updateToLatest.mockImplementationOnce((a, b, onUpdate) => {
|
||||
onUpdate(0, null);
|
||||
onUpdate(10, 3600.2123);
|
||||
return 90;
|
||||
});
|
||||
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/update', 'bulb');
|
||||
@@ -95,7 +100,7 @@ describe('OTA update', () => {
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb',
|
||||
stringify({"update_available":false,"update":{"state":"idle"}}),
|
||||
stringify({"update_available":false,"update":{"state":"idle","installed_version":90,"latest_version":90}}),
|
||||
{retain: true, qos: 0}, expect.any(Function)
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
@@ -141,7 +146,7 @@ describe('OTA update', () => {
|
||||
const mapped = zigbeeHerdsmanConverters.findByDevice(device)
|
||||
mockClear(mapped);
|
||||
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce(false);
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce({available: false, currentFileVersion: 10, otaFileVersion: 10});
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/check', "bulb");
|
||||
await flushPromises();
|
||||
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1);
|
||||
@@ -153,7 +158,7 @@ describe('OTA update', () => {
|
||||
);
|
||||
|
||||
MQTT.publish.mockClear();
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce(true);
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce({available: true, currentFileVersion: 10, otaFileVersion: 12});
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/check', "bulb");
|
||||
await flushPromises();
|
||||
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(2);
|
||||
@@ -246,7 +251,7 @@ describe('OTA update', () => {
|
||||
const data = {imageType: 12382};
|
||||
const mapped = zigbeeHerdsmanConverters.findByDevice(device)
|
||||
mockClear(mapped);
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce(true);
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce({available: true, currentFileVersion: 10, otaFileVersion: 12});
|
||||
const payload = {data, cluster: 'genOta', device, endpoint: device.getEndpoint(1), type: 'commandQueryNextImageRequest', linkquality: 10};
|
||||
logger.info.mockClear();
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
@@ -255,7 +260,7 @@ describe('OTA update', () => {
|
||||
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledWith(device, logger, {"imageType": 12382});
|
||||
expect(logger.info).toHaveBeenCalledWith(`Update available for 'bulb'`);
|
||||
expect(device.endpoints[0].commandResponse).toHaveBeenCalledTimes(1);
|
||||
expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith("genOta", "queryNextImageResponse", {"status": 0x95});
|
||||
expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith("genOta", "queryNextImageResponse", {"status": 0x98});
|
||||
|
||||
// Should not request again when device asks again after a short time
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
@@ -263,13 +268,13 @@ describe('OTA update', () => {
|
||||
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1);
|
||||
|
||||
logger.info.mockClear();
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce(false);
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce({available: false, currentFileVersion: 10, otaFileVersion: 10});
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(logger.info).not.toHaveBeenCalledWith(`Update available for 'bulb'`);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb',
|
||||
stringify({"update_available":true,"update":{"state":"available"}}),
|
||||
stringify({"update_available":true,"update":{"state":"available","installed_version":10,"latest_version":12}}),
|
||||
{retain: true, qos: 0}, expect.any(Function)
|
||||
);
|
||||
});
|
||||
@@ -302,7 +307,7 @@ describe('OTA update', () => {
|
||||
const data = {imageType: 12382};
|
||||
const mapped = zigbeeHerdsmanConverters.findByDevice(device)
|
||||
mockClear(mapped);
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce(false);
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce({available: false, currentFileVersion: 13, otaFileVersion: 13});
|
||||
const payload = {data, cluster: 'genOta', device, endpoint: device.getEndpoint(1), type: 'commandQueryNextImageRequest', linkquality: 10};
|
||||
logger.info.mockClear();
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
@@ -310,10 +315,10 @@ describe('OTA update', () => {
|
||||
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1);
|
||||
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledWith(device, logger, {"imageType": 12382});
|
||||
expect(device.endpoints[0].commandResponse).toHaveBeenCalledTimes(1);
|
||||
expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith("genOta", "queryNextImageResponse", {"status": 0x95});
|
||||
expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith("genOta", "queryNextImageResponse", {"status": 0x98});
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb',
|
||||
stringify({"update_available":false,"update":{"state":"idle"}}),
|
||||
stringify({"update_available":false,"update":{"state":"idle","installed_version": 13, "latest_version": 13}}),
|
||||
{retain: true, qos: 0}, expect.any(Function)
|
||||
);
|
||||
});
|
||||
@@ -324,7 +329,7 @@ describe('OTA update', () => {
|
||||
const data = {imageType: 12382};
|
||||
const mapped = zigbeeHerdsmanConverters.findByDevice(device)
|
||||
mockClear(mapped);
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce(true);
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce({available: true, currentFileVersion: 10, otaFileVersion: 13});
|
||||
const payload = {data, cluster: 'genOta', device, endpoint: device.getEndpoint(1), type: 'commandQueryNextImageRequest', linkquality: 10};
|
||||
logger.info.mockClear();
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
@@ -342,15 +347,15 @@ describe('OTA update', () => {
|
||||
expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith("genOta", "queryNextImageResponse", {"status": 152});
|
||||
});
|
||||
|
||||
it('Shouldnt respond with NO_IMAGE_AVAILABLE when not supporting OTA and device has no OTA endpoint', async () => {
|
||||
it('Should respond with NO_IMAGE_AVAILABLE when not supporting OTA and device has no OTA endpoint to standard endpoint', async () => {
|
||||
const device = zigbeeHerdsman.devices.SV01;
|
||||
const data = {imageType: 12382};
|
||||
const payload = {data, cluster: 'genOta', device, endpoint: device.getEndpoint(1), type: 'commandQueryNextImageRequest', linkquality: 10};
|
||||
logger.error.mockClear();
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(device.endpoints[0].commandResponse).toHaveBeenCalledTimes(0);
|
||||
expect(logger.error).toHaveBeenCalledTimes(0);
|
||||
expect(device.endpoints[0].commandResponse).toHaveBeenCalledTimes(1);
|
||||
expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith("genOta", "queryNextImageResponse", {"status": 152});
|
||||
});
|
||||
|
||||
it('Legacy api: Should OTA update a device', async () => {
|
||||
@@ -369,6 +374,7 @@ describe('OTA update', () => {
|
||||
mapped.ota.updateToLatest.mockImplementationOnce((a, b, onUpdate) => {
|
||||
onUpdate(0, null);
|
||||
onUpdate(10, 3600);
|
||||
return 91;
|
||||
});
|
||||
|
||||
MQTT.events.message('zigbee2mqtt/bridge/ota_update/update', 'bulb');
|
||||
@@ -411,7 +417,7 @@ describe('OTA update', () => {
|
||||
mockClear(mapped);
|
||||
|
||||
logger.info.mockClear();
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce(false);
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce({available: false, currentFileVersion: 13, otaFileVersion: 13});
|
||||
MQTT.events.message('zigbee2mqtt/bridge/ota_update/check', 'bulb');
|
||||
await flushPromises();
|
||||
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1);
|
||||
@@ -419,7 +425,7 @@ describe('OTA update', () => {
|
||||
expect(logger.info).toHaveBeenCalledWith(`No update available for 'bulb'`);
|
||||
|
||||
logger.info.mockClear();
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce(true);
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce({available: true, currentFileVersion: 13, otaFileVersion: 15});
|
||||
MQTT.events.message('zigbee2mqtt/bridge/ota_update/check', 'bulb');
|
||||
await flushPromises();
|
||||
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(2);
|
||||
@@ -468,7 +474,7 @@ describe('OTA update', () => {
|
||||
it('Set zigbee_ota_override_index_location', async () => {
|
||||
settings.set(['ota', 'zigbee_ota_override_index_location'], 'local.index.json');
|
||||
await resetExtension();
|
||||
expect(spyUseIndexOverride).toHaveBeenCalledWith(data.mockDir + '/local.index.json');
|
||||
expect(spyUseIndexOverride).toHaveBeenCalledWith(path.join(data.mockDir, 'local.index.json'));
|
||||
spyUseIndexOverride.mockClear();
|
||||
|
||||
settings.set(['ota', 'zigbee_ota_override_index_location'], 'http://my.site/index.json');
|
||||
|
||||
+17
-5
@@ -507,6 +507,18 @@ describe('Publish', () => {
|
||||
expect(endpoint3.read).toHaveBeenCalledWith('genOnOff', ['onOff']);
|
||||
});
|
||||
|
||||
it('Should handle get with multiple cover endpoints', async () => {
|
||||
const device = zigbeeHerdsman.devices.zigfred_plus;
|
||||
const endpoint11 = device.getEndpoint(11);
|
||||
const endpoint12 = device.getEndpoint(12);
|
||||
await MQTT.events.message('zigbee2mqtt/zigfred_plus/get', stringify({state_l6: '', state_l7: ''}));
|
||||
await flushPromises();
|
||||
expect(endpoint11.read).toHaveBeenCalledTimes(1);
|
||||
expect(endpoint11.read).toHaveBeenCalledWith('closuresWindowCovering', ['currentPositionLiftPercentage']);
|
||||
expect(endpoint12.read).toHaveBeenCalledTimes(1);
|
||||
expect(endpoint12.read).toHaveBeenCalledWith('closuresWindowCovering', ['currentPositionLiftPercentage']);
|
||||
});
|
||||
|
||||
it('Should log error when device has no such endpoint (via topic)', async () => {
|
||||
const device = zigbeeHerdsman.devices.QBKG03LM;
|
||||
const endpoint2 = device.getEndpoint(2);
|
||||
@@ -919,11 +931,11 @@ describe('Publish', () => {
|
||||
expect(endpoint.command).toHaveBeenCalledTimes(3);
|
||||
expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 50, transtime: 0}, {}]);
|
||||
expect(endpoint.command.mock.calls[1]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 0, transtime: 0}, {}]);
|
||||
expect(endpoint.command.mock.calls[2]).toEqual(["genOnOff", "on", {}, {}]);
|
||||
expect(endpoint.command.mock.calls[2]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 50, transtime: 0}, {}]);
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(3);
|
||||
expect(MQTT.publish.mock.calls[0]).toEqual(["zigbee2mqtt/bulb_color", stringify({state: 'ON', brightness: 50}), {"qos": 0, "retain": false}, expect.any(Function)]);
|
||||
expect(MQTT.publish.mock.calls[1]).toEqual(["zigbee2mqtt/bulb_color", stringify({state: 'OFF', brightness: 0}), {"qos": 0, "retain": false}, expect.any(Function)]);
|
||||
expect(MQTT.publish.mock.calls[2]).toEqual(["zigbee2mqtt/bulb_color", stringify({state: 'ON', brightness: 1}), {"qos": 0, "retain": false}, expect.any(Function)]);
|
||||
expect(MQTT.publish.mock.calls[2]).toEqual(["zigbee2mqtt/bulb_color", stringify({state: 'ON', brightness: 50}), {"qos": 0, "retain": false}, expect.any(Function)]);
|
||||
});
|
||||
|
||||
it('Should turn device off when brightness 0 is send with transition', async () => {
|
||||
@@ -938,11 +950,11 @@ describe('Publish', () => {
|
||||
expect(endpoint.command).toHaveBeenCalledTimes(3);
|
||||
expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 50, transtime: 0}, {}]);
|
||||
expect(endpoint.command.mock.calls[1]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 0, transtime: 30}, {}]);
|
||||
expect(endpoint.command.mock.calls[2]).toEqual(["genOnOff", "on", {}, {}]);
|
||||
expect(endpoint.command.mock.calls[2]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 50, transtime: 0}, {}]);
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(3);
|
||||
expect(MQTT.publish.mock.calls[0]).toEqual(["zigbee2mqtt/bulb_color", stringify({state: 'ON', brightness: 50}), {"qos": 0, "retain": false}, expect.any(Function)]);
|
||||
expect(MQTT.publish.mock.calls[1]).toEqual(["zigbee2mqtt/bulb_color", stringify({state: 'OFF', brightness: 0}), {"qos": 0, "retain": false}, expect.any(Function)]);
|
||||
expect(MQTT.publish.mock.calls[2]).toEqual(["zigbee2mqtt/bulb_color", stringify({state: 'ON', brightness: 1}), {"qos": 0, "retain": false}, expect.any(Function)]);
|
||||
expect(MQTT.publish.mock.calls[2]).toEqual(["zigbee2mqtt/bulb_color", stringify({state: 'ON', brightness: 50}), {"qos": 0, "retain": false}, expect.any(Function)]);
|
||||
});
|
||||
|
||||
it('Should allow to set color via hue and saturation', async () => {
|
||||
@@ -952,7 +964,7 @@ describe('Publish', () => {
|
||||
await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload));
|
||||
await flushPromises();
|
||||
expect(endpoint.command).toHaveBeenCalledTimes(1);
|
||||
expect(endpoint.command.mock.calls[0]).toEqual(["lightingColorCtrl", "enhancedMoveToHueAndSaturation", {"direction": 0, "enhancehue": 44877, "saturation": 200, "transtime": 0,}, {}]);
|
||||
expect(endpoint.command.mock.calls[0]).toEqual(["lightingColorCtrl", "enhancedMoveToHueAndSaturation", {"direction": 0, "enhancehue": 45510, "saturation": 127, "transtime": 0,}, {}]);
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({"color":{"hue":250,"saturation":50}, "color_mode": "hs"});
|
||||
|
||||
+52
-9
@@ -16,6 +16,8 @@ describe('Receive', () => {
|
||||
jest.useFakeTimers();
|
||||
controller = new Controller(jest.fn(), jest.fn());
|
||||
await controller.start();
|
||||
await jest.runOnlyPendingTimers();
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -84,7 +86,7 @@ describe('Receive', () => {
|
||||
await zigbeeHerdsman.events.message({data: {currentPositionLiftPercentage: 90, currentPositionTiltPercentage: 80}, cluster: 'closuresWindowCovering', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10});
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/J1_cover', stringify({position: 10, tilt: 20}), {retain: false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/J1_cover', stringify({position: 10, tilt: 20, state: 'OPEN'}), {retain: false, qos: 0}, expect.any(Function));
|
||||
|
||||
// Inverted
|
||||
MQTT.publish.mockClear();
|
||||
@@ -92,7 +94,7 @@ describe('Receive', () => {
|
||||
await zigbeeHerdsman.events.message({data: {currentPositionLiftPercentage: 90, currentPositionTiltPercentage: 80}, cluster: 'closuresWindowCovering', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10});
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/J1_cover', stringify({position: 90, tilt: 80}), {retain: false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/J1_cover', stringify({position: 90, tilt: 80, state: 'OPEN'}), {retain: false, qos: 0}, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Should allow to disable the legacy integration', async () => {
|
||||
@@ -176,6 +178,47 @@ describe('Receive', () => {
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({temperature: 0.07, pressure: 2, humidity: 0.03});
|
||||
});
|
||||
|
||||
it('Should NOT publish old messages from State cache during debouncing', async () => {
|
||||
|
||||
// Summary:
|
||||
// First send multiple measurements to device that is debouncing. Make sure only one message is sent out to MQTT. This also ensures first message is cached to "State".
|
||||
// Then send another measurement to that same device and trigger asyncronous event to push data from Cache. Newest value should be sent out.
|
||||
|
||||
const device = zigbeeHerdsman.devices.WSDCGQ11LM;
|
||||
settings.set(['devices', device.ieeeAddr, 'debounce'], 0.1);
|
||||
await zigbeeHerdsman.events.message({data: {measuredValue: 8}, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10});
|
||||
await zigbeeHerdsman.events.message( {data: {measuredValue: 1}, cluster: 'msRelativeHumidity', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10} );
|
||||
await zigbeeHerdsman.events.message( {data: {measuredValue: 2}, cluster: 'msPressureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10} );
|
||||
await flushPromises();
|
||||
jest.advanceTimersByTime(50);
|
||||
// Test that measurements are combined(=debounced)
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(0);
|
||||
jest.runOnlyPendingTimers();
|
||||
await flushPromises();
|
||||
|
||||
// Test that only one MQTT is sent out and test its values.
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: 0.08, humidity: 0.01, pressure: 2});
|
||||
|
||||
// Send another Zigbee messages...
|
||||
await zigbeeHerdsman.events.message({data: {measuredValue: 9}, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10});
|
||||
const realDevice = controller.zigbee.resolveEntity(device);
|
||||
|
||||
// Trigger asyncronous event while device is "debouncing" to trigger Message to be sent out from State cache.
|
||||
await controller.publishEntityState( realDevice, {} );
|
||||
jest.runOnlyPendingTimers();
|
||||
await flushPromises();
|
||||
|
||||
// Total of 3 messages should have triggered.
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(3);
|
||||
|
||||
// Test that message pushed by asyncronous message contains NEW measurement and not old.
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({temperature: 0.09, humidity: 0.01, pressure: 2});
|
||||
// Test that messages after debouncing contains NEW measurement and not old.
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[2][1])).toStrictEqual({temperature: 0.09, humidity: 0.01, pressure: 2});
|
||||
});
|
||||
|
||||
it('Shouldnt republish old state', async () => {
|
||||
// https://github.com/Koenkk/zigbee2mqtt/issues/3572
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
@@ -242,27 +285,27 @@ describe('Receive', () => {
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 1, "retain": false});
|
||||
});
|
||||
|
||||
it('Should handle a zigbee message with voltage 3010', async () => {
|
||||
it('Should handle a zigbee message with voltage 2990', async () => {
|
||||
const device = zigbeeHerdsman.devices.WXKG02LM_rev1;
|
||||
const data = {'65281': {'1': 3010}}
|
||||
const data = {'65281': {'1': 2990}}
|
||||
const payload = {data, cluster: 'genBasic', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({battery: 100, voltage: 3010});
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({battery: 93, voltage: 2990});
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
|
||||
});
|
||||
|
||||
it('Should publish 1 message when converted twice', async () => {
|
||||
const device = zigbeeHerdsman.devices.RTCGQ11LM;
|
||||
const data = {'65281': {'1': 3045, '3': 19, '4': 17320, '5': 35, '6': [0, 3], '10': 51107, '11': 381, '100': 0}}
|
||||
const data = {'65281': {'1': 3045, '3': 19, '5': 35, '6': [0, 3], '11': 381, '100': 0}}
|
||||
const payload = {data, cluster: 'genBasic', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/occupancy_sensor');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({'battery': 100, 'illuminance': 381, "illuminance_lux": 381, 'voltage': 3045, 'temperature': 19});
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({'battery': 100, 'illuminance': 381, "illuminance_lux": 381, 'voltage': 3045, 'device_temperature': 19, 'power_outage_count': 34});
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
|
||||
});
|
||||
|
||||
@@ -419,7 +462,7 @@ describe('Receive', () => {
|
||||
const data = {instantaneousDemand:496,currentSummDelivered:[0,6648]}
|
||||
|
||||
const SP600_NEW = zigbeeHerdsman.devices.SP600_NEW;
|
||||
await zigbeeHerdsman.events.message({data, cluster: 'seMetering', device: SP600_NEW, endpoint: SP600_NEW.getEndpoint(1), type: 'attributeReport', linkquality: 10});
|
||||
await zigbeeHerdsman.events.message({data, cluster: 'seMetering', device: SP600_NEW, endpoint: SP600_NEW.getEndpoint(1), type: 'attributeReport', linkquality: 10, meta: {zclTransactionSequenceNumber: 1}});
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/SP600_NEW');
|
||||
@@ -427,7 +470,7 @@ describe('Receive', () => {
|
||||
|
||||
MQTT.publish.mockClear();
|
||||
const SP600_OLD = zigbeeHerdsman.devices.SP600_OLD;
|
||||
await zigbeeHerdsman.events.message({data, cluster: 'seMetering', device: SP600_OLD, endpoint: SP600_OLD.getEndpoint(1), type: 'attributeReport', linkquality: 10});
|
||||
await zigbeeHerdsman.events.message({data, cluster: 'seMetering', device: SP600_OLD, endpoint: SP600_OLD.getEndpoint(1), type: 'attributeReport', linkquality: 10, meta: {zclTransactionSequenceNumber: 2}});
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/SP600_OLD');
|
||||
|
||||
+96
-15
@@ -39,6 +39,7 @@ describe('Settings', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
remove(configurationFile);
|
||||
remove(secretFile);
|
||||
remove(devicesFile);
|
||||
remove(groupsFile);
|
||||
clearEnvironmentVariables();
|
||||
@@ -66,20 +67,42 @@ describe('Settings', () => {
|
||||
it('Should apply environment variables', () => {
|
||||
process.env['ZIGBEE2MQTT_CONFIG_SERIAL_DISABLE_LED'] = 'true';
|
||||
process.env['ZIGBEE2MQTT_CONFIG_ADVANCED_SOFT_RESET_TIMEOUT'] = 1;
|
||||
process.env['ZIGBEE2MQTT_CONFIG_ADVANCED_OUTPUT'] = 'csvtest';
|
||||
process.env['ZIGBEE2MQTT_CONFIG_ADVANCED_OUTPUT'] = 'attribute_and_json';
|
||||
process.env['ZIGBEE2MQTT_CONFIG_ADVANCED_LOG_OUTPUT'] = '["console"]';
|
||||
process.env['ZIGBEE2MQTT_CONFIG_MAP_OPTIONS_GRAPHVIZ_COLORS_FILL'] = '{"enddevice": "#ff0000", "coordinator": "#00ff00", "router": "#0000ff"}';
|
||||
process.env['ZIGBEE2MQTT_CONFIG_MQTT_BASE_TOPIC'] = 'testtopic';
|
||||
process.env['ZIGBEE2MQTT_CONFIG_MQTT_SERVER'] = 'testserver';
|
||||
process.env['ZIGBEE2MQTT_CONFIG_ADVANCED_NETWORK_KEY'] = 'GENERATE';
|
||||
process.env['ZIGBEE2MQTT_CONFIG_DEVICES'] = 'devices.yaml';
|
||||
|
||||
const contentDevices = {
|
||||
'0x00158d00018255df': {
|
||||
friendly_name: '0x00158d00018255df',
|
||||
retain: false,
|
||||
},
|
||||
};
|
||||
|
||||
write(configurationFile, {});
|
||||
write(devicesFile, contentDevices);
|
||||
expect(settings.validate()).toStrictEqual([]);
|
||||
|
||||
const s = settings.get();
|
||||
const expected = objectAssignDeep.noMutate({}, settings.testing.defaults);
|
||||
expected.devices = {};
|
||||
expected.devices = {
|
||||
'0x00158d00018255df': {
|
||||
friendly_name: '0x00158d00018255df',
|
||||
retain: false,
|
||||
},
|
||||
};
|
||||
expected.groups = {};
|
||||
expected.serial.disable_led = true;
|
||||
expected.advanced.soft_reset_timeout = 1;
|
||||
expected.advanced.output = 'csvtest';
|
||||
expected.advanced.log_output = ["console"];
|
||||
expected.advanced.output = 'attribute_and_json';
|
||||
expected.map_options.graphviz.colors.fill = {enddevice: '#ff0000', coordinator: '#00ff00', router: '#0000ff'};
|
||||
expected.mqtt.base_topic = 'testtopic';
|
||||
expected.mqtt.server = 'testserver';
|
||||
expected.advanced.network_key = 'GENERATE';
|
||||
|
||||
expect(s).toStrictEqual(expected);
|
||||
});
|
||||
@@ -100,6 +123,22 @@ describe('Settings', () => {
|
||||
expect(actual).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('Should add devices even when devices exist empty', () => {
|
||||
write(configurationFile, {devices: []});
|
||||
settings.addDevice('0x12345678');
|
||||
|
||||
const actual = read(configurationFile);
|
||||
const expected = {
|
||||
devices: {
|
||||
'0x12345678': {
|
||||
friendly_name: '0x12345678',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(actual).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('Should read devices', () => {
|
||||
const content = {
|
||||
devices: {
|
||||
@@ -171,6 +210,49 @@ describe('Settings', () => {
|
||||
expect(read(secretFile)).toStrictEqual({...contentSecret, username: 'test123', network_key: [1,2,3,4]});
|
||||
});
|
||||
|
||||
it('Should read ALL secrets form a separate file', () => {
|
||||
const contentConfiguration = {
|
||||
mqtt: {
|
||||
server: '!secret server',
|
||||
user: '!secret username',
|
||||
password: '!secret.yaml password',
|
||||
},
|
||||
advanced: {
|
||||
network_key: '!secret network_key',
|
||||
}
|
||||
};
|
||||
|
||||
const contentSecret = {
|
||||
server: 'my.mqtt.server',
|
||||
username: 'mysecretusername',
|
||||
password: 'mysecretpassword',
|
||||
network_key: [1,2,3],
|
||||
};
|
||||
|
||||
write(secretFile, contentSecret, false);
|
||||
write(configurationFile, contentConfiguration);
|
||||
|
||||
const expected = {
|
||||
base_topic: 'zigbee2mqtt',
|
||||
include_device_information: false,
|
||||
force_disable_retain: false,
|
||||
password: "mysecretpassword",
|
||||
server: "my.mqtt.server",
|
||||
user: "mysecretusername",
|
||||
};
|
||||
|
||||
expect(settings.get().mqtt).toStrictEqual(expected);
|
||||
expect(settings.get().advanced.network_key).toStrictEqual([1,2,3]);
|
||||
|
||||
settings.testing.write();
|
||||
expect(read(configurationFile)).toStrictEqual(contentConfiguration);
|
||||
expect(read(secretFile)).toStrictEqual(contentSecret);
|
||||
|
||||
settings.set(['mqtt', 'server'], 'not.secret.server');
|
||||
expect(read(configurationFile)).toStrictEqual(contentConfiguration);
|
||||
expect(read(secretFile)).toStrictEqual({...contentSecret, server: 'not.secret.server'});
|
||||
});
|
||||
|
||||
it('Should read devices form a separate file', () => {
|
||||
const contentConfiguration = {
|
||||
devices: 'devices.yaml',
|
||||
@@ -694,8 +776,7 @@ describe('Settings', () => {
|
||||
|
||||
it('Should throw error when yaml file does not exist', () => {
|
||||
settings.testing.clear();
|
||||
const error = `ENOENT: no such file or directory, open '${configurationFile}'`;
|
||||
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
|
||||
expect(settings.validate()[0].startsWith(`ENOENT: no such file or directory, open `)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('Configuration shouldnt be valid when invalid QOS value is used', async () => {
|
||||
@@ -747,15 +828,15 @@ describe('Settings', () => {
|
||||
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
|
||||
});
|
||||
|
||||
it('Configuration friendly name cannot contain null char', async () => {
|
||||
it('Configuration friendly name cannot contain control char', async () => {
|
||||
write(configurationFile, {
|
||||
...minimalConfig,
|
||||
devices: {'0x0017880104e45519': {friendly_name: 'blaa/blaa' + String.fromCharCode(0), retain: false}},
|
||||
devices: {'0x0017880104e45519': {friendly_name: 'blaa/blaa\u009f', retain: false}},
|
||||
});
|
||||
|
||||
settings.reRead();
|
||||
|
||||
const error = `friendly_name is not allowed to contain null char`;
|
||||
const error = `friendly_name is not allowed to contain control char`;
|
||||
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
|
||||
});
|
||||
|
||||
@@ -838,7 +919,7 @@ describe('Settings', () => {
|
||||
|
||||
it('Frontend config', () => {
|
||||
write(configurationFile, {...minimalConfig,
|
||||
frontend: true,
|
||||
frontend: true,
|
||||
});
|
||||
|
||||
settings.reRead();
|
||||
@@ -847,7 +928,7 @@ describe('Settings', () => {
|
||||
|
||||
it('Baudrate config', () => {
|
||||
write(configurationFile, {...minimalConfig,
|
||||
advanced: {baudrate: 20},
|
||||
advanced: {baudrate: 20},
|
||||
});
|
||||
|
||||
settings.reRead();
|
||||
@@ -856,7 +937,7 @@ describe('Settings', () => {
|
||||
|
||||
it('ikea_ota_use_test_url config', () => {
|
||||
write(configurationFile, {...minimalConfig,
|
||||
advanced: {ikea_ota_use_test_url: true},
|
||||
advanced: {ikea_ota_use_test_url: true},
|
||||
});
|
||||
|
||||
settings.reRead();
|
||||
@@ -865,7 +946,7 @@ describe('Settings', () => {
|
||||
|
||||
it('transmit_power config', () => {
|
||||
write(configurationFile, {...minimalConfig,
|
||||
experimental: {transmit_power: 1337},
|
||||
experimental: {transmit_power: 1337},
|
||||
});
|
||||
|
||||
settings.reRead();
|
||||
@@ -874,7 +955,7 @@ describe('Settings', () => {
|
||||
|
||||
it('output config', () => {
|
||||
write(configurationFile, {...minimalConfig,
|
||||
experimental: {output: 'json'},
|
||||
experimental: {output: 'json'},
|
||||
});
|
||||
|
||||
settings.reRead();
|
||||
@@ -883,7 +964,7 @@ describe('Settings', () => {
|
||||
|
||||
it('Baudrartsctste config', () => {
|
||||
write(configurationFile, {...minimalConfig,
|
||||
advanced: {rtscts: true},
|
||||
advanced: {rtscts: true},
|
||||
});
|
||||
|
||||
settings.reRead();
|
||||
@@ -892,7 +973,7 @@ describe('Settings', () => {
|
||||
|
||||
it('Deprecated: Home Assistant config', () => {
|
||||
write(configurationFile, {...minimalConfig,
|
||||
homeassistant: {discovery_topic: 'new'},
|
||||
homeassistant: {discovery_topic: 'new'},
|
||||
advanced: {homeassistant_discovery_topic: 'old', homeassistant_status_topic: 'olds'},
|
||||
});
|
||||
|
||||
|
||||
@@ -153,6 +153,23 @@ function writeDefaultConfiguration() {
|
||||
'0x0017880104e43559': {
|
||||
friendly_name: 'U202DST600ZB'
|
||||
},
|
||||
'0xf4ce368a38be56a1': {
|
||||
retain: false,
|
||||
friendly_name: 'zigfred_plus',
|
||||
front_surface_enabled: 'true',
|
||||
dimmer_1_enabled: 'true',
|
||||
dimmer_1_dimming_enabled: 'true',
|
||||
dimmer_2_enabled: 'true',
|
||||
dimmer_2_dimming_enabled: 'true',
|
||||
dimmer_3_enabled: 'true',
|
||||
dimmer_3_dimming_enabled: 'true',
|
||||
dimmer_4_enabled: 'true',
|
||||
dimmer_4_dimming_enabled: 'true',
|
||||
cover_1_enabled: 'true',
|
||||
cover_1_tilt_enabled: 'true',
|
||||
cover_2_enabled: 'true',
|
||||
cover_2_tilt_enabled: 'true',
|
||||
},
|
||||
'0x0017880104e44559': {
|
||||
friendly_name: '3157100_thermostat',
|
||||
},
|
||||
@@ -243,6 +260,9 @@ const defaultState = {
|
||||
"0x0017880104e45517": {
|
||||
"brightness": 255
|
||||
},
|
||||
"1": {
|
||||
'state': 'ON',
|
||||
}
|
||||
}
|
||||
|
||||
function getDefaultState() {
|
||||
|
||||
@@ -12,6 +12,7 @@ const callTransports = (level, message) => {
|
||||
}
|
||||
|
||||
const mock = {
|
||||
init: jest.fn(),
|
||||
info: jest.fn().mockImplementation((msg) => callTransports('info', msg)),
|
||||
warn: jest.fn().mockImplementation((msg) => callTransports('warn', msg)),
|
||||
error: jest.fn().mockImplementation((msg) => callTransports('error', msg)),
|
||||
|
||||
@@ -27,7 +27,11 @@ const clusters = {
|
||||
'lightingColorCtrl': 768,
|
||||
'closuresWindowCovering': 258,
|
||||
'hvacThermostat': 513,
|
||||
'msIlluminanceMeasurement': 1024,
|
||||
'msTemperatureMeasurement': 1026,
|
||||
'msRelativeHumidity': 1029,
|
||||
'msSoilMoisture': 1032,
|
||||
'msCO2': 1037
|
||||
}
|
||||
|
||||
class Endpoint {
|
||||
@@ -133,6 +137,7 @@ const TS0601_thermostat = new Device('EndDevice', '0x0017882104a44559', 6544,41
|
||||
const ZNCZ02LM = new Device('Router', '0x0017880104e45524', 6540,4151, [new Endpoint(1, [0], [], '0x0017880104e45524')], true, "Mains (single phase)", "lumi.plug");
|
||||
const GLEDOPTO_2ID = new Device('Router', '0x0017880104e45724', 6540,4151, [new Endpoint(11, [0,3,4,5,6,8,768], [], '0x0017880104e45724', [], {}, [], 49246, 528), new Endpoint(12, [0, 3, 4, 5, 6, 8, 768], [], '0x0017880104e45724', [], {}, [], 260, 258), new Endpoint(13, [4096], [4096], '0x0017880104e45724', [], {}, [], 49246, 57694), new Endpoint(15, [0, 3, 4, 5, 6, 8, 768], [], '0x0017880104e45724', [], {}, [], 49246, 256)], true, "Mains (single phase)", 'GL-C-007', false, 'GLEDOPTO');
|
||||
const QBKG03LM = new Device('Router', '0x0017880104e45542', 6540,4151, [new Endpoint(1, [0], [], '0x0017880104e45542'), new Endpoint(2, [0, 6], [], '0x0017880104e45542'), new Endpoint(3, [0, 6], [], '0x0017880104e45542')], true, "Mains (single phase)", 'lumi.ctrl_neutral2');
|
||||
const zigfred_plus = new Device('Router', '0xf4ce368a38be56a1', 6589, 0x129C, [new Endpoint(5, [0, 3, 4, 5, 6, 8, 0x0300, 0xFC42], [0xFC42], '0xf4ce368a38be56a1'), new Endpoint(7, [0, 3, 4, 5, 6, 8], [], '0xf4ce368a38be56a1'), new Endpoint(8, [0, 3, 4, 5, 6, 8], [], '0xf4ce368a38be56a1'), new Endpoint(9, [0, 3, 4, 5, 6, 8], [], '0xf4ce368a38be56a1'), new Endpoint(10, [0, 3, 4, 5, 6, 8], [], '0xf4ce368a38be56a1'), new Endpoint(11, [0, 3, 4, 5, 0x0102], [], '0xf4ce368a38be56a1'), new Endpoint(12, [0, 3, 4, 5, 0x0102], [], '0xf4ce368a38be56a1')], true, "Mains (single phase)", 'zigfred plus', false, 'Siglis');
|
||||
|
||||
const groups = {
|
||||
'group_1': new Group(1, []),
|
||||
@@ -188,7 +193,8 @@ const devices = {
|
||||
'MKS-CM-W5': new Device('Router', '0x90fd9ffffe4b64ac', 33901, 4476, [new Endpoint(1, [0,4,3,5,10,258,13,19,6,1,1030,8,768,1027,1029,1026], [0,3,4,6,8,5], '0x90fd9ffffe4b64aa', [], {})], true, "Mains (single phase)", "qnazj70", false),
|
||||
'GL-S-007ZS': new Device('Router', '0x0017880104e45526', 6540,4151, [new Endpoint(1, [0], [], '0x0017880104e45526')], true, "Mains (single phase)", 'GL-S-007ZS'),
|
||||
'U202DST600ZB': new Device('Router', '0x0017880104e43559', 6540,4151, [new Endpoint(10, [0, 6], [], '0x0017880104e43559'), new Endpoint(11, [0, 6], [], '0x0017880104e43559')], true, "Mains (single phase)", 'U202DST600ZB'),
|
||||
'3157100': new Device('Router', '0x0017880104e44559', 6542,4151, [new Endpoint(1, [], [], '0x0017880104e44559')], true, "Mains (single phase)", '3157100'),
|
||||
'zigfred_plus': zigfred_plus,
|
||||
'3157100': new Device('Router', '0x0017880104e44559', 6542,4151, [new Endpoint(1, [], [], '0x0017880104e44559')], true, "Mains (single phase)", '3157100', false, 'Centralite'),
|
||||
'J1': new Device('Router', '0x0017880104a44559', 6543,4151, [new Endpoint(1, [], [], '0x0017880104a44559')], true, "Mains (single phase)", 'J1 (5502)'),
|
||||
'TS0601_thermostat': TS0601_thermostat,
|
||||
'external_converter_device': new Device('EndDevice', '0x0017880104e45511', 1114, 'external', [new Endpoint(1, [], [], '0x0017880104e45511')], false, null, 'external_converter_device' ),
|
||||
@@ -197,7 +203,7 @@ const devices = {
|
||||
'temperature_sensor': new Device('EndDevice', '0x0017880104e45561', 6544,4151, [new Endpoint(1, [0,3,4,1026], [])], true, "Battery", "temperature.sensor"),
|
||||
'heating_actuator': new Device('Router', '0x0017880104e45562', 6545,4151, [new Endpoint(1, [0,3,4,513], [1026])], true, "Mains (single phase)", "heating.actuator"),
|
||||
'bj_scene_switch': new Device('EndDevice', '0xd85def11a1002caa', 50117, 4398, [new Endpoint(10, [0,4096], [3,4,5,6,8,25,768,4096], '0xd85def11a1002caa', [{target: bulb_color_2.endpoints[0], cluster: {ID: 8, name: 'genLevelCtrl'}}, {target: bulb_color_2.endpoints[0], cluster: {ID: 6, name: 'genOnOff'}}, {target: bulb_color_2.endpoints[0], cluster: {ID: 768, name: 'lightingColorCtrl'}},]), new Endpoint(11, [0,4096], [3,4,5,6,8,25,768,4096], '0xd85def11a1002caa')], true, 'Battery', 'RB01', false, 'Busch-Jaeger', '20161222', '1.2.0'),
|
||||
|
||||
'GW003-AS-IN-TE-FC': new Device('Router', '0x0017548104a44669', 6545,4699, [new Endpoint(1, [3], [0,3,513,514], '0x0017548104a44669')], true, "Mains (single phase)", 'Adapter Zigbee FUJITSU'),
|
||||
}
|
||||
|
||||
const mock = {
|
||||
@@ -207,8 +213,10 @@ const mock = {
|
||||
touchlinkScan: jest.fn(),
|
||||
touchlinkIdentify: jest.fn(),
|
||||
start: jest.fn(),
|
||||
backup: jest.fn(),
|
||||
isStopping: jest.fn(),
|
||||
permitJoin: jest.fn(),
|
||||
addInstallCode: jest.fn(),
|
||||
getCoordinatorVersion: jest.fn().mockReturnValue({type: 'z-Stack', meta: {version: 1, revision: 20190425}}),
|
||||
getNetworkParameters: jest.fn().mockReturnValue({panID: 0x162a, extendedPanID: [0, 11, 22], channel: 15}),
|
||||
on: (type, handler) => {
|
||||
|
||||
@@ -21,4 +21,7 @@
|
||||
"lib/**/*",
|
||||
"lib/util/settings.schema.json"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
],
|
||||
}
|
||||
Reference in New Issue
Block a user