mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-08-29 07:08:56 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5ca977579 | ||
|
|
1e1e0e4ca9 | ||
|
|
5f19d9cc82 | ||
|
|
e2ebe5e1ff | ||
|
|
a93838be15 | ||
|
|
044e8466ed | ||
|
|
c0bdf2e3a2 | ||
|
|
9321a04ecc | ||
|
|
184eef72b0 | ||
|
|
bb6c226d55 | ||
|
|
e892a54013 | ||
|
|
2c6176ff10 | ||
|
|
83b8fc9778 | ||
|
|
f0e9ebe4ed | ||
|
|
8902bd1d60 | ||
|
|
1857ac525b | ||
|
|
6427da244f | ||
|
|
339816c160 | ||
|
|
2a3670d3d7 | ||
|
|
889e972176 | ||
|
|
e213356c40 | ||
|
|
1cde620d70 | ||
|
|
ae14fa5cd6 | ||
|
|
672a0cac4f | ||
|
|
4ea55fe22a | ||
|
|
05769ce1a3 | ||
|
|
61116ec670 | ||
|
|
709046db2a | ||
|
|
bef8a682ca | ||
|
|
8f0794cb8f | ||
|
|
cd2fb54615 | ||
|
|
7893f9256f | ||
|
|
09f26a104c | ||
|
|
5ffae5a09a | ||
|
|
f7ceb62eec | ||
|
|
b8f689fe6e | ||
|
|
09e0fe8e3f | ||
|
|
fdf97180b0 | ||
|
|
5a9aef6d3b | ||
|
|
863230eaa3 | ||
|
|
cb0cc149a1 | ||
|
|
516bd704c6 | ||
|
|
46e75374b6 | ||
|
|
ee167e603a | ||
|
|
9b12933a99 | ||
|
|
ea7a4b1955 | ||
|
|
147a130628 | ||
|
|
1fda02ae7c | ||
|
|
4a664cf230 | ||
|
|
1a0baee788 | ||
|
|
1287a596f9 | ||
|
|
1862c8b035 | ||
|
|
8c848b8368 | ||
|
|
bdfabeb3da | ||
|
|
ec0ee53c7a | ||
|
|
14f5c4d3c9 | ||
|
|
f3e13f16f3 | ||
|
|
061285bd17 | ||
|
|
25448f823e | ||
|
|
99006895b5 | ||
|
|
26bed2d998 | ||
|
|
cc45413e2d | ||
|
|
ba77860489 | ||
|
|
14377c04a0 | ||
|
|
3238687f60 | ||
|
|
0632f83baf | ||
|
|
259a3417e2 | ||
|
|
a20ec7caa1 | ||
|
|
b08bf33ccf | ||
|
|
577bb0d12c | ||
|
|
92d88b680a | ||
|
|
00900958ef | ||
|
|
11a6e36a82 | ||
|
|
d6b9627146 | ||
|
|
15cad740ee | ||
|
|
ab14cca0ee | ||
|
|
87889c0688 | ||
|
|
17a6fff3ce | ||
|
|
f171d54e4b | ||
|
|
9faeb53ddb | ||
|
|
21a782d2c6 | ||
|
|
88cec5dfa6 | ||
|
|
a62f4289a7 | ||
|
|
1b5e010cd3 | ||
|
|
0f053425f3 | ||
|
|
04e68014da | ||
|
|
54dbb9732f | ||
|
|
8703847337 | ||
|
|
a85c68cca0 | ||
|
|
4592b00fe1 | ||
|
|
d9ddce5797 | ||
|
|
b3310c49ca | ||
|
|
781b542f92 | ||
|
|
43726d8bb1 | ||
|
|
8137b6f417 | ||
|
|
c439508129 | ||
|
|
e5233494af | ||
|
|
a2d378cc86 | ||
|
|
3d151298ae | ||
|
|
5554579683 | ||
|
|
ce2455510b | ||
|
|
bf526f6f50 | ||
|
|
004f4a5eb9 | ||
|
|
059e4719c6 | ||
|
|
a0278b7577 | ||
|
|
0f89053d05 | ||
|
|
e1ff6bdac5 | ||
|
|
73221f50f5 | ||
|
|
37405ee079 | ||
|
|
52fb19360e | ||
|
|
e47b672bcb | ||
|
|
63c88cba5b | ||
|
|
f0d0a62471 | ||
|
|
a497cf0457 | ||
|
|
ed5675960e | ||
|
|
ba5bf3c209 | ||
|
|
e40a3ba3bf | ||
|
|
ecf1b9ded4 | ||
|
|
314ab1b705 | ||
|
|
b6fda868ed | ||
|
|
443a14e40a | ||
|
|
c68790f2c4 | ||
|
|
c9d00875c0 | ||
|
|
0120284356 | ||
|
|
aa9e81d64d | ||
|
|
f4bf504642 | ||
|
|
5fc7760db6 | ||
|
|
ffa4fe35d9 | ||
|
|
778720b1b5 | ||
|
|
0826db07d0 | ||
|
|
7642b295e1 | ||
|
|
c9c599b60b | ||
|
|
c0a87ff520 | ||
|
|
4fbb6667ef | ||
|
|
1357b06185 | ||
|
|
6d16875a37 | ||
|
|
f7f9a1699b | ||
|
|
46b96640f3 | ||
|
|
51c91e685e | ||
|
|
8a6582739f | ||
|
|
c569840d6c | ||
|
|
2302240897 | ||
|
|
60349171d2 | ||
|
|
35f0def29f | ||
|
|
a512ca2bd5 | ||
|
|
96dcb93859 | ||
|
|
37d516d770 | ||
|
|
6c59bff223 | ||
|
|
f16616d3a6 | ||
|
|
959e7458a4 | ||
|
|
5828ee9989 | ||
|
|
054892641b | ||
|
|
c0326ec2bb | ||
|
|
189c0f360e | ||
|
|
dc70840c8a | ||
|
|
0b3c8df810 | ||
|
|
4405e71610 | ||
|
|
fd4a88acd0 | ||
|
|
3963a8d26f | ||
|
|
720addd906 | ||
|
|
4558bffd41 | ||
|
|
89c66094c6 | ||
|
|
ae08deb2fd | ||
|
|
ff02e54f25 | ||
|
|
963064dc22 | ||
|
|
0d9741e511 | ||
|
|
7d9c21ba24 | ||
|
|
b8744ce890 | ||
|
|
dd8ee60535 | ||
|
|
d5c6e39e7d | ||
|
|
588124b971 | ||
|
|
a7eea5ecad | ||
|
|
e13bdf1a31 | ||
|
|
30823a5a0c | ||
|
|
5bee8ccafe | ||
|
|
8ffd7653ac | ||
|
|
99a29293dd | ||
|
|
9d61184013 | ||
|
|
4f300aa72d | ||
|
|
cf7e0d1aa9 | ||
|
|
17874cfdc1 | ||
|
|
e12a371093 | ||
|
|
7e74b8c549 | ||
|
|
cdf64823ef | ||
|
|
5d195fc299 | ||
|
|
fd932926e7 | ||
|
|
38159a536c | ||
|
|
7e2ca5a97e | ||
|
|
b95dcd0f9b | ||
|
|
73f0f8fe43 | ||
|
|
16f3e884fc | ||
|
|
c5e6b9dd13 | ||
|
|
cae5d1a004 | ||
|
|
85375ffbae | ||
|
|
43fc66c436 | ||
|
|
5718cd0157 | ||
|
|
dd6e34a152 | ||
|
|
1ddf60bef0 | ||
|
|
6ff3954b71 | ||
|
|
6dd2926a1d | ||
|
|
5970e498f3 | ||
|
|
dcd81cbbf2 | ||
|
|
9e5fb04314 | ||
|
|
d324a91a5f | ||
|
|
fcfeac81e4 | ||
|
|
c44816efee | ||
|
|
86f19affa1 | ||
|
|
e21bfc1567 | ||
|
|
329c290505 | ||
|
|
b624a8ccad | ||
|
|
a43cd5a864 | ||
|
|
5f6b37bf83 | ||
|
|
653dfc7b6f | ||
|
|
aa87443c95 | ||
|
|
f5a5668359 | ||
|
|
5f2e93f64d | ||
|
|
7cd30a3a05 | ||
|
|
34a3e3c00b | ||
|
|
28e32884d9 | ||
|
|
dda2e064b2 | ||
|
|
9605e724e1 | ||
|
|
6912f8108d | ||
|
|
3c0e784afb | ||
|
|
be50d650a4 | ||
|
|
a5057ac101 | ||
|
|
72a74d7dc1 | ||
|
|
dccdc5aafa | ||
|
|
6dd60c09bf | ||
|
|
4ec905efca | ||
|
|
f62eabcb6d | ||
|
|
fd2cf41f0f | ||
|
|
755d101738 | ||
|
|
6fdd58be8a | ||
|
|
26083a62d9 | ||
|
|
30b35e7272 | ||
|
|
f6c01a02c9 | ||
|
|
35b9e6204d | ||
|
|
84f9d09b3f | ||
|
|
f260bf974b | ||
|
|
a2ea9c548f | ||
|
|
932a66a5c4 | ||
|
|
3d0541a32d | ||
|
|
3bb1f0f10b | ||
|
|
85b042ac25 | ||
|
|
f1fe1edb49 | ||
|
|
70d177485d | ||
|
|
fd44537984 | ||
|
|
dc20816043 | ||
|
|
c6d09df01a | ||
|
|
74cdde23b9 | ||
|
|
57aefe477d | ||
|
|
d5a3ce3871 | ||
|
|
92de1ca942 | ||
|
|
6f477c2923 | ||
|
|
5453c082c4 | ||
|
|
07fbc1b9a3 | ||
|
|
afeed4f372 | ||
|
|
2a197a4ba8 | ||
|
|
5e4e6d81de | ||
|
|
faede151f3 | ||
|
|
ea4aaf2cfa | ||
|
|
ac94b95bba | ||
|
|
708c497614 | ||
|
|
bd8aaea767 | ||
|
|
ac4ca88b8a | ||
|
|
8651fd7162 | ||
|
|
03ff4e6e5a | ||
|
|
3bc2ec8c79 | ||
|
|
c4804e574e | ||
|
|
5d3461c4c6 | ||
|
|
9380bbcadf | ||
|
|
3a0fe60b22 | ||
|
|
a1eea76dd4 | ||
|
|
69e805c882 | ||
|
|
0fe3a9b45c | ||
|
|
ecf64c8058 | ||
|
|
d3f705162d | ||
|
|
bd8282a41a | ||
|
|
82e87d563e | ||
|
|
d6c2b536ee | ||
|
|
919178840f | ||
|
|
84593a55ee | ||
|
|
1b8154bac1 | ||
|
|
8a133de1d1 | ||
|
|
75dddf462e | ||
|
|
91bbb0d4a8 | ||
|
|
d25d093a1a | ||
|
|
0bf91fd300 | ||
|
|
bf285681b4 | ||
|
|
cf9a5b7553 | ||
|
|
c2a0a6b2d1 | ||
|
|
eaff35421f | ||
|
|
ec20bfdd15 | ||
|
|
1f2d3ec850 | ||
|
|
d9b418cdde | ||
|
|
56ea61be3e | ||
|
|
b59afbd10d | ||
|
|
21fc5f0275 | ||
|
|
9d6f6186b0 | ||
|
|
590a897e9b | ||
|
|
753e8c3da7 | ||
|
|
ef57083ae9 | ||
|
|
1fa13f3f96 | ||
|
|
0fb84294f0 | ||
|
|
828df38e09 | ||
|
|
a13bd85ef3 | ||
|
|
65526e7f68 | ||
|
|
81d04d3dae | ||
|
|
16c222d3b3 | ||
|
|
0e61618935 | ||
|
|
bede1032e6 | ||
|
|
0786f22ce6 | ||
|
|
c9c4fad7df | ||
|
|
a52d3ea073 | ||
|
|
dcc410f6c5 | ||
|
|
c7743e6734 | ||
|
|
cc56141c39 | ||
|
|
60c2593669 | ||
|
|
db945c90be | ||
|
|
9df1065634 | ||
|
|
65f23171e6 | ||
|
|
920d1c91c7 | ||
|
|
698b40554e | ||
|
|
cb9beaa2ad | ||
|
|
d652856e90 | ||
|
|
7d09d065c5 | ||
|
|
352fa2db50 | ||
|
|
349ebad659 | ||
|
|
8fefa16fbc | ||
|
|
99b330e85e | ||
|
|
3f49e2e596 | ||
|
|
dcb75b19d3 | ||
|
|
c213a96133 | ||
|
|
78984a65ab | ||
|
|
6c059c3898 | ||
|
|
ac6eb19aff | ||
|
|
6df4e39f18 | ||
|
|
b0d3c2f964 | ||
|
|
f797a12250 | ||
|
|
081d3c3dfe | ||
|
|
7ca966ce39 | ||
|
|
fc664bdc68 | ||
|
|
2cb27b14f9 | ||
|
|
86a102e081 | ||
|
|
9119e54c2c | ||
|
|
f379bc0391 | ||
|
|
2e736bf501 | ||
|
|
1e7f985d4e | ||
|
|
61c4ff33e0 | ||
|
|
73ae48f387 | ||
|
|
0a1c60b2cf | ||
|
|
4eb56319c4 | ||
|
|
d8ea36af29 | ||
|
|
d0cf61bf10 | ||
|
|
ddb83cc6e4 | ||
|
|
e54de4bea7 | ||
|
|
97cc56b691 | ||
|
|
c9df5c8221 | ||
|
|
7f13e2c3e6 | ||
|
|
0aab565083 | ||
|
|
153f8bcfb5 | ||
|
|
c04ed81adb | ||
|
|
a1cf75456b | ||
|
|
b063e67e24 | ||
|
|
a4da482225 | ||
|
|
98302bebd1 | ||
|
|
2197328db5 | ||
|
|
c5b9176614 | ||
|
|
0322354c37 | ||
|
|
b692c94fe9 | ||
|
|
1496861811 | ||
|
|
5459152f01 | ||
|
|
1db926173d | ||
|
|
da9ee71d80 | ||
|
|
d9f02230a0 | ||
|
|
bf68f4575f | ||
|
|
33ea08ad22 | ||
|
|
3a17db7c79 | ||
|
|
e55252b9ff | ||
|
|
71d12c3c71 | ||
|
|
bc7b2bd5e0 | ||
|
|
50d58d3dff | ||
|
|
4180bd8933 | ||
|
|
172d11677b | ||
|
|
ab5537e18c | ||
|
|
b06e3b61f2 | ||
|
|
03d913defc | ||
|
|
e4bcac5411 | ||
|
|
5ed29d045b | ||
|
|
75acffa8c7 | ||
|
|
ade84dc6c0 | ||
|
|
7d49e136a6 | ||
|
|
0c1dba7208 | ||
|
|
63f4f681c4 | ||
|
|
1afbe05318 | ||
|
|
8376b7c5f6 | ||
|
|
b71a9920de | ||
|
|
2fd59cb7ef | ||
|
|
60e72c612c | ||
|
|
fbbc1fad73 | ||
|
|
d5de918247 | ||
|
|
f916ec5f4c | ||
|
|
469a01ebb6 | ||
|
|
6ede5a373e | ||
|
|
6e1af542e5 | ||
|
|
53051e9192 | ||
|
|
542a5f1564 | ||
|
|
b82f0e46aa | ||
|
|
f0f2dc847b | ||
|
|
8433571b1e | ||
|
|
2141d670bf | ||
|
|
fccb4de565 | ||
|
|
236afaacaf | ||
|
|
f463ca68a5 | ||
|
|
7502ba5312 | ||
|
|
c05b70b30b | ||
|
|
c22f8b36ba | ||
|
|
7276d116c2 | ||
|
|
1b7549dd73 | ||
|
|
7e4fff8454 | ||
|
|
ad43d61f28 | ||
|
|
ed5c52654f | ||
|
|
4f1b521bee | ||
|
|
a46f06a312 | ||
|
|
6311dfb0f1 | ||
|
|
ee82a885f7 | ||
|
|
88e6c10fb2 | ||
|
|
88524b7aed | ||
|
|
b2a27dd64a | ||
|
|
2a520b0e81 | ||
|
|
0c32f25203 | ||
|
|
1aa020875a | ||
|
|
9e5acdc94e | ||
|
|
417879b6a8 | ||
|
|
4ee7845b2a | ||
|
|
fb9600d55d | ||
|
|
47de3e20ae | ||
|
|
c00f091d7f | ||
|
|
b0b2bd2444 | ||
|
|
1e0c64caf5 | ||
|
|
db1fd6dd12 | ||
|
|
9ee0049248 | ||
|
|
7403fc7ce4 | ||
|
|
79d8cb69c0 |
+1
-2
@@ -1,2 +1 @@
|
||||
node_modules/*
|
||||
support/*
|
||||
node_modules/*
|
||||
+3
-1
@@ -1,10 +1,12 @@
|
||||
{
|
||||
"env": {
|
||||
"mocha": true,
|
||||
"es6": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": ["eslint:recommended", "google"],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6,
|
||||
"ecmaVersion": 2018,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
Unsupported device
|
||||
First check: https://github.com/Koenkk/zigbee2mqtt/wiki/How-to-support-new-devices
|
||||
Did you read the FAQ?
|
||||
https://www.zigbee2mqtt.io/information/FAQ.md
|
||||
|
||||
Unsupported device?
|
||||
https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html
|
||||
|
||||
Bug report
|
||||
A question?
|
||||
Read the documentation: https://www.zigbee2mqtt.io/
|
||||
|
||||
Bug report:
|
||||
- First make sure your on the latest version of zigbee2mqtt.
|
||||
- Make sure you are running the latest CC253X firmware from https://github.com/koenkk/z-stack-firmware.
|
||||
- Provide a clear description of the problem.
|
||||
- If applicable, provide steps how to reproduce the problem.
|
||||
- Provide a log with `log_level` set to `debug`, see https://github.com/Koenkk/zigbee2mqtt/wiki/Configuration
|
||||
- Provide a log with `log_level` set to `debug`, see https://www.zigbee2mqtt.io/configuration/configuration.html
|
||||
- DON'T copy logs directly here, post a link to https://hastebin.com/ or https://pastebin.com/.
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
daysUntilStale: 60
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 7
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- enhancement
|
||||
- bug
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: stale
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
|
||||
+2
-1
@@ -62,4 +62,5 @@ data/database.db
|
||||
data/config.json
|
||||
data/log*.txt
|
||||
data/state.json
|
||||
data/log
|
||||
data/log
|
||||
data-backup/
|
||||
+7
-3
@@ -6,19 +6,23 @@ services:
|
||||
- docker
|
||||
|
||||
node_js:
|
||||
- "8"
|
||||
- "10"
|
||||
|
||||
before_install:
|
||||
- "echo '{\"experimental\": true}' | sudo tee /etc/docker/daemon.json"
|
||||
- sudo apt update -y
|
||||
- sudo apt install --only-upgrade docker-ce -y
|
||||
- sudo service docker restart
|
||||
|
||||
install:
|
||||
- npm install
|
||||
|
||||
before_script:
|
||||
- npm test
|
||||
- npm run verify-homeassistant-mapping
|
||||
- npm run eslint
|
||||
|
||||
script:
|
||||
- ".travis/docker.sh"
|
||||
- ".travis/wiki.sh"
|
||||
- ".travis/trigger-downstream.sh"
|
||||
|
||||
env:
|
||||
|
||||
+7
-7
@@ -10,7 +10,7 @@ tag_push() {
|
||||
}
|
||||
|
||||
build_and_push() {
|
||||
docker build --build-arg COMMIT=$(git rev-parse --short HEAD) -t $DOCKER_USERNAME/zigbee2mqtt:$1 -f $2 .
|
||||
docker build --platform=$3 --build-arg COMMIT=$(git rev-parse --short HEAD) -t $DOCKER_USERNAME/zigbee2mqtt:$1 -f $2 .
|
||||
docker push $DOCKER_USERNAME/zigbee2mqtt:$1
|
||||
}
|
||||
|
||||
@@ -23,9 +23,9 @@ then
|
||||
login
|
||||
|
||||
# Push versioned images
|
||||
build_and_push "$TRAVIS_TAG" docker/Dockerfile.amd64
|
||||
build_and_push "$TRAVIS_TAG-arm32v6" docker/Dockerfile.arm32v6
|
||||
build_and_push "$TRAVIS_TAG-arm64v8" docker/Dockerfile.arm64v8
|
||||
build_and_push "$TRAVIS_TAG" docker/Dockerfile.amd64 amd64
|
||||
build_and_push "$TRAVIS_TAG-arm32v6" docker/Dockerfile.arm32v6 arm
|
||||
build_and_push "$TRAVIS_TAG-arm64v8" docker/Dockerfile.arm64v8 arm64
|
||||
|
||||
# Push latest images.
|
||||
tag_push "$TRAVIS_TAG-arm32v6" "arm32v6"
|
||||
@@ -35,9 +35,9 @@ elif [ "$TRAVIS_BRANCH" = "dev" -a "$TRAVIS_PULL_REQUEST" = "false" ]
|
||||
then
|
||||
echo "Updating docker images for dev branch!"
|
||||
login
|
||||
build_and_push latest-dev docker/Dockerfile.amd64
|
||||
build_and_push arm32v6-dev docker/Dockerfile.arm32v6
|
||||
build_and_push arm64v8-dev docker/Dockerfile.arm64v8
|
||||
build_and_push latest-dev docker/Dockerfile.amd64 amd64
|
||||
build_and_push arm32v6-dev docker/Dockerfile.arm32v6 arm
|
||||
build_and_push arm64v8-dev docker/Dockerfile.arm64v8 arm64
|
||||
else
|
||||
echo "Not updating docker images, triggered by pull request or not on master/dev branch"
|
||||
fi
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
#!/bin/sh -e
|
||||
|
||||
setup_git() {
|
||||
git config --global user.email "travis@travis-ci.org"
|
||||
git config --global user.name "Travis CI"
|
||||
}
|
||||
|
||||
update_wiki() {
|
||||
rm -rf zigbee2mqtt.wiki
|
||||
git clone https://${GH_TOKEN}@github.com/Koenkk/zigbee2mqtt.wiki.git
|
||||
npm run docgen zigbee2mqtt.wiki
|
||||
cd zigbee2mqtt.wiki
|
||||
git add -A
|
||||
git diff-index --quiet HEAD || git commit -m "Travis CI: update wiki"
|
||||
git push origin
|
||||
cd ..
|
||||
rm -rf zigbee2mqtt.wiki
|
||||
}
|
||||
|
||||
# Only update wiki if on master branch and not pull request
|
||||
if [ "$TRAVIS_BRANCH" = "master" -a "$TRAVIS_PULL_REQUEST" = "false" ]
|
||||
then
|
||||
echo "Updating wiki!"
|
||||
setup_git
|
||||
update_wiki
|
||||
else
|
||||
echo "Not updating wiki, triggered by pull request or not on master branch"
|
||||
fi
|
||||
@@ -1,44 +1,60 @@
|
||||
[](https://api.travis-ci.org/Koenkk/zigbee2mqtt.svg)
|
||||
<div align="center">
|
||||
<a href="https://github.com/koenkk/zigbee2mqtt">
|
||||
<img width="150" height="150" src="images/logo.png">
|
||||
</a>
|
||||
<br>
|
||||
<br>
|
||||
<div style="display: flex;">
|
||||
<a href="https://travis-ci.org/Koenkk/zigbee2mqtt/">
|
||||
<img src="https://img.shields.io/travis/Koenkk/zigbee2mqtt.svg">
|
||||
</a>
|
||||
<a href="https://github.com/Koenkk/zigbee2mqtt/releases">
|
||||
<img src="https://img.shields.io/github/release/koenkk/zigbee2mqtt.svg">
|
||||
</a>
|
||||
<a href="https://github.com/Koenkk/zigbee2mqtt/stargazers">
|
||||
<img src="https://img.shields.io/github/stars/koenkk/zigbee2mqtt.svg">
|
||||
</a>
|
||||
<a href="https://www.paypal.me/koenkk">
|
||||
<img src="https://img.shields.io/badge/donate-PayPal-blue.svg">
|
||||
</a>
|
||||
</div>
|
||||
<h1>Zigbee2mqtt 🌉 🐝</h1>
|
||||
<p>
|
||||
Allows you to use your Zigbee devices <b>without</b> the vendors bridge or gateway.
|
||||
</p>
|
||||
<p>
|
||||
It bridges events and allows you to control your Zigbee devices via MQTT. In this way you can integrate your Zigbee devices with whatever smart home infrastructure you are using.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
# zigbee2mqtt :bridge_at_night: :honeybee:
|
||||

|
||||
<img align="right" height="40px" src="https://user-images.githubusercontent.com/7738048/40915536-1993dd32-6805-11e8-9f22-4dfca52133c0.png">
|
||||
## [Getting started](https://www.zigbee2mqtt.io/#getting-started)
|
||||
The [documentation](https://www.zigbee2mqtt.io/) provides you all the information needed to get up and running! Make sure you don't skip sections if this is your first visit, as there might be important details in there for you.
|
||||
|
||||
Allows you to use your Zigbee devices **without** the vendors (Xiaomi/TRADFRI/Hue) bridge or gateway.
|
||||
If you aren't familiar with **Zigbee** terminology make sure you [read this](https://www.zigbee2mqtt.io/information/zigbee_network.html) to help you out.
|
||||
|
||||
It bridges events and allows you to control your Zigbee devices via MQTT. In this way you can integrate your Zigbee devices with whatever smart home infrastructure you are using.
|
||||
|
||||
## [Getting started](https://github.com/Koenkk/zigbee2mqtt/wiki/Getting-started)
|
||||
The [wiki](https://github.com/Koenkk/zigbee2mqtt/wiki) provides you all the information needed to get up and running! Make sure you don't skip sections if this is your first visit, as there might be important details in there for you.
|
||||
|
||||
[The documentation](https://github.com/Koenkk/zigbee2mqtt/wiki) will provide information on how to prepare, configure and start **zigbee2mqtt**, but also on how to customize the setup to fit your needs.
|
||||
|
||||
If you aren't familiar with **zigbee** terminology make sure you [read this](https://github.com/Koenkk/zigbee2mqtt/wiki/ZigBee-network) to help you out.
|
||||
|
||||
## Integrations
|
||||
## [Integrations](https://www.zigbee2mqtt.io/#integration)
|
||||
Zigbee2mqtt integrates well with (almost) every home automation solution because it uses MQTT. However the following integrations are worth mentioning:
|
||||
|
||||
<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/)
|
||||
- [Hassio](https://www.home-assistant.io/hassio/): Using [the official addon](https://github.com/danielwelch/hassio-zigbee2mqtt) from [danielwelch](https://github.com/danielwelch)
|
||||
- Generic install or Hassbian: Using instructions [here](https://github.com/Koenkk/zigbee2mqtt/wiki/Integrating-with-Home-Assistant)
|
||||
- Generic install or Hassbian: Using instructions [here](https://www.zigbee2mqtt.io/integration/home_assistant.html)
|
||||
|
||||
## Architecture
|
||||

|
||||
<img align="left" height="100px" width="100px" src="https://user-images.githubusercontent.com/2734836/47615848-b8dd8700-dabd-11e8-9d77-175002dd8987.png">
|
||||
|
||||
### [Domoticz](https://www.domoticz.com/)
|
||||
- Integration implemented in [domoticz-zigbee2mqtt-plugin](https://github.com/stas-demydiuk/domoticz-zigbee2mqtt-plugin)
|
||||
|
||||
<br>
|
||||
|
||||
## Architecture
|
||||

|
||||
|
||||
## Supported devices
|
||||
See [Supported devices](https://github.com/Koenkk/zigbee2mqtt/wiki/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/information/supported_devices.html) to check whether your device is supported. There is quite an extensive list, including devices from vendors like Xiaomi, Ikea, Philips, OSRAM and more.
|
||||
|
||||
If it's not listed in [Supported devices](https://github.com/Koenkk/zigbee2mqtt/wiki/Supported-devices), support can be added (fairly) easy, see [How to support new devices](https://github.com/Koenkk/zigbee2mqtt/wiki/How-to-support-new-devices).
|
||||
If it's not listed in [Supported devices](https://www.zigbee2mqtt.io/information/supported_devices.html), support can be added (fairly) easy, see [How to support new devices](https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html).
|
||||
|
||||
## Support & help
|
||||
If you need assistance you can check [opened issues](https://github.com/Koenkk/zigbee2mqtt/issues). Feel free to help with Pull Requests when you were able to fix things or add new devices or just share the love on social media.
|
||||
|
||||
## Contributors
|
||||
* [AndrewLinden](https://github.com/AndrewLinden)
|
||||
* [oskarn97](https://github.com/oskarn97)
|
||||
* [dgomes](https://github.com/dgomes)
|
||||
* [Koenkk](https://github.com/Koenk)
|
||||
* [kirovilya](https://github.com/kirovilya)
|
||||
* [ciotlosm](https://github.com/ciotlosm)
|
||||
If you need assistance you can check [opened issues](https://github.com/Koenkk/zigbee2mqtt/issues). Feel free to help with Pull Requests when you were able to fix things or add new devices or just share the love on social media.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.7
|
||||
FROM alpine:3.9
|
||||
|
||||
# Copy files
|
||||
ADD . /app
|
||||
@@ -12,7 +12,7 @@ ARG COMMIT
|
||||
RUN echo "{\"hash\": \"$COMMIT\"}" > .hash.json
|
||||
|
||||
# Install dependencies
|
||||
RUN apk add --update --no-cache make gcc g++ python linux-headers udev nodejs git && \
|
||||
RUN apk add --update --no-cache make gcc g++ python linux-headers udev nodejs npm git && \
|
||||
npm install --unsafe-perm && \
|
||||
apk del make gcc g++ python linux-headers udev git
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM arm32v6/alpine:3.7
|
||||
FROM arm32v6/alpine:3.9
|
||||
|
||||
# Setup cross-build
|
||||
ENV QEMU_EXECVE 1
|
||||
@@ -14,7 +14,7 @@ ADD . /app
|
||||
RUN [ "qemu-arm-static", "/bin/sh", "-c", \
|
||||
"cp /app/data/configuration.yaml /app && \
|
||||
cp /app/docker/run.sh /app && chmod +x /app/run.sh && \
|
||||
apk add --update --no-cache make gcc g++ python linux-headers udev nodejs git && \
|
||||
apk add --update --no-cache make gcc g++ python linux-headers udev nodejs npm git && \
|
||||
npm install --unsafe-perm && \
|
||||
apk del make gcc g++ python linux-headers udev git"]
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM arm64v8/alpine:3.7
|
||||
FROM arm64v8/alpine:3.9
|
||||
|
||||
# Setup cross-build
|
||||
ENV QEMU_EXECVE 1
|
||||
@@ -14,7 +14,7 @@ ADD . /app
|
||||
RUN [ "qemu-aarch64-static", "/bin/sh", "-c", \
|
||||
"cp /app/data/configuration.yaml /app && \
|
||||
cp /app/docker/run.sh /app && chmod +x /app/run.sh && \
|
||||
apk add --update --no-cache make gcc g++ python linux-headers udev nodejs git && \
|
||||
apk add --update --no-cache make gcc g++ python linux-headers udev nodejs npm git && \
|
||||
npm install --unsafe-perm && \
|
||||
apk del make gcc g++ python linux-headers udev git"]
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
version: '3'
|
||||
services:
|
||||
zigbee2mqtt:
|
||||
container_name: zigbee2mqtt
|
||||
image: koenkk/zigbee2mqtt
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
devices:
|
||||
# CC251
|
||||
#- /dev/ttyUSB_cc2531:/dev/ttyACM0
|
||||
# CC2530 / GBAN GB2530S
|
||||
#- /dev/ttyUSB_cc2530:/dev/ttyACM0
|
||||
restart: always
|
||||
network_mode: host
|
||||
@@ -0,0 +1 @@
|
||||
theme: jekyll-theme-cayman
|
||||
@@ -0,0 +1 @@
|
||||
Moved to [https://www.zigbee2mqtt.io](https://www.zigbee2mqtt.io)
|
||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 167 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
@@ -1,5 +1,12 @@
|
||||
const Controller = require('./lib/controller');
|
||||
const semver = require('semver');
|
||||
const engines = require('./package.json').engines;
|
||||
|
||||
const version = engines.node;
|
||||
if (!semver.satisfies(process.version, version)) {
|
||||
console.log(`\t\tZigbee2mqtt requires node version ${version}, you are running ${process.version}!\n`); // eslint-disable-line
|
||||
}
|
||||
|
||||
const Controller = require('./lib/controller');
|
||||
const controller = new Controller();
|
||||
controller.start();
|
||||
|
||||
|
||||
+233
-549
@@ -4,579 +4,180 @@ const State = require('./state');
|
||||
const logger = require('./util/logger');
|
||||
const settings = require('./util/settings');
|
||||
const zigbeeShepherdConverters = require('zigbee-shepherd-converters');
|
||||
const homeassistant = require('./homeassistant');
|
||||
const objectAssignDeep = require(`object-assign-deep`);
|
||||
const objectAssignDeep = require('object-assign-deep');
|
||||
|
||||
const mqttConfigRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/config/\\w+`, 'g');
|
||||
const mqttDeviceRegex = new RegExp(`${settings.get().mqtt.base_topic}/[\\w\\s\\d]+/set`, 'g');
|
||||
const mqttDevicePrefixRegex = new RegExp(`${settings.get().mqtt.base_topic}/[\\w\\s\\d]+/[\\w\\s\\d]+/set`, 'g');
|
||||
|
||||
const pollInterval = 60 * 1000; // seconds * 1000.
|
||||
const softResetTimeout = 3600 * 1000; // seconds * 1000.
|
||||
|
||||
const allowedLogLevels = ['error', 'warn', 'info', 'debug'];
|
||||
|
||||
/**
|
||||
* Home Assistant requires ALL attributes to be present in ALL MQTT messages send by the device.
|
||||
* https://community.home-assistant.io/t/missing-value-with-mqtt-only-last-data-set-is-shown/47070/9
|
||||
*
|
||||
* Therefore zigbee2mqtt BY DEFAULT caches all values and resend it with every message.
|
||||
* advanced.cache_state in configuration.yaml allows to configure this.
|
||||
* https://github.com/Koenkk/zigbee2mqtt/wiki/Configuration
|
||||
*/
|
||||
const cacheState = settings.get().advanced && settings.get().advanced.cache_state === false ? false : true;
|
||||
if (settings.get().homeassistant && !cacheState) {
|
||||
logger.warn('In order for Home Assistant integration to work properly set `cache_state: true');
|
||||
}
|
||||
// Extensions
|
||||
const ExtensionNetworkMap = require('./extension/networkMap');
|
||||
const ExtensionSoftReset = require('./extension/softReset');
|
||||
const ExtensionXiaomi = require('./extension/xiaomi');
|
||||
const ExtensionDevicePublish = require('./extension/devicePublish');
|
||||
const ExtensionHomeAssistant = require('./extension/homeassistant');
|
||||
const ExtensionDeviceConfigure = require('./extension/deviceConfigure');
|
||||
const ExtensionDeviceGroupMembership = require('./extension/deviceGroupMembership');
|
||||
const ExtensionDeviceReceive = require('./extension/deviceReceive');
|
||||
const ExtensionBridgeConfig = require('./extension/bridgeConfig');
|
||||
const ExtensionGroups = require('./extension/groups');
|
||||
const ExtensionDeviceAvailability = require('./extension/deviceAvailability');
|
||||
const ExtensionDeviceBind = require('./extension/deviceBind');
|
||||
const ExtensionDeviceReport = require('./extension/deviceReport');
|
||||
const ExtensionLivolo = require('./extension/livolo');
|
||||
|
||||
class Controller {
|
||||
constructor() {
|
||||
this.zigbee = new Zigbee();
|
||||
this.mqtt = new MQTT();
|
||||
this.state = new State();
|
||||
this.configured = [];
|
||||
this.handleZigbeeMessage = this.handleZigbeeMessage.bind(this);
|
||||
this.handleMQTTMessage = this.handleMQTTMessage.bind(this);
|
||||
|
||||
// Bind methods
|
||||
this.onMQTTConnected = this.onMQTTConnected.bind(this);
|
||||
this.onZigbeeMessage = this.onZigbeeMessage.bind(this);
|
||||
this.onMQTTMessage = this.onMQTTMessage.bind(this);
|
||||
this.publishEntityState = this.publishEntityState.bind(this);
|
||||
|
||||
// Initialize extensions.
|
||||
this.extensions = [
|
||||
new ExtensionDeviceReceive(this.zigbee, this.mqtt, this.state, this.publishEntityState),
|
||||
new ExtensionDeviceGroupMembership(this.zigbee, this.mqtt, this.publishEntityState),
|
||||
new ExtensionDeviceConfigure(this.zigbee, this.mqtt, this.state, this.publishEntityState),
|
||||
new ExtensionDevicePublish(this.zigbee, this.mqtt, this.state, this.publishEntityState),
|
||||
new ExtensionNetworkMap(this.zigbee, this.mqtt, this.state, this.publishEntityState),
|
||||
new ExtensionXiaomi(this.zigbee, this.mqtt, this.state, this.publishEntityState),
|
||||
new ExtensionBridgeConfig(this.zigbee, this.mqtt, this.state, this.publishEntityState),
|
||||
new ExtensionGroups(this.zigbee, this.mqtt, this.state, this.publishEntityState),
|
||||
new ExtensionDeviceBind(this.zigbee, this.mqtt, this.state, this.publishEntityState),
|
||||
|
||||
];
|
||||
|
||||
if (settings.get().advanced.report) {
|
||||
this.extensions.push(new ExtensionDeviceReport(
|
||||
this.zigbee, this.mqtt, this.state, this.publishEntityState
|
||||
));
|
||||
}
|
||||
|
||||
if (settings.get().homeassistant) {
|
||||
this.extensions.push(new ExtensionHomeAssistant(
|
||||
this.zigbee, this.mqtt, this.state, this.publishEntityState
|
||||
));
|
||||
}
|
||||
|
||||
if (settings.get().advanced.soft_reset_timeout !== 0) {
|
||||
this.extensions.push(new ExtensionSoftReset(
|
||||
this.zigbee, this.mqtt, this.state, this.publishEntityState
|
||||
));
|
||||
}
|
||||
|
||||
if (settings.get().advanced.availability_timeout) {
|
||||
this.extensions.push(new ExtensionDeviceAvailability(
|
||||
this.zigbee, this.mqtt, this.state, this.publishEntityState
|
||||
));
|
||||
}
|
||||
|
||||
if (settings.get().experimental.livolo) {
|
||||
// https://github.com/Koenkk/zigbee2mqtt/issues/592
|
||||
this.extensions.push(new ExtensionLivolo(
|
||||
this.zigbee, this.mqtt, this.state, this.publishEntityState
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
onMQTTConnected() {
|
||||
// Resend all cached states.
|
||||
this.sendAllCachedStates();
|
||||
|
||||
// Call extensions
|
||||
this.extensions.filter((e) => e.onMQTTConnected).forEach((e) => e.onMQTTConnected());
|
||||
}
|
||||
|
||||
onZigbeeStarted() {
|
||||
// Log zigbee clients on startup and configure.
|
||||
const devices = this.zigbee.getAllClients();
|
||||
logger.info(`Currently ${devices.length} devices are joined:`);
|
||||
devices.forEach((device) => {
|
||||
logger.info(this.getDeviceStartupLogMessage(device));
|
||||
});
|
||||
|
||||
// Enable zigbee join.
|
||||
if (settings.get().permit_join) {
|
||||
logger.warn('`permit_join` set to `true` in configuration.yaml.');
|
||||
logger.warn('Allowing new devices to join.');
|
||||
logger.warn('Set `permit_join` to `false` once you joined all devices.');
|
||||
}
|
||||
|
||||
this.zigbee.permitJoin(settings.get().permit_join);
|
||||
|
||||
// Connect to MQTT broker
|
||||
this.mqtt.connect(this.onMQTTMessage, this.onMQTTConnected);
|
||||
|
||||
// Call extensions
|
||||
this.extensions.filter((e) => e.onZigbeeStarted).forEach((e) => e.onZigbeeStarted());
|
||||
}
|
||||
|
||||
onZigbeeMessage(message) {
|
||||
// Variables
|
||||
let device = null;
|
||||
let mappedDevice = null;
|
||||
|
||||
// Check if message has a device
|
||||
if (message.endpoints && message.endpoints.length && message.endpoints[0].device) {
|
||||
device = message.endpoints[0].device;
|
||||
}
|
||||
|
||||
// Retrieve modelId from message
|
||||
if (device && device.modelId) {
|
||||
mappedDevice = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
|
||||
}
|
||||
|
||||
// Log
|
||||
logger.debug(
|
||||
`Received zigbee message of type '${message.type}' with data '${JSON.stringify(message.data)}'` +
|
||||
(device ? ` of device '${device.modelId}' (${device.ieeeAddr})` : '')
|
||||
);
|
||||
|
||||
// Call extensions.
|
||||
this.extensions
|
||||
.filter((e) => e.onZigbeeMessage)
|
||||
.forEach((e) => e.onZigbeeMessage(message, device, mappedDevice));
|
||||
}
|
||||
|
||||
onMQTTMessage(topic, message) {
|
||||
logger.debug(`Received MQTT message on '${topic}' with data '${message}'`);
|
||||
|
||||
// Call extensions
|
||||
const results = this.extensions
|
||||
.filter((e) => e.onMQTTMessage)
|
||||
.map((e) => e.onMQTTMessage(topic, message));
|
||||
|
||||
if (!results.includes(true)) {
|
||||
logger.warn(`Cannot handle MQTT message on '${topic}' with data '${message}'`);
|
||||
}
|
||||
}
|
||||
|
||||
start() {
|
||||
this.state.start();
|
||||
|
||||
this.startupLogVersion(() => {
|
||||
this.zigbee.start(this.handleZigbeeMessage, (error) => {
|
||||
this.zigbee.start(this.onZigbeeMessage, (error) => {
|
||||
if (error) {
|
||||
logger.error('Failed to start', error);
|
||||
logger.error('Exiting...');
|
||||
process.exit(1);
|
||||
} else {
|
||||
// Log zigbee clients on startup and configure.
|
||||
const devices = this.zigbee.getAllClients();
|
||||
logger.info(`Currently ${devices.length} devices are joined:`);
|
||||
devices.forEach((device) => {
|
||||
logger.info(this.getDeviceStartupLogMessage(device));
|
||||
this.configureDevice(device);
|
||||
});
|
||||
|
||||
// Enable zigbee join.
|
||||
if (settings.get().permit_join) {
|
||||
logger.warn('`permit_join` set to `true` in configuration.yaml.');
|
||||
logger.warn('Allowing new devices to join.');
|
||||
logger.warn('Set `permit_join` to `false` once you joined all devices.');
|
||||
this.zigbee.permitJoin(true);
|
||||
}
|
||||
|
||||
// Start timers.
|
||||
this.pollTimer(true);
|
||||
this.softResetTimeout(true);
|
||||
|
||||
// Connect to MQTT broker
|
||||
const subscriptions = [
|
||||
`${settings.get().mqtt.base_topic}/+/set`,
|
||||
`${settings.get().mqtt.base_topic}/+/+/set`,
|
||||
`${settings.get().mqtt.base_topic}/bridge/config/+`,
|
||||
];
|
||||
|
||||
if (settings.get().homeassistant) {
|
||||
subscriptions.push('hass/status');
|
||||
}
|
||||
|
||||
this.mqtt.connect(this.handleMQTTMessage, subscriptions, () => this.handleMQTTConnected());
|
||||
this.onZigbeeStarted();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
handleMQTTConnected() {
|
||||
// Home Assistant MQTT discovery on MQTT connected.
|
||||
if (settings.get().homeassistant) {
|
||||
// MQTT discovery of all paired devices on startup.
|
||||
this.zigbee.getAllClients().forEach((device) => {
|
||||
const mappedModel = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
|
||||
if (mappedModel) {
|
||||
homeassistant.discover(device.ieeeAddr, mappedModel.model, this.mqtt, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Resend all cached states.
|
||||
this.sendAllCachedStates();
|
||||
}
|
||||
|
||||
sendAllCachedStates() {
|
||||
this.zigbee.getAllClients().forEach((device) => {
|
||||
if (this.state.exists(device.ieeeAddr)) {
|
||||
this.mqttPublishDeviceState(device.ieeeAddr, this.state.get(device.ieeeAddr), false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
softResetTimeout(start) {
|
||||
if (this._softResetTimer) {
|
||||
clearTimeout(this._softResetTimer);
|
||||
this._softResetTimer = null;
|
||||
}
|
||||
|
||||
if (start) {
|
||||
this._softResetTimer = setTimeout(() => {
|
||||
this.zigbee.softReset((error) => {
|
||||
if (error) {
|
||||
logger.warn('Soft reset error', error);
|
||||
this.zigbee.stop((error) => {
|
||||
logger.warn('Zigbee stopped');
|
||||
this.zigbee.start(this.handleZigbeeMessage, (error) => {
|
||||
if (error) {
|
||||
logger.error('Failed to restart!');
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
logger.warn('Soft resetted zigbee');
|
||||
}
|
||||
|
||||
this.softResetTimeout(true);
|
||||
});
|
||||
}, softResetTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
pollTimer(start) {
|
||||
// Some routers need polling to prevent them from sleeping.
|
||||
if (start && !this._pollTimer) {
|
||||
this._pollTimer = setInterval(() => {
|
||||
const devices = this.zigbee.getAllClients().filter((d) => {
|
||||
const power = d.powerSource ? d.powerSource.toLowerCase().split(' ')[0] : 'unknown';
|
||||
return power !== 'battery' && power !== 'unknown' && d.type === 'Router';
|
||||
});
|
||||
|
||||
devices.forEach((d) => this.zigbee.ping(d.ieeeAddr));
|
||||
}, pollInterval);
|
||||
} else if (!start && this._pollTimer) {
|
||||
clearTimeout(this._pollTimer);
|
||||
this._pollTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
stop(callback) {
|
||||
this.state.save();
|
||||
// Call extensions
|
||||
this.extensions.filter((e) => e.stop).forEach((e) => e.stop());
|
||||
|
||||
// Wrap-up
|
||||
this.state.stop();
|
||||
this.mqtt.disconnect();
|
||||
this.pollTimer(false);
|
||||
this.softResetTimeout(false);
|
||||
this.zigbee.stop(callback);
|
||||
}
|
||||
|
||||
configureDevice(device) {
|
||||
let friendlyName = 'unknown';
|
||||
const ieeeAddr = device.ieeeAddr;
|
||||
if (settings.getDevice(ieeeAddr)) {
|
||||
friendlyName = settings.getDevice(ieeeAddr).friendly_name;
|
||||
}
|
||||
if (ieeeAddr && device.modelId && !this.configured.includes(ieeeAddr)) {
|
||||
const mappedModel = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
|
||||
|
||||
// Call configure function of device.
|
||||
if (mappedModel && mappedModel.configure) {
|
||||
mappedModel.configure(ieeeAddr, this.zigbee.shepherd, this.zigbee.getCoordinator(), (ok, msg) => {
|
||||
if (ok) {
|
||||
logger.info(`Succesfully configured ${friendlyName} ${ieeeAddr}`);
|
||||
} else {
|
||||
logger.error(`Failed to configure ${friendlyName} ${ieeeAddr}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Setup an OnAfIncomingMsg handler if needed.
|
||||
if (mappedModel && mappedModel.onAfIncomingMsg) {
|
||||
mappedModel.onAfIncomingMsg.forEach((ep) => this.zigbee.registerOnAfIncomingMsg(ieeeAddr, ep));
|
||||
}
|
||||
|
||||
this.configured.push(ieeeAddr);
|
||||
}
|
||||
}
|
||||
|
||||
getDeviceStartupLogMessage(device) {
|
||||
let friendlyName = 'unknown';
|
||||
let type = 'unknown';
|
||||
let friendlyDevice = {model: 'unkown', description: 'unknown'};
|
||||
const mappedModel = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
|
||||
if (mappedModel) {
|
||||
friendlyDevice = mappedModel;
|
||||
}
|
||||
|
||||
if (settings.getDevice(device.ieeeAddr)) {
|
||||
friendlyName = settings.getDevice(device.ieeeAddr).friendly_name;
|
||||
}
|
||||
|
||||
if (device.type) {
|
||||
type = device.type;
|
||||
}
|
||||
|
||||
return `${friendlyName} (${device.ieeeAddr}): ${friendlyDevice.model} - ` +
|
||||
`${friendlyDevice.vendor} ${friendlyDevice.description} (${type})`;
|
||||
}
|
||||
|
||||
handleZigbeeMessage(message) {
|
||||
// Zigbee message receieved, reset soft reset timeout.
|
||||
this.softResetTimeout(true);
|
||||
|
||||
logger.debug('Recieved zigbee message with data', JSON.stringify(message.data));
|
||||
if (message.type == 'devInterview' && !settings.getDevice(message.data)) {
|
||||
logger.info('Connecting with device...');
|
||||
this.mqtt.log('pairing', 'connecting with device');
|
||||
}
|
||||
|
||||
if (message.type == 'devIncoming') {
|
||||
logger.info('Device incoming...');
|
||||
this.mqtt.log('pairing', 'device incoming');
|
||||
}
|
||||
|
||||
// We dont handle messages without endpoints.
|
||||
if (!message.endpoints) {
|
||||
return;
|
||||
}
|
||||
|
||||
const device = message.endpoints[0].device;
|
||||
|
||||
if (!device) {
|
||||
logger.warn('Message without device!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is a new device.
|
||||
if (!settings.getDevice(device.ieeeAddr)) {
|
||||
logger.info(`New device with address ${device.ieeeAddr} connected!`);
|
||||
settings.addDevice(device.ieeeAddr);
|
||||
this.mqtt.log('device_connected', device.ieeeAddr);
|
||||
}
|
||||
|
||||
// We can't handle devices without modelId.
|
||||
if (!device.modelId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Map Zigbee modelID to vendor modelID.
|
||||
const modelID = message.endpoints[0].device.modelId;
|
||||
const mappedModel = zigbeeShepherdConverters.findByZigbeeModel(modelID);
|
||||
|
||||
if (!mappedModel) {
|
||||
logger.warn(`Device with modelID '${modelID}' is not supported.`);
|
||||
logger.warn(`Please see: https://github.com/Koenkk/zigbee2mqtt/wiki/How-to-support-new-devices`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Configure device.
|
||||
this.configureDevice(device);
|
||||
|
||||
// Home Assistant MQTT discovery
|
||||
if (settings.get().homeassistant) {
|
||||
homeassistant.discover(device.ieeeAddr, mappedModel.model, this.mqtt, false);
|
||||
}
|
||||
|
||||
// After this point we cant handle message withoud cid anymore.
|
||||
if (!message.data || (!message.data.cid && !message.data.cmdId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find a conveter for this message.
|
||||
const cid = message.data.cid;
|
||||
const cmdId = message.data.cmdId;
|
||||
const converters = mappedModel.fromZigbee.filter((c) => {
|
||||
if (cid) {
|
||||
return c.cid === cid && c.type === message.type;
|
||||
} else if (cmdId) {
|
||||
return c.cmd === cmdId;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (!converters.length) {
|
||||
if (cid) {
|
||||
logger.warn(
|
||||
`No converter available for '${mappedModel.model}' with cid '${cid}', ` +
|
||||
`type '${message.type}' and data '${JSON.stringify(message.data)}'`
|
||||
);
|
||||
} else if (cmdId) {
|
||||
logger.warn(
|
||||
`No converter available for '${mappedModel.model}' with cmd '${cmdId}' ` +
|
||||
`and data '${JSON.stringify(message.data)}'`
|
||||
);
|
||||
}
|
||||
|
||||
logger.warn(`Please see: https://github.com/Koenkk/zigbee2mqtt/wiki/How-to-support-new-devices.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert this Zigbee message to a MQTT message.
|
||||
// Get payload for the message.
|
||||
// - If a payload is returned publish it to the MQTT broker
|
||||
// - If NO payload is returned do nothing. This is for non-standard behaviour
|
||||
// for e.g. click switches where we need to count number of clicks and detect long presses.
|
||||
converters.forEach((converter) => {
|
||||
const publish = (payload) => {
|
||||
// Don't cache messages with following properties:
|
||||
const dontCacheProperties = ['click', 'action', 'button', 'button_left', 'button_right'];
|
||||
let cache = true;
|
||||
dontCacheProperties.forEach((property) => {
|
||||
if (payload.hasOwnProperty(property)) {
|
||||
cache = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Add device linkquality.
|
||||
if (message.linkquality) {
|
||||
payload.linkquality = message.linkquality;
|
||||
}
|
||||
|
||||
this.mqttPublishDeviceState(device.ieeeAddr, payload, cache);
|
||||
};
|
||||
|
||||
const payload = converter.convert(mappedModel, message, publish, settings.getDevice(device.ieeeAddr));
|
||||
|
||||
if (payload) {
|
||||
publish(payload);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleMQTTMessage(topic, message) {
|
||||
logger.debug(`Recieved mqtt message on topic '${topic}' with data '${message}'`);
|
||||
|
||||
if (topic.match(mqttConfigRegex)) {
|
||||
this.handleMQTTMessageConfig(topic, message);
|
||||
} else if (topic.match(mqttDeviceRegex) || topic.match(mqttDevicePrefixRegex)) {
|
||||
this.handleMQTTMessageDevice(topic, message, topic.match(mqttDevicePrefixRegex));
|
||||
} else if (topic === 'hass/status') {
|
||||
if (message.toString().toLowerCase() === 'online') {
|
||||
const timer = setTimeout(() => {
|
||||
this.sendAllCachedStates();
|
||||
clearTimeout(timer);
|
||||
}, 20000);
|
||||
}
|
||||
} else {
|
||||
logger.warn(`Cannot handle MQTT message with topic '${topic}' and message '${message}'`);
|
||||
}
|
||||
}
|
||||
|
||||
handleMQTTMessageConfig(topic, message) {
|
||||
const option = topic.split('/')[3];
|
||||
|
||||
if (option === 'permit_join') {
|
||||
this.zigbee.permitJoin(message.toString().toLowerCase() === 'true');
|
||||
} else if (option === 'log_level') {
|
||||
const level = message.toString().toLowerCase();
|
||||
if (allowedLogLevels.includes(level)) {
|
||||
logger.info(`Switching log level to '${level}'`);
|
||||
logger.transports.console.level = level;
|
||||
logger.transports.file.level = level;
|
||||
} else {
|
||||
logger.error(`Could not set log level to '${level}'. Allowed level: '${allowedLogLevels.join(',')}'`);
|
||||
}
|
||||
} else if (option === 'devices') {
|
||||
const devices = this.zigbee.getAllClients().map((device) => {
|
||||
const mappedModel = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
|
||||
const friendlyDevice = settings.getDevice(device.ieeeAddr);
|
||||
|
||||
return {
|
||||
ieeeAddr: device.ieeeAddr,
|
||||
type: device.type,
|
||||
model: mappedModel ? mappedModel.model : device.modelId,
|
||||
friendly_name: friendlyDevice ? friendlyDevice.friendly_name : device.ieeeAddr,
|
||||
};
|
||||
});
|
||||
|
||||
this.mqtt.log('devices', devices);
|
||||
} else if (option === 'remove') {
|
||||
message = message.toString();
|
||||
const IDByFriendlyName = settings.getIDByFriendlyName(message);
|
||||
const deviceID = IDByFriendlyName ? IDByFriendlyName : message;
|
||||
const device = this.zigbee.getDevice(deviceID);
|
||||
|
||||
const cleanup = () => {
|
||||
// Clear Home Assistant MQTT discovery message
|
||||
if (settings.get().homeassistant && device) {
|
||||
const mappedModel = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
|
||||
if (mappedModel) {
|
||||
homeassistant.clear(deviceID, mappedModel.model, this.mqtt);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from configuration.yaml
|
||||
settings.removeDevice(deviceID);
|
||||
|
||||
// Remove from state
|
||||
this.state.remove(deviceID);
|
||||
|
||||
logger.info(`Successfully removed ${deviceID}`);
|
||||
this.mqtt.log('device_removed', message);
|
||||
};
|
||||
|
||||
// Remove from zigbee network.
|
||||
if (device) {
|
||||
this.zigbee.removeDevice(deviceID, (error) => {
|
||||
if (!error) {
|
||||
cleanup();
|
||||
} else {
|
||||
logger.error(`Failed to remove ${deviceID}`);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
cleanup();
|
||||
}
|
||||
} else if (option === 'rename') {
|
||||
const invalid = `Invalid rename message format expected {old: 'friendly_name', new: 'new_name} ` +
|
||||
`got ${message.toString()}`;
|
||||
|
||||
let json = null;
|
||||
try {
|
||||
json = JSON.parse(message.toString());
|
||||
} catch (e) {
|
||||
logger.error(invalid);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate message
|
||||
if (!json.new || !json.old) {
|
||||
logger.error(invalid);
|
||||
return;
|
||||
}
|
||||
|
||||
if (settings.changeFriendlyName(json.old, json.new)) {
|
||||
logger.info(`Successfully renamed - ${json.old} to ${json.new} `);
|
||||
} else {
|
||||
logger.error(`Failed to renamed - ${json.old} to ${json.new}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Homeassistant rediscover
|
||||
if (settings.get().homeassistant) {
|
||||
const ID = settings.getIDByFriendlyName(json.new);
|
||||
const device = this.zigbee.getDevice(ID);
|
||||
const mappedModel = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
|
||||
if (mappedModel) {
|
||||
homeassistant.discover(device.ieeeAddr, mappedModel.model, this.mqtt, true);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.warn(`Cannot handle MQTT config option '${option}' with message '${message}'`);
|
||||
}
|
||||
}
|
||||
|
||||
handleMQTTMessageDevice(topic, message, withPrefix) {
|
||||
const friendlyName = topic.split('/')[1];
|
||||
const topicPrefix = withPrefix ? topic.split('/')[2] : '';
|
||||
|
||||
// Map friendlyName to deviceID.
|
||||
const deviceID = settings.getIDByFriendlyName(friendlyName);
|
||||
|
||||
if (!deviceID) {
|
||||
logger.error(`Cannot handle '${topic}' because deviceID of '${friendlyName}' cannot be found`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert the MQTT message to a Zigbee message.
|
||||
let json = null;
|
||||
try {
|
||||
json = JSON.parse(message);
|
||||
} catch (e) {
|
||||
// Cannot be parsed to JSON, assume state message.
|
||||
json = {state: message.toString()};
|
||||
}
|
||||
|
||||
// Find ep for this device
|
||||
const device = this.zigbee.getDevice(deviceID);
|
||||
if (!device) {
|
||||
logger.error(`Failed to find device with deviceID ${deviceID}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const mappedModel = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
|
||||
if (!mappedModel) {
|
||||
logger.warn(`Device with modelID '${device.modelId}' is not supported.`);
|
||||
logger.warn(`Please see: https://github.com/Koenkk/zigbee2mqtt/wiki/How-to-support-new-devices`);
|
||||
return;
|
||||
}
|
||||
|
||||
const ep = mappedModel.ep && mappedModel.ep[topicPrefix] ? mappedModel.ep[topicPrefix] : null;
|
||||
const published = [];
|
||||
|
||||
Object.keys(json).forEach((key) => {
|
||||
// Find converter for this key.
|
||||
const converter = mappedModel.toZigbee.find((c) => c.key === key);
|
||||
|
||||
if (!converter) {
|
||||
logger.error(`No converter available for '${key}' (${json[key]})`);
|
||||
return;
|
||||
}
|
||||
|
||||
const message = converter.convert(json[key], json);
|
||||
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
const callback = (error) => {
|
||||
// Devices do not report when they go off, this ensures state (on/off) is always in sync.
|
||||
if (!error && key.startsWith('state')) {
|
||||
const msg = {};
|
||||
const _key = topicPrefix ? `${key}_${topicPrefix}` : key;
|
||||
msg[_key] = json[key];
|
||||
this.mqttPublishDeviceState(deviceID, msg, true);
|
||||
}
|
||||
};
|
||||
|
||||
this.zigbee.publish(deviceID, message.cid, message.cmd, message.zclData, ep, callback);
|
||||
|
||||
published.push({message: message, converter: converter});
|
||||
});
|
||||
|
||||
/**
|
||||
* After publishing a command to a zigbee device we want to monitor the changed attribute(s) so that
|
||||
* everything stays in sync.
|
||||
*/
|
||||
published.forEach((p) => {
|
||||
let counter = 0;
|
||||
let secondsToMonitor = 1;
|
||||
|
||||
// In case of a transition we need to monitor for the whole transition time.
|
||||
if (p.message.zclData.hasOwnProperty('transtime')) {
|
||||
// Note that: transtime 10 = 0.1 seconds, 100 = 1 seconds, etc.
|
||||
secondsToMonitor = (p.message.zclData.transtime / 10) + 1;
|
||||
}
|
||||
|
||||
const timer = setInterval(() => {
|
||||
counter++;
|
||||
|
||||
// Doing a 'read' will result in the device sending a zigbee message with the current attribute value.
|
||||
// which will be handled by this.handleZigbeeMessage.
|
||||
p.converter.attr.forEach((attribute) => {
|
||||
this.zigbee.read(deviceID, p.message.cid, attribute, ep, () => null);
|
||||
});
|
||||
|
||||
if (counter >= secondsToMonitor) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
mqttPublishDeviceState(deviceID, payload, cache) {
|
||||
if (cacheState) {
|
||||
// Add cached state to payload
|
||||
if (this.state.exists(deviceID)) {
|
||||
payload = objectAssignDeep.noMutate(this.state.get(deviceID), payload);
|
||||
}
|
||||
|
||||
// Update state cache with new state.
|
||||
if (cache) {
|
||||
this.state.set(deviceID, payload);
|
||||
}
|
||||
}
|
||||
|
||||
const deviceSettings = settings.getDevice(deviceID);
|
||||
const options = {
|
||||
retain: deviceSettings.retain,
|
||||
qos: deviceSettings.qos ? deviceSettings.qos : 0,
|
||||
};
|
||||
|
||||
this.mqtt.publish(deviceSettings.friendly_name, JSON.stringify(payload), options);
|
||||
}
|
||||
|
||||
startupLogVersion(callback) {
|
||||
const git = require('git-last-commit');
|
||||
const packageJSON = require('../package.json');
|
||||
@@ -600,6 +201,89 @@ class Controller {
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
getDeviceStartupLogMessage(device) {
|
||||
let friendlyName = 'unknown';
|
||||
let type = 'unknown';
|
||||
let friendlyDevice = {model: 'unkown', description: 'unknown'};
|
||||
const mappedModel = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
|
||||
if (mappedModel) {
|
||||
friendlyDevice = mappedModel;
|
||||
}
|
||||
|
||||
if (settings.getDevice(device.ieeeAddr)) {
|
||||
friendlyName = settings.getDevice(device.ieeeAddr).friendly_name;
|
||||
}
|
||||
|
||||
if (device.type) {
|
||||
type = device.type;
|
||||
}
|
||||
|
||||
return `${friendlyName} (${device.ieeeAddr}): ${friendlyDevice.model} - ` +
|
||||
`${friendlyDevice.vendor} ${friendlyDevice.description} (${type})`;
|
||||
}
|
||||
|
||||
sendAllCachedStates() {
|
||||
this.zigbee.getAllClients().forEach((device) => {
|
||||
if (this.state.exists(device.ieeeAddr)) {
|
||||
this.publishEntityState(device.ieeeAddr, this.state.get(device.ieeeAddr), false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
publishEntityState(entityID, payload, cache) {
|
||||
const entity = settings.resolveEntity(entityID);
|
||||
const appSettings = settings.get();
|
||||
let messagePayload = {...payload};
|
||||
|
||||
if (appSettings.advanced.cache_state) {
|
||||
// Add cached state to payload
|
||||
if (this.state.exists(entityID)) {
|
||||
messagePayload = objectAssignDeep.noMutate(this.state.get(entityID), payload);
|
||||
}
|
||||
|
||||
// Update state cache with new state.
|
||||
if (cache) {
|
||||
this.state.set(entityID, messagePayload);
|
||||
}
|
||||
}
|
||||
|
||||
const entitySettings = entity.type === 'device' ? settings.getDevice(entityID) : settings.getGroup(entityID);
|
||||
const options = {
|
||||
retain: entitySettings ? entitySettings.retain : false,
|
||||
qos: entitySettings && entitySettings.qos ? entitySettings.qos : 0,
|
||||
};
|
||||
|
||||
if (entity.type === 'device' && appSettings.mqtt.include_device_information) {
|
||||
messagePayload.device = this.getDeviceInfoForMqtt(entityID);
|
||||
}
|
||||
|
||||
if (settings.get().experimental.output === 'json') {
|
||||
this.mqtt.publish(entity.friendlyName, JSON.stringify(messagePayload), options);
|
||||
} else if (settings.get().experimental.output === 'attribute') {
|
||||
Object.keys(messagePayload).forEach((key) => {
|
||||
this.mqtt.publish(`${entity.friendlyName}/${key}`, JSON.stringify(messagePayload[key]), options);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getDeviceInfoForMqtt(ieeeAddr) {
|
||||
const device = this.zigbee.getDevice(ieeeAddr);
|
||||
const {type, nwkAddr, manufId, manufName, powerSource, modelId, status} = device;
|
||||
const deviceSettings = settings.getDevice(device.ieeeAddr);
|
||||
|
||||
return {
|
||||
ieeeAddr,
|
||||
friendlyName: deviceSettings ? (deviceSettings.friendly_name || '') : '',
|
||||
type,
|
||||
nwkAddr,
|
||||
manufId,
|
||||
manufName,
|
||||
powerSource,
|
||||
modelId,
|
||||
status,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Controller;
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
const settings = require('../util/settings');
|
||||
const logger = require('../util/logger');
|
||||
const zigbeeShepherdConverters = require('zigbee-shepherd-converters');
|
||||
|
||||
const configRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/config/\\w+`, 'g');
|
||||
const allowedLogLevels = ['error', 'warn', 'info', 'debug'];
|
||||
|
||||
class BridgeConfig {
|
||||
constructor(zigbee, mqtt, state, publishEntityState) {
|
||||
this.zigbee = zigbee;
|
||||
this.mqtt = mqtt;
|
||||
this.state = state;
|
||||
this.publishEntityState = publishEntityState;
|
||||
|
||||
// Bind functions
|
||||
this.permitJoin = this.permitJoin.bind(this);
|
||||
this.lastSeen = this.lastSeen.bind(this);
|
||||
this.reset = this.reset.bind(this);
|
||||
this.logLevel = this.logLevel.bind(this);
|
||||
this.devices = this.devices.bind(this);
|
||||
this.rename = this.rename.bind(this);
|
||||
this.remove = this.remove.bind(this);
|
||||
this.ban = this.ban.bind(this);
|
||||
this.deviceOptions = this.deviceOptions.bind(this);
|
||||
|
||||
// Set supported options
|
||||
this.supportedOptions = {
|
||||
'permit_join': this.permitJoin,
|
||||
'last_seen': this.lastSeen,
|
||||
'reset': this.reset,
|
||||
'log_level': this.logLevel,
|
||||
'devices': this.devices,
|
||||
'rename': this.rename,
|
||||
'remove': this.remove,
|
||||
'ban': this.ban,
|
||||
'device_options': this.deviceOptions,
|
||||
};
|
||||
}
|
||||
|
||||
deviceOptions(topic, message) {
|
||||
let json = null;
|
||||
try {
|
||||
json = JSON.parse(message.toString());
|
||||
} catch (e) {
|
||||
logger.error('Failed to parse message as JSON');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json.hasOwnProperty('friendly_name') || !json.hasOwnProperty('options')) {
|
||||
logger.error('Invalid JSON message, should contain "friendly_name" and "options"');
|
||||
return;
|
||||
}
|
||||
|
||||
const ieeeAddr = settings.getIeeeAddrByFriendlyName(json.friendly_name);
|
||||
if (!ieeeAddr) {
|
||||
logger.error(`Failed to find device '${json.friendly_name}'`);
|
||||
return;
|
||||
}
|
||||
|
||||
settings.changeDeviceOptions(ieeeAddr, json.options);
|
||||
logger.info(`Changed device specific options of '${json.friendly_name}' (${JSON.stringify(json.options)})`);
|
||||
}
|
||||
|
||||
permitJoin(topic, message) {
|
||||
this.zigbee.permitJoin(message.toString().toLowerCase() === 'true', () => this.publish());
|
||||
}
|
||||
|
||||
reset(topic, message) {
|
||||
this.zigbee.softReset((error) => {
|
||||
if (error) {
|
||||
logger.error('Soft reset failed');
|
||||
} else {
|
||||
logger.info('Soft resetted ZNP');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
lastSeen(topic, message) {
|
||||
const allowed = ['disable', 'ISO_8601', 'epoch', 'ISO_8601_local'];
|
||||
message = message.toString();
|
||||
|
||||
if (!allowed.includes(message)) {
|
||||
logger.error(`${message} is not an allowed value, possible: ${allowed}`);
|
||||
return;
|
||||
}
|
||||
|
||||
settings.set(['advanced', 'last_seen'], message);
|
||||
logger.info(`Set last_seen to ${message}`);
|
||||
}
|
||||
|
||||
logLevel(topic, message) {
|
||||
const level = message.toString().toLowerCase();
|
||||
if (allowedLogLevels.includes(level)) {
|
||||
logger.info(`Switching log level to '${level}'`);
|
||||
logger.transports.console.level = level;
|
||||
logger.transports.file.level = level;
|
||||
} else {
|
||||
logger.error(`Could not set log level to '${level}'. Allowed level: '${allowedLogLevels.join(',')}'`);
|
||||
}
|
||||
|
||||
this.publish();
|
||||
}
|
||||
|
||||
devices(topic, message) {
|
||||
const devices = this.zigbee.getAllClients().map((device) => {
|
||||
const mappedDevice = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
|
||||
const friendlyDevice = settings.getDevice(device.ieeeAddr);
|
||||
|
||||
return {
|
||||
ieeeAddr: device.ieeeAddr,
|
||||
type: device.type,
|
||||
model: mappedDevice ? mappedDevice.model : device.modelId,
|
||||
friendly_name: friendlyDevice ? friendlyDevice.friendly_name : device.ieeeAddr,
|
||||
};
|
||||
});
|
||||
|
||||
this.mqtt.log('devices', devices);
|
||||
}
|
||||
|
||||
rename(topic, message) {
|
||||
const invalid = `Invalid rename message format expected {old: 'friendly_name', new: 'new_name} ` +
|
||||
`got ${message.toString()}`;
|
||||
|
||||
let json = null;
|
||||
try {
|
||||
json = JSON.parse(message.toString());
|
||||
} catch (e) {
|
||||
logger.error(invalid);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate message
|
||||
if (!json.new || !json.old) {
|
||||
logger.error(invalid);
|
||||
return;
|
||||
}
|
||||
|
||||
if (settings.changeFriendlyName(json.old, json.new)) {
|
||||
logger.info(`Successfully renamed - ${json.old} to ${json.new} `);
|
||||
this.mqtt.log('device_renamed', {from: json.old, to: json.new});
|
||||
} else {
|
||||
logger.error(`Failed to renamed - ${json.old} to ${json.new}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
remove(topic, message) {
|
||||
this.removeOrBan(false, message);
|
||||
}
|
||||
|
||||
ban(topic, message) {
|
||||
this.removeOrBan(true, message);
|
||||
}
|
||||
|
||||
removeOrBan(ban, message) {
|
||||
message = message.toString();
|
||||
const IDByFriendlyName = settings.getIeeeAddrByFriendlyName(message);
|
||||
const deviceID = IDByFriendlyName ? IDByFriendlyName : message;
|
||||
const device = this.zigbee.getDevice(deviceID);
|
||||
|
||||
const cleanup = () => {
|
||||
// Remove from configuration.yaml
|
||||
settings.removeDevice(deviceID);
|
||||
|
||||
// Remove from state
|
||||
this.state.remove(deviceID);
|
||||
|
||||
logger.info(`Successfully ${ban ? 'banned' : 'removed'} ${deviceID}`);
|
||||
this.mqtt.log(ban ? 'device_banned' : 'device_removed', message);
|
||||
};
|
||||
|
||||
// Remove from zigbee network.
|
||||
if (device) {
|
||||
this.zigbee.removeDevice(deviceID, ban, (error) => {
|
||||
if (!error) {
|
||||
cleanup();
|
||||
} else {
|
||||
logger.error(`Failed to ${ban ? 'ban' : 'remove'} ${deviceID}`);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
onMQTTConnected() {
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/config/+`);
|
||||
this.publish();
|
||||
}
|
||||
|
||||
onMQTTMessage(topic, message) {
|
||||
if (!topic.match(configRegex)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const option = topic.split('/').slice(-1)[0];
|
||||
|
||||
if (!this.supportedOptions.hasOwnProperty(option)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.supportedOptions[option](topic, message);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
publish() {
|
||||
const topic = `bridge/config`;
|
||||
const payload = {
|
||||
log_level: logger.transports.console.level,
|
||||
permit_join: this.zigbee.getPermitJoin(),
|
||||
};
|
||||
|
||||
this.mqtt.publish(topic, JSON.stringify(payload), {retain: true, qos: 0}, null);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BridgeConfig;
|
||||
@@ -0,0 +1,156 @@
|
||||
const logger = require('../util/logger');
|
||||
const settings = require('../util/settings');
|
||||
const utils = require('../util/utils');
|
||||
const zigbeeShepherdConverters = require('zigbee-shepherd-converters');
|
||||
|
||||
// Some EndDevices should be pinged
|
||||
// e.g. E11-G13 https://github.com/Koenkk/zigbee2mqtt/issues/775#issuecomment-453683846
|
||||
const pingableDevices = [
|
||||
zigbeeShepherdConverters.devices.find((d) => d.model === 'E11-G13'),
|
||||
];
|
||||
|
||||
const toZigbeeCandidates = ['state'];
|
||||
|
||||
/**
|
||||
* This extensions pings devices to check if they are online.
|
||||
*/
|
||||
class DeviceAvailability {
|
||||
constructor(zigbee, mqtt, state, publishEntityState) {
|
||||
this.zigbee = zigbee;
|
||||
this.mqtt = mqtt;
|
||||
this.availability_timeout = settings.get().advanced.availability_timeout;
|
||||
this.timers = {};
|
||||
this.pending = [];
|
||||
this.state = {};
|
||||
|
||||
// Initialize blacklist
|
||||
this.blacklist = settings.get().advanced.availability_blacklist.map((e) => {
|
||||
return settings.getIeeeAddrByFriendlyName(e) || e;
|
||||
});
|
||||
}
|
||||
|
||||
isPingable(device) {
|
||||
if (this.blacklist.includes(device.ieeeAddr)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pingableDevices.find((d) => d.zigbeeModel.includes(device.modelId))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return device.type === 'Router' && (device.powerSource && device.powerSource !== 'Battery');
|
||||
}
|
||||
|
||||
getAllPingableDevices() {
|
||||
return this.zigbee.getAllClients().filter((d) => this.isPingable(d));
|
||||
}
|
||||
|
||||
onMQTTConnected() {
|
||||
// As some devices are not checked for availability (e.g. battery powered devices)
|
||||
// we mark all device as online by default.
|
||||
this.zigbee.getDevices()
|
||||
.filter((d) => d.type !== 'Coordinator')
|
||||
.forEach((device) => this.publishAvailability(device.ieeeAddr, true));
|
||||
|
||||
// Start timers for all devices
|
||||
this.getAllPingableDevices().forEach((device) => this.setTimer(device.ieeeAddr));
|
||||
}
|
||||
|
||||
handleInterval(ieeeAddr) {
|
||||
// Check if a job is already pending.
|
||||
// This avoids overflowing of the queue in case the queue is not able to catch-up with the jobs being added.
|
||||
if (this.pending.includes(ieeeAddr)) {
|
||||
logger.debug(`Skipping ping for ${ieeeAddr} becuase job is already in queue`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.pending.push(ieeeAddr);
|
||||
|
||||
// When a device is already unavailable, log the ping failed on 'debug' instead of 'error'.
|
||||
const errorLogLevel = this.state.hasOwnProperty(ieeeAddr) && !this.state[ieeeAddr] ? 'debug' : 'error';
|
||||
|
||||
this.zigbee.ping(ieeeAddr, errorLogLevel, (error) => {
|
||||
this.publishAvailability(ieeeAddr, !error);
|
||||
|
||||
// Remove from pending jobs.
|
||||
const index = this.pending.indexOf(ieeeAddr);
|
||||
if (index !== -1) {
|
||||
this.pending.splice(index, 1);
|
||||
}
|
||||
|
||||
this.setTimer(ieeeAddr);
|
||||
});
|
||||
}
|
||||
|
||||
setTimer(ieeeAddr) {
|
||||
if (this.timers[ieeeAddr]) {
|
||||
clearTimeout(this.timers[ieeeAddr]);
|
||||
}
|
||||
|
||||
this.timers[ieeeAddr] = setTimeout(() => {
|
||||
this.handleInterval(ieeeAddr);
|
||||
}, utils.secondsToMilliseconds(this.availability_timeout));
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.zigbee.getDevices()
|
||||
.filter((d) => d.type !== 'Coordinator')
|
||||
.forEach((device) => this.publishAvailability(device.ieeeAddr, false));
|
||||
}
|
||||
|
||||
onReconnect(ieeeAddr) {
|
||||
const device = this.zigbee.getDevice(ieeeAddr);
|
||||
let mappedDevice = null;
|
||||
|
||||
if (device && device.modelId) {
|
||||
mappedDevice = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
|
||||
}
|
||||
|
||||
if (mappedDevice) {
|
||||
const converters = mappedDevice.toZigbee.filter((tz) => {
|
||||
return tz.key.find((k) => toZigbeeCandidates.includes(k));
|
||||
});
|
||||
|
||||
converters.forEach((converter) => {
|
||||
const converted = converter.convert(null, null, null, 'get');
|
||||
if (converted) {
|
||||
this.zigbee.publish(
|
||||
ieeeAddr, 'device', converted.cid, converted.cmd, converted.cmdType,
|
||||
converted.zclData, converted.cfg, null, () => {}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
publishAvailability(ieeeAddr, available) {
|
||||
if (this.state.hasOwnProperty(ieeeAddr) && !this.state[ieeeAddr] && available) {
|
||||
this.onReconnect(ieeeAddr);
|
||||
}
|
||||
|
||||
this.state[ieeeAddr] = available;
|
||||
const deviceSettings = settings.getDevice(ieeeAddr);
|
||||
const name = deviceSettings ? deviceSettings.friendly_name : ieeeAddr;
|
||||
const topic = `${name}/availability`;
|
||||
const payload = available ? 'online' : 'offline';
|
||||
this.mqtt.publish(topic, payload, {retain: true, qos: 0});
|
||||
}
|
||||
|
||||
onZigbeeMessage(message, device, mappedDevice) {
|
||||
// When a zigbee message from a device is received we know the device is still alive.
|
||||
// => reset the timer.
|
||||
if (device && this.isPingable(this.zigbee.getDevice(device.ieeeAddr))) {
|
||||
// When a message is received and the device is marked as offline, mark it online.
|
||||
if (this.state.hasOwnProperty(device.ieeeAddr) && !this.state[device.ieeeAddr]) {
|
||||
this.publishAvailability(device.ieeeAddr, true);
|
||||
} else if (!this.state.hasOwnProperty(device.ieeeAddr)) {
|
||||
// A new device has been connected
|
||||
this.publishAvailability(device.ieeeAddr, true);
|
||||
}
|
||||
|
||||
this.setTimer(device.ieeeAddr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DeviceAvailability;
|
||||
@@ -0,0 +1,127 @@
|
||||
const settings = require('../util/settings');
|
||||
const logger = require('../util/logger');
|
||||
const Queue = require('queue');
|
||||
|
||||
const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/(bind|unbind)/.+$`);
|
||||
|
||||
const allowedClusters = [
|
||||
5, // genScenes
|
||||
6, // genOnOff
|
||||
8, // genLevelCtrl
|
||||
768, // lightingColorCtrl
|
||||
];
|
||||
|
||||
class DeviceBind {
|
||||
constructor(zigbee, mqtt, state, publishEntityState) {
|
||||
this.zigbee = zigbee;
|
||||
this.mqtt = mqtt;
|
||||
this.state = state;
|
||||
this.publishEntityState = publishEntityState;
|
||||
|
||||
// Setup queue
|
||||
this.queue = new Queue();
|
||||
this.queue.concurrency = 1;
|
||||
this.queue.autostart = true;
|
||||
}
|
||||
|
||||
onMQTTConnected() {
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/bind/+`);
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/unbind/+`);
|
||||
}
|
||||
|
||||
parseTopic(topic) {
|
||||
if (!topic.match(topicRegex)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Remove base from topic
|
||||
topic = topic.replace(`${settings.get().mqtt.base_topic}/bridge/`, '');
|
||||
|
||||
// Parse type from topic
|
||||
const type = topic.split('/')[0];
|
||||
|
||||
// Remove type from topic
|
||||
topic = topic.replace(`${type}/`, '');
|
||||
|
||||
return {ID: topic, type};
|
||||
}
|
||||
|
||||
onMQTTMessage(topic, message) {
|
||||
topic = this.parseTopic(topic);
|
||||
|
||||
if (!topic) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Find source; can only be a device.
|
||||
const sourceEntity = settings.resolveEntity(topic.ID);
|
||||
const source = this.zigbee.getEndpoint(sourceEntity.ID);
|
||||
|
||||
if (!source) {
|
||||
logger.error(`Failed to find device '${sourceEntity.ID}'`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Find target; can be a device or group.
|
||||
const targetEntity = settings.resolveEntity(message.toString());
|
||||
let target = null;
|
||||
|
||||
if (targetEntity.type === 'device') {
|
||||
target = this.zigbee.getEndpoint(targetEntity.ID);
|
||||
|
||||
if (!target) {
|
||||
logger.error(`Failed to find target device '${targetEntity.ID}'`);
|
||||
return false;
|
||||
}
|
||||
} else if (targetEntity.type === 'group') {
|
||||
target = targetEntity.ID;
|
||||
}
|
||||
|
||||
// Find which clusters are supported by both the source and target.
|
||||
// Groups are assumed to support all clusters (as we don't know which devices are in)
|
||||
let supported = [];
|
||||
if (targetEntity.type === 'device') {
|
||||
supported = target.getSimpleDesc().inClusterList.filter((cluster) => {
|
||||
return allowedClusters.includes(cluster);
|
||||
});
|
||||
} else if (targetEntity.type === 'group') {
|
||||
supported = allowedClusters;
|
||||
}
|
||||
|
||||
const clusters = source.getSimpleDesc().outClusterList.filter((cluster) => {
|
||||
return supported.includes(cluster);
|
||||
});
|
||||
|
||||
// Bind
|
||||
clusters.forEach((cluster) => {
|
||||
this.queue.push((queueCallback) => {
|
||||
logger.debug(`${topic.type}ing cluster '${cluster}' from ${sourceEntity.ID}' to '${targetEntity.ID}'`);
|
||||
|
||||
source[topic.type](cluster, target, (error) => {
|
||||
if (error) {
|
||||
logger.error(
|
||||
`Failed to ${topic.type} cluster '${cluster}' from ${sourceEntity.ID}' to ` +
|
||||
`'${targetEntity.ID}' (${error})`
|
||||
);
|
||||
} else {
|
||||
logger.info(
|
||||
`Successfully ${topic.type === 'bind' ? 'bound' : 'unbound'} cluster '${cluster}' from ` +
|
||||
`${sourceEntity.ID}' to '${targetEntity.ID}'`
|
||||
);
|
||||
|
||||
this.mqtt.log(
|
||||
`device_${topic.type}`,
|
||||
{from: sourceEntity.ID, to: targetEntity.ID, cluster}
|
||||
);
|
||||
}
|
||||
|
||||
queueCallback();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DeviceBind;
|
||||
@@ -0,0 +1,89 @@
|
||||
const settings = require('../util/settings');
|
||||
const logger = require('../util/logger');
|
||||
const zigbeeShepherdConverters = require('zigbee-shepherd-converters');
|
||||
const Queue = require('queue');
|
||||
|
||||
/**
|
||||
* This extensions handles configuration of devices.
|
||||
*/
|
||||
class DeviceConfigure {
|
||||
constructor(zigbee, mqtt, state, publishEntityState) {
|
||||
this.zigbee = zigbee;
|
||||
this.configured = {};
|
||||
this.attempts = {};
|
||||
|
||||
this.queue = new Queue();
|
||||
this.queue.concurrency = 1;
|
||||
this.queue.autostart = true;
|
||||
}
|
||||
|
||||
onZigbeeStarted() {
|
||||
this.zigbee.getAllClients().forEach((device) => {
|
||||
const mappedDevice = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
|
||||
|
||||
if (mappedDevice) {
|
||||
this.configure(device.ieeeAddr, mappedDevice);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onZigbeeMessage(message, device, mappedDevice) {
|
||||
if (device && mappedDevice) {
|
||||
const ieeeAddr = device.ieeeAddr;
|
||||
if (!this.configured[ieeeAddr]) {
|
||||
this.configure(ieeeAddr, mappedDevice);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
configure(ieeeAddr, mappedDevice) {
|
||||
if (!this.attempts.hasOwnProperty(ieeeAddr)) {
|
||||
this.attempts[ieeeAddr] = 0;
|
||||
}
|
||||
|
||||
if (mappedDevice.configure) {
|
||||
const friendlyName = settings.getDevice(ieeeAddr) ? settings.getDevice(ieeeAddr).friendly_name : 'unknown';
|
||||
// mark device as currently being configured to prevent race conditions
|
||||
this.configured[ieeeAddr] = 'configuring';
|
||||
|
||||
this.queue.push((queueCallback) => {
|
||||
logger.debug(`Configuring ${friendlyName} (${ieeeAddr}) ...`);
|
||||
|
||||
// Call configure function of this device.
|
||||
mappedDevice.configure(ieeeAddr, this.zigbee.shepherd, this.zigbee.getCoordinator(), (ok, msg) => {
|
||||
if (ok) {
|
||||
logger.info(`Successfully configured ${friendlyName} (${ieeeAddr})`);
|
||||
this.configured[ieeeAddr] = 'configured';
|
||||
} else {
|
||||
// If a device is not joined in the network the configure command may fail
|
||||
// (especially, immediately after zigbee start)
|
||||
// also, this command may fail if the network is busy or lost data packets
|
||||
if (this.attempts[ieeeAddr] > 1) {
|
||||
// Give up after 3 attempts.
|
||||
this.configured[ieeeAddr] = 'failed';
|
||||
} else {
|
||||
// we're not done yet so mark not busy
|
||||
delete this.configured[ieeeAddr];
|
||||
}
|
||||
|
||||
logger.warn(
|
||||
`Failed to configure ${friendlyName} (${ieeeAddr}) ('${msg}')` +
|
||||
` (attempt #${this.attempts[ieeeAddr] + 1})`
|
||||
);
|
||||
|
||||
logger.warn(`This can be ignored if the device is working properly`);
|
||||
|
||||
this.attempts[ieeeAddr] += 1;
|
||||
}
|
||||
|
||||
queueCallback();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// no need to configure anyting. So just mark as done
|
||||
this.configured[ieeeAddr] = 'configured';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DeviceConfigure;
|
||||
@@ -0,0 +1,77 @@
|
||||
const settings = require('../util/settings');
|
||||
const logger = require('../util/logger');
|
||||
|
||||
const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/device/.+/get_group_membership$`);
|
||||
|
||||
class DeviceGroupMembership {
|
||||
constructor(zigbee, mqtt, publishEntityState) {
|
||||
this.zigbee = zigbee;
|
||||
this.mqtt = mqtt;
|
||||
this.publishEntityState = publishEntityState;
|
||||
}
|
||||
|
||||
onMQTTConnected() {
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/device/+/get_group_membership`);
|
||||
}
|
||||
|
||||
parseTopic(topic) {
|
||||
if (!topic.match(topicRegex)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Remove base from topic
|
||||
topic = topic.replace(`${settings.get().mqtt.base_topic}/bridge/device/`, '');
|
||||
|
||||
// Remove command from topic
|
||||
topic = topic.replace(`/get_group_membership`, '');
|
||||
|
||||
return {friendlyName: topic};
|
||||
}
|
||||
|
||||
onMQTTMessage(topic, message) {
|
||||
topic = this.parseTopic(topic);
|
||||
|
||||
if (!topic) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Map message to ieeeAddr and check if device exist.
|
||||
const friendlyName = topic.friendlyName;
|
||||
const ieeeAddr = settings.getIeeeAddrByFriendlyName(friendlyName) || friendlyName;
|
||||
if (!this.zigbee.getDevice(ieeeAddr)) {
|
||||
logger.error(`Failed to find device '${friendlyName}'`);
|
||||
return;
|
||||
}
|
||||
|
||||
const callback = (error, rsp) => {
|
||||
if (error) {
|
||||
logger.error(`Failed to get membership of ${ieeeAddr}`);
|
||||
} else {
|
||||
const {grouplist, capacity} = rsp;
|
||||
const msgGroupList = `${ieeeAddr} is in groups [${grouplist}]`;
|
||||
let msgCapacity;
|
||||
if (capacity === 254) {
|
||||
msgCapacity = 'it can be a part of at least 1 more group';
|
||||
} else {
|
||||
msgCapacity = `its remaining group capacity is ${capacity === 255 ? 'unknown' : capacity}`;
|
||||
}
|
||||
logger.info(`${msgGroupList} and ${msgCapacity}`);
|
||||
|
||||
// never cache
|
||||
this.publishEntityState(ieeeAddr, {
|
||||
group_list: grouplist,
|
||||
group_capacity: capacity,
|
||||
}, false);
|
||||
}
|
||||
};
|
||||
|
||||
this.zigbee.publish(
|
||||
ieeeAddr, 'device', 'genGroups', 'getMembership', 'functional',
|
||||
{groupcount: 0, grouplist: []}, null, null, callback,
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DeviceGroupMembership;
|
||||
@@ -0,0 +1,254 @@
|
||||
|
||||
const settings = require('../util/settings');
|
||||
const zigbeeShepherdConverters = require('zigbee-shepherd-converters');
|
||||
const logger = require('../util/logger');
|
||||
|
||||
const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/.+/(set|get)$`);
|
||||
const postfixes = [
|
||||
'left', 'right', 'center', 'bottom_left', 'bottom_right',
|
||||
'top_left', 'top_right', 'white', 'rgb', 'system', 'top', 'bottom',
|
||||
];
|
||||
const maxDepth = 20;
|
||||
|
||||
const groupConverters = [
|
||||
{
|
||||
from: (converted) => {
|
||||
if (converted.cid === 'genOnOff') {
|
||||
return {state: converted.cmd.toUpperCase()};
|
||||
} else if (converted.cid === 'genLevelCtrl') {
|
||||
return {state: 'ON', brightness: Number(converted.zclData.level)};
|
||||
}
|
||||
},
|
||||
to: zigbeeShepherdConverters.toZigbeeConverters.light_onoff_brightness,
|
||||
},
|
||||
{
|
||||
from: (converted) => {
|
||||
return {color_temp: Number(converted.zclData.colortemp)};
|
||||
},
|
||||
to: zigbeeShepherdConverters.toZigbeeConverters.light_colortemp,
|
||||
},
|
||||
{
|
||||
from: (converted) => {
|
||||
if (converted.zclData.hasOwnProperty('colorx') && converted.zclData.hasOwnProperty('colory')) {
|
||||
return {
|
||||
color: {
|
||||
x: converted.zclData.colorx / 65535,
|
||||
y: converted.zclData.colory / 65535,
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
to: zigbeeShepherdConverters.toZigbeeConverters.light_color,
|
||||
},
|
||||
{
|
||||
from: () => null,
|
||||
to: zigbeeShepherdConverters.toZigbeeConverters.ignore_transition,
|
||||
},
|
||||
];
|
||||
|
||||
class DevicePublish {
|
||||
constructor(zigbee, mqtt, state, publishEntityState) {
|
||||
this.zigbee = zigbee;
|
||||
this.mqtt = mqtt;
|
||||
this.state = state;
|
||||
this.publishEntityState = publishEntityState;
|
||||
}
|
||||
|
||||
onMQTTConnected() {
|
||||
// Subscribe to topics.
|
||||
for (let step = 1; step < maxDepth; step++) {
|
||||
const topic = `${settings.get().mqtt.base_topic}/${'+/'.repeat(step)}`;
|
||||
this.mqtt.subscribe(`${topic}set`);
|
||||
this.mqtt.subscribe(`${topic}get`);
|
||||
}
|
||||
}
|
||||
|
||||
parseTopic(topic) {
|
||||
if (!topic.match(topicRegex)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Remove base from topic
|
||||
topic = topic.replace(`${settings.get().mqtt.base_topic}/`, '');
|
||||
|
||||
// Parse type from topic
|
||||
const type = topic.substr(topic.lastIndexOf('/') + 1, topic.length);
|
||||
|
||||
// Remove type from topic
|
||||
topic = topic.replace(`/${type}`, '');
|
||||
|
||||
// Check if we have to deal with a postfix.
|
||||
let postfix = '';
|
||||
if (postfixes.find((p) => topic.endsWith(`/${p}`))) {
|
||||
postfix = topic.substr(topic.lastIndexOf('/') + 1, topic.length);
|
||||
|
||||
// Remove postfix from topic
|
||||
topic = topic.replace(`/${postfix}`, '');
|
||||
}
|
||||
|
||||
const ID = topic;
|
||||
|
||||
return {type, ID, postfix};
|
||||
}
|
||||
|
||||
handlePublishError(entity, message, error) {
|
||||
const meta = {
|
||||
entity,
|
||||
message: message.toString(),
|
||||
};
|
||||
|
||||
this.mqtt.log('zigbee_publish_error', error.toString(), meta);
|
||||
}
|
||||
|
||||
handlePublished(entity, topic, converter, converted) {
|
||||
if (entity.type === 'device' && topic.type === 'set') {
|
||||
// Devices do not report when they go off, this ensures state (on/off) is always in sync.
|
||||
// Brightness onoff converters also control the state. (do a moveToLevelWithOnOff)
|
||||
const msg = {};
|
||||
const _key = topic.postfix ? `state_${topic.postfix}` : 'state';
|
||||
|
||||
if (converted.cid === 'genOnOff') {
|
||||
msg[_key] = converted.cmd.toUpperCase();
|
||||
} else if (converted.cid === 'genLevelCtrl' && converted.cmd === 'moveToLevelWithOnOff') {
|
||||
msg[_key] = 'ON';
|
||||
msg['brightness'] = Number(converted.zclData.level);
|
||||
} else if (converted.cid === 'genLevelCtrl' && converted.cmd === 'moveToLevel') {
|
||||
msg['brightness'] = Number(converted.zclData.level);
|
||||
}
|
||||
|
||||
if (Object.keys(msg).length > 0) {
|
||||
this.publishEntityState(entity.ID, msg, true);
|
||||
}
|
||||
} else if (entity.type === 'group' && topic.type === 'set') {
|
||||
// As a group doesn't confirm it's state, we mock the state here.
|
||||
const payload = groupConverters.find((g) => g.to === converter).from(converted);
|
||||
if (payload) {
|
||||
this.publishEntityState(entity.ID, payload, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMQTTMessage(topic, message) {
|
||||
topic = this.parseTopic(topic);
|
||||
|
||||
if (!topic) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Resolve the entity
|
||||
const entity = settings.resolveEntity(topic.ID);
|
||||
|
||||
// Get entity details
|
||||
let endpoint = null;
|
||||
let converters = null;
|
||||
let device = null;
|
||||
|
||||
if (entity.type === 'device') {
|
||||
device = this.zigbee.getDevice(entity.ID);
|
||||
if (!device) {
|
||||
logger.error(`Failed to find device with ieeAddr: '${entity.ID}'`);
|
||||
this.mqtt.log('entity_not_found', entity.ID);
|
||||
return;
|
||||
}
|
||||
|
||||
// Map device to a model
|
||||
const model = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
|
||||
if (!model) {
|
||||
logger.warn(`Device with modelID '${device.modelId}' is not supported.`);
|
||||
logger.warn(`Please see: https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine endpoint to publish to.
|
||||
if (model.hasOwnProperty('ep')) {
|
||||
const eps = model.ep(device);
|
||||
endpoint = eps.hasOwnProperty(topic.postfix) ? eps[topic.postfix] : null;
|
||||
if (endpoint === null && eps.hasOwnProperty('default')) {
|
||||
endpoint = eps['default'];
|
||||
}
|
||||
}
|
||||
|
||||
converters = model.toZigbee;
|
||||
} else if (entity.type === 'group') {
|
||||
converters = groupConverters.map((g) => g.to);
|
||||
}
|
||||
|
||||
// Convert the MQTT message to a Zigbee message.
|
||||
let json = null;
|
||||
try {
|
||||
json = JSON.parse(message);
|
||||
} catch (e) {
|
||||
// Cannot be parsed to JSON, assume state message.
|
||||
json = {state: message.toString()};
|
||||
}
|
||||
|
||||
// Ensure that state and brightness are executed before other commands.
|
||||
const keys = Object.keys(json);
|
||||
keys.sort((a, b) => (['state', 'brightness'].includes(a) ? -1 : 1));
|
||||
|
||||
// For each key in the JSON message find the matching converter.
|
||||
const usedConverters = [];
|
||||
keys.forEach((key) => {
|
||||
const converter = converters.find((c) => c.key.includes(key));
|
||||
|
||||
if (usedConverters.includes(converter)) {
|
||||
// Use a converter only once (e.g. light_onoff_brightness converters can convert state and brightness)
|
||||
return;
|
||||
}
|
||||
|
||||
if (!converter) {
|
||||
logger.error(`No converter available for '${key}' (${json[key]})`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Converter didn't return a result, skip
|
||||
const converted = converter.convert(key, json[key], json, topic.type, topic.postfix);
|
||||
if (!converted) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.zigbee.publish(
|
||||
entity.ID,
|
||||
entity.type,
|
||||
converted.cid,
|
||||
converted.cmd,
|
||||
converted.cmdType,
|
||||
converted.zclData,
|
||||
converted.cfg,
|
||||
endpoint,
|
||||
(error, rsp) => {
|
||||
if (!error) {
|
||||
this.handlePublished(entity, topic, converter, converted);
|
||||
} else {
|
||||
this.handlePublishError(entity, message, error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// It's possible for devices to get out of sync when writing an attribute that's not reportable.
|
||||
// So here we re-read the value after a specified timeout, this timeout could for example be the
|
||||
// transition time of a color change or for forcing a state read for devices that don't
|
||||
// automatically report a new state when set.
|
||||
// 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.
|
||||
const deviceSettings = settings.getDevice(entity.ID);
|
||||
if (topic.type === 'set' && entity.type === 'device'
|
||||
&& converted.hasOwnProperty('readAfterWriteTime')
|
||||
&& !(deviceSettings && deviceSettings.report)) {
|
||||
const getConverted = converter.convert(key, json[key], json, 'get');
|
||||
setTimeout(() => {
|
||||
this.zigbee.publish(
|
||||
entity.ID, entity.type, getConverted.cid, getConverted.cmd, getConverted.cmdType,
|
||||
getConverted.zclData, getConverted.cfg, endpoint, () => {}
|
||||
);
|
||||
}, converted.readAfterWriteTime);
|
||||
}
|
||||
|
||||
usedConverters.push(converter);
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DevicePublish;
|
||||
@@ -0,0 +1,170 @@
|
||||
const settings = require('../util/settings');
|
||||
const logger = require('../util/logger');
|
||||
const utils = require('../util/utils');
|
||||
|
||||
const dontCacheProperties = [
|
||||
'action', 'button', 'button_left', 'button_right', 'click', 'forgotten', 'keyerror',
|
||||
'step_size', 'transition_time', 'action_color_temperature', 'action_color',
|
||||
'action_group',
|
||||
];
|
||||
|
||||
/**
|
||||
* This extensions handles messages received from devices.
|
||||
*/
|
||||
class DeviceReceive {
|
||||
constructor(zigbee, mqtt, state, publishEntityState) {
|
||||
this.zigbee = zigbee;
|
||||
this.mqtt = mqtt;
|
||||
this.state = state;
|
||||
this.publishEntityState = publishEntityState;
|
||||
this.coordinator = null;
|
||||
this.elapsed = {};
|
||||
}
|
||||
|
||||
onZigbeeStarted() {
|
||||
this.coordinator = this.zigbee.getCoordinator().device.ieeeAddr;
|
||||
}
|
||||
|
||||
onZigbeeMessage(message, device, mappedDevice) {
|
||||
if (message.type == 'devInterview') {
|
||||
if (!settings.getDevice(message.data)) {
|
||||
logger.info('Connecting with device...');
|
||||
this.mqtt.log('pairing', 'connecting with device');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type == 'devIncoming') {
|
||||
logger.info('Device incoming...');
|
||||
this.mqtt.log('pairing', 'device incoming');
|
||||
}
|
||||
|
||||
if (!device) {
|
||||
logger.warn('Message without device!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (device.ieeeAddr === this.coordinator) {
|
||||
logger.debug('Ignoring message from coordinator');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is a new device.
|
||||
if (!settings.getDevice(device.ieeeAddr)) {
|
||||
logger.info(`New device '${device.modelId}' with address ${device.ieeeAddr} connected!`);
|
||||
settings.addDevice(device.ieeeAddr);
|
||||
this.mqtt.log('device_connected', device.ieeeAddr, {modelID: device.modelId});
|
||||
}
|
||||
|
||||
if (!mappedDevice) {
|
||||
logger.warn(`Device with modelID '${device.modelId}' is not supported.`);
|
||||
logger.warn(`Please see: https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html`);
|
||||
return;
|
||||
}
|
||||
|
||||
// After this point we cant handle message withoud cid or cmdId anymore.
|
||||
if (!message.data || (!message.data.cid && !message.data.cmdId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find a conveter for this message.
|
||||
const cid = message.data.cid;
|
||||
const cmdId = message.data.cmdId;
|
||||
const converters = mappedDevice.fromZigbee.filter((c) => {
|
||||
if (cid) {
|
||||
// readRsp messages have the same structure as attReport messages.
|
||||
// search for attReport converters on readRsp.
|
||||
const search = message.type === 'readRsp' ? 'attReport' : message.type;
|
||||
return c.cid === cid && c.type === search;
|
||||
} else if (cmdId) {
|
||||
return c.cmd === cmdId;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
// Check if there is an available converter
|
||||
if (!converters.length) {
|
||||
if (cid) {
|
||||
// Don't log readRsp messages, they are not interesting most of the time.
|
||||
if (message.type === 'readRsp') {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.warn(
|
||||
`No converter available for '${mappedDevice.model}' with cid '${cid}', ` +
|
||||
`type '${message.type}' and data '${JSON.stringify(message.data)}'`
|
||||
);
|
||||
} else if (cmdId) {
|
||||
logger.warn(
|
||||
`No converter available for '${mappedDevice.model}' with cmd '${cmdId}' ` +
|
||||
`and data '${JSON.stringify(message.data)}'`
|
||||
);
|
||||
}
|
||||
|
||||
logger.warn(`Please see: https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert this Zigbee message to a MQTT message.
|
||||
// Get payload for the message.
|
||||
// - If a payload is returned publish it to the MQTT broker
|
||||
// - If NO payload is returned do nothing. This is for non-standard behaviour
|
||||
// for e.g. click switches where we need to count number of clicks and detect long presses.
|
||||
const publish = (payload) => {
|
||||
// Don't cache messages with following properties:
|
||||
let cache = true;
|
||||
dontCacheProperties.forEach((property) => {
|
||||
if (payload.hasOwnProperty(property)) {
|
||||
cache = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Add device linkquality.
|
||||
if (message.hasOwnProperty('linkquality')) {
|
||||
payload.linkquality = message.linkquality;
|
||||
}
|
||||
|
||||
// Add last seen timestamp
|
||||
const now = Date.now();
|
||||
switch (settings.get().advanced.last_seen) {
|
||||
case 'ISO_8601':
|
||||
payload.last_seen = new Date(now).toISOString();
|
||||
break;
|
||||
case 'ISO_8601_local':
|
||||
payload.last_seen = utils.toLocalISOString(new Date(now));
|
||||
break;
|
||||
case 'epoch':
|
||||
payload.last_seen = now;
|
||||
break;
|
||||
}
|
||||
|
||||
if (settings.get().advanced.elapsed) {
|
||||
if (this.elapsed[device.ieeeAddr]) {
|
||||
payload.elapsed = now - this.elapsed[device.ieeeAddr];
|
||||
}
|
||||
|
||||
this.elapsed[device.ieeeAddr] = now;
|
||||
}
|
||||
|
||||
this.publishEntityState(device.ieeeAddr, payload, cache);
|
||||
};
|
||||
|
||||
let payload = {};
|
||||
converters.forEach((converter) => {
|
||||
const options = {...settings.get().device_options, ...settings.getDevice(device.ieeeAddr)};
|
||||
const converted = converter.convert(mappedDevice, message, publish, options);
|
||||
|
||||
if (converted) {
|
||||
payload = {...payload, ...converted};
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(payload).length) {
|
||||
publish(payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DeviceReceive;
|
||||
@@ -0,0 +1,98 @@
|
||||
const zigbeeShepherdConverters = require('zigbee-shepherd-converters');
|
||||
const logger = require('../util/logger');
|
||||
|
||||
const candidates = {
|
||||
'genOnOff': {
|
||||
attrs: ['onOff'],
|
||||
reportIntervalMin: 3,
|
||||
reportIntervalMax: 300,
|
||||
reportableChange: 0,
|
||||
},
|
||||
'genLevelCtrl': {
|
||||
attrs: ['currentLevel'],
|
||||
},
|
||||
'lightingColorCtrl': {
|
||||
attrs: ['colorTemperature', 'currentX', 'currentY'],
|
||||
},
|
||||
};
|
||||
|
||||
const reportInterval = {
|
||||
min: 3,
|
||||
max: 3600,
|
||||
};
|
||||
|
||||
const reportableChange = 0;
|
||||
|
||||
class DeviceReport {
|
||||
constructor(zigbee, mqtt, state, publishEntityState) {
|
||||
this.zigbee = zigbee;
|
||||
this.mqtt = mqtt;
|
||||
this.state = state;
|
||||
this.publishEntityState = publishEntityState;
|
||||
}
|
||||
|
||||
setupReporting(mappedDevice, device) {
|
||||
let epId = null;
|
||||
|
||||
// Check if this device uses a different epId.
|
||||
if (mappedDevice.hasOwnProperty('ep')) {
|
||||
const eps = mappedDevice.ep(device);
|
||||
epId = eps[''] || null;
|
||||
}
|
||||
|
||||
const endpoint = this.zigbee.getEndpoint(device.ieeeAddr, epId);
|
||||
|
||||
if (!endpoint) {
|
||||
logger.error(`Failed to setup reporting for ${device.ieeeAddr}, endpoint not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug(`Setting up reporting for ${device.ieeeAddr}`);
|
||||
|
||||
Object.values(endpoint.clusters).filter((c) => c).forEach((c) => {
|
||||
const cluster = c.attrs.cid;
|
||||
if (candidates[cluster]) {
|
||||
const candidate = candidates[cluster];
|
||||
const attributeNames = candidate.attrs.filter((a) => c.attrs.hasOwnProperty(a));
|
||||
const attributes = attributeNames.map((attribute) => {
|
||||
return {
|
||||
attr: attribute,
|
||||
min: candidate.hasOwnProperty('reportIntervalMin')?
|
||||
candidate.reportIntervalMin:reportInterval.min,
|
||||
max: candidate.hasOwnProperty('reportIntervalMax')?
|
||||
candidate.reportIntervalMax:reportInterval.max,
|
||||
change: candidate.hasOwnProperty('reportableChange')?
|
||||
candidate.reportableChange:reportableChange,
|
||||
};
|
||||
});
|
||||
|
||||
if (attributes.length > 0) {
|
||||
this.zigbee.report(endpoint, cluster, attributes);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onZigbeeStarted() {
|
||||
this.zigbee.getAllClients().forEach((device) => {
|
||||
const mappedDevice = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
|
||||
|
||||
if (mappedDevice) {
|
||||
this.setupReporting(mappedDevice, device);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onZigbeeMessage(message, device, mappedDevice) {
|
||||
// Handle messages of type endDeviceAnnce and devIncoming.
|
||||
// This message is typically send when a device comes online after being powered off
|
||||
// Ikea TRADFRI tend to forget their reporting after powered off.
|
||||
// Re-setup reporting.
|
||||
// https://github.com/Koenkk/zigbee2mqtt/issues/966
|
||||
if (device && mappedDevice && ['endDeviceAnnce', 'devIncoming'].includes(message.type)) {
|
||||
this.setupReporting(mappedDevice, device);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DeviceReport;
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* This extensions is for documentation purposes only.
|
||||
* It describes all methods that are called by the controller.
|
||||
*/
|
||||
class ExtensionTemplate {
|
||||
/**
|
||||
* Besides intializing variables, the constructor should do nothing!
|
||||
*
|
||||
* @param {Zigbee} zigbee Zigbee controller
|
||||
* @param {MQTT} mqtt MQTT controller
|
||||
* @param {State} state State controller
|
||||
* @param {Function} publishEntityState Method to publish device state to MQTT.
|
||||
*/
|
||||
constructor(zigbee, mqtt, state, publishEntityState) {
|
||||
this.zigbee = zigbee;
|
||||
this.mqtt = mqtt;
|
||||
this.state = state;
|
||||
this.publishEntityState = publishEntityState;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called by the controller once Zigbee has been started.
|
||||
*/
|
||||
onZigbeeStarted() {}
|
||||
|
||||
/**
|
||||
* This method is called by the controller once connected to the MQTT server.
|
||||
*/
|
||||
onMQTTConnected() {}
|
||||
|
||||
/**
|
||||
* Is called when a Zigbee message from a device is received.
|
||||
* @param {Object?} message The received message (can be null)
|
||||
* @param {Object?} device The device of the message (can be null)
|
||||
* @param {Object?} mappedDevice The mapped device (can be null)
|
||||
*/
|
||||
onZigbeeMessage(message, device, mappedDevice) {}
|
||||
|
||||
/**
|
||||
* Is called when a MQTT message is received
|
||||
* @param {string} topic Topic on which the message was received
|
||||
* @param {Object} message The received message
|
||||
* @return {boolean} if the message was handled
|
||||
*/
|
||||
onMQTTMessage(topic, message) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is called once the extension has to stop
|
||||
*/
|
||||
stop() {}
|
||||
}
|
||||
|
||||
module.exports = ExtensionTemplate;
|
||||
@@ -0,0 +1,103 @@
|
||||
const settings = require('../util/settings');
|
||||
const logger = require('../util/logger');
|
||||
|
||||
const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/group/.+/(remove|add|remove_all)$`);
|
||||
|
||||
class Groups {
|
||||
constructor(zigbee, mqtt, state, publishEntityState) {
|
||||
this.zigbee = zigbee;
|
||||
this.mqtt = mqtt;
|
||||
this.state = state;
|
||||
this.publishEntityState = publishEntityState;
|
||||
}
|
||||
|
||||
onMQTTConnected() {
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/group/+/remove`);
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/group/+/add`);
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/group/+/remove_all`);
|
||||
}
|
||||
|
||||
parseTopic(topic) {
|
||||
if (!topic.match(topicRegex)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Remove base from topic
|
||||
topic = topic.replace(`${settings.get().mqtt.base_topic}/bridge/group/`, '');
|
||||
|
||||
// Parse type from topic
|
||||
const type = topic.substr(topic.lastIndexOf('/') + 1, topic.length);
|
||||
|
||||
// Remove type from topic
|
||||
topic = topic.replace(`/${type}`, '');
|
||||
|
||||
return {friendly_name: topic, type};
|
||||
}
|
||||
|
||||
onMQTTMessage(topic, message) {
|
||||
topic = this.parseTopic(topic);
|
||||
|
||||
if (!topic) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Find ID of this group.
|
||||
const groupID = settings.getGroupIDByFriendlyName(topic.friendly_name);
|
||||
if (!groupID) {
|
||||
logger.error(`Group with friendly_name '${topic.friendly_name}' doesn't exist`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (groupID === 99) {
|
||||
logger.error('Group 99 is reserved, please use a different groupID');
|
||||
return;
|
||||
}
|
||||
|
||||
// Map message to ieeeAddr and check if device exist.
|
||||
message = message.toString();
|
||||
const ieeeAddr = settings.getIeeeAddrByFriendlyName(message) || message;
|
||||
if (!this.zigbee.getDevice(ieeeAddr)) {
|
||||
logger.error(`Failed to find device '${message}'`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Send command to the device.
|
||||
let payload = null;
|
||||
let cmd = null;
|
||||
if (topic.type === 'add') {
|
||||
payload = {groupid: groupID, groupname: ''};
|
||||
cmd = 'add';
|
||||
} else if (topic.type === 'remove') {
|
||||
payload = {groupid: groupID};
|
||||
cmd = 'remove';
|
||||
} else if (topic.type === 'remove_all') {
|
||||
payload = {};
|
||||
cmd = 'removeAll';
|
||||
}
|
||||
|
||||
const callback = (error, rsp) => {
|
||||
if (error) {
|
||||
logger.error(`Failed to ${topic.type} ${ieeeAddr} from ${topic.friendly_name}`);
|
||||
} else {
|
||||
logger.info(`Successfully ${topic.type} ${ieeeAddr} to ${topic.friendly_name}`);
|
||||
|
||||
// Log to MQTT
|
||||
const log = {device: message};
|
||||
if (['remove', 'add'].includes(topic.type)) {
|
||||
log.group = topic.friendly_name;
|
||||
}
|
||||
|
||||
this.mqtt.log(log);
|
||||
}
|
||||
};
|
||||
|
||||
this.zigbee.publish(
|
||||
ieeeAddr, 'device', 'genGroups', cmd, 'functional',
|
||||
payload, null, null, callback,
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Groups;
|
||||
@@ -0,0 +1,758 @@
|
||||
const zigbeeShepherdConverters = require('zigbee-shepherd-converters');
|
||||
const settings = require('../util/settings');
|
||||
const logger = require('../util/logger');
|
||||
const zigbee2mqttVersion = require('../../package.json').version;
|
||||
|
||||
const configurations = {
|
||||
// Binary sensor
|
||||
'binary_sensor_occupancy': {
|
||||
type: 'binary_sensor',
|
||||
object_id: 'occupancy',
|
||||
discovery_payload: {
|
||||
payload_on: true,
|
||||
payload_off: false,
|
||||
value_template: '{{ value_json.occupancy }}',
|
||||
device_class: 'motion',
|
||||
},
|
||||
},
|
||||
'binary_sensor_presence': {
|
||||
type: 'binary_sensor',
|
||||
object_id: 'presence',
|
||||
discovery_payload: {
|
||||
payload_on: true,
|
||||
payload_off: false,
|
||||
value_template: '{{ value_json.presence }}',
|
||||
device_class: 'presence',
|
||||
},
|
||||
},
|
||||
'binary_sensor_contact': {
|
||||
type: 'binary_sensor',
|
||||
object_id: 'contact',
|
||||
discovery_payload: {
|
||||
payload_on: false,
|
||||
payload_off: true,
|
||||
value_template: '{{ value_json.contact }}',
|
||||
device_class: 'door',
|
||||
},
|
||||
},
|
||||
'binary_sensor_water_leak': {
|
||||
type: 'binary_sensor',
|
||||
object_id: 'water_leak',
|
||||
discovery_payload: {
|
||||
payload_on: true,
|
||||
payload_off: false,
|
||||
value_template: '{{ value_json.water_leak }}',
|
||||
device_class: 'moisture',
|
||||
},
|
||||
},
|
||||
'binary_sensor_smoke': {
|
||||
type: 'binary_sensor',
|
||||
object_id: 'smoke',
|
||||
discovery_payload: {
|
||||
payload_on: true,
|
||||
payload_off: false,
|
||||
value_template: '{{ value_json.smoke }}',
|
||||
device_class: 'smoke',
|
||||
},
|
||||
},
|
||||
'binary_sensor_gas': {
|
||||
type: 'binary_sensor',
|
||||
object_id: 'gas',
|
||||
discovery_payload: {
|
||||
payload_on: true,
|
||||
payload_off: false,
|
||||
value_template: '{{ value_json.gas }}',
|
||||
device_class: 'gas',
|
||||
},
|
||||
},
|
||||
'binary_sensor_router': {
|
||||
type: 'binary_sensor',
|
||||
object_id: 'router',
|
||||
discovery_payload: {
|
||||
payload_on: true,
|
||||
payload_off: false,
|
||||
value_template: '{{ value_json.state }}',
|
||||
device_class: 'connectivity',
|
||||
},
|
||||
},
|
||||
'binary_sensor_battery_low': {
|
||||
type: 'binary_sensor',
|
||||
object_id: 'battery_low',
|
||||
discovery_payload: {
|
||||
payload_on: true,
|
||||
payload_off: false,
|
||||
value_template: '{{ value_json.battery_low}}',
|
||||
device_class: 'battery',
|
||||
},
|
||||
},
|
||||
|
||||
// Sensor
|
||||
'sensor_illuminance': {
|
||||
type: 'sensor',
|
||||
object_id: 'illuminance',
|
||||
discovery_payload: {
|
||||
unit_of_measurement: 'lx',
|
||||
device_class: 'illuminance',
|
||||
value_template: '{{ value_json.illuminance }}',
|
||||
},
|
||||
},
|
||||
'sensor_humidity': {
|
||||
type: 'sensor',
|
||||
object_id: 'humidity',
|
||||
discovery_payload: {
|
||||
unit_of_measurement: '%',
|
||||
device_class: 'humidity',
|
||||
value_template: '{{ value_json.humidity }}',
|
||||
},
|
||||
},
|
||||
'sensor_temperature': {
|
||||
type: 'sensor',
|
||||
object_id: 'temperature',
|
||||
discovery_payload: {
|
||||
unit_of_measurement: '°C',
|
||||
device_class: 'temperature',
|
||||
value_template: '{{ value_json.temperature }}',
|
||||
},
|
||||
},
|
||||
'sensor_pressure': {
|
||||
type: 'sensor',
|
||||
object_id: 'pressure',
|
||||
discovery_payload: {
|
||||
unit_of_measurement: 'hPa',
|
||||
device_class: 'pressure',
|
||||
value_template: '{{ value_json.pressure }}',
|
||||
},
|
||||
},
|
||||
'sensor_click': {
|
||||
type: 'sensor',
|
||||
object_id: 'click',
|
||||
discovery_payload: {
|
||||
icon: 'mdi:toggle-switch',
|
||||
value_template: '{{ value_json.click }}',
|
||||
expire_after: 1,
|
||||
},
|
||||
},
|
||||
'sensor_power': {
|
||||
type: 'sensor',
|
||||
object_id: 'power',
|
||||
discovery_payload: {
|
||||
unit_of_measurement: 'Watt',
|
||||
icon: 'mdi:flash',
|
||||
value_template: '{{ value_json.power }}',
|
||||
},
|
||||
},
|
||||
'sensor_action': {
|
||||
type: 'sensor',
|
||||
object_id: 'action',
|
||||
discovery_payload: {
|
||||
icon: 'mdi:gesture-double-tap',
|
||||
value_template: '{{ value_json.action }}',
|
||||
expire_after: 1,
|
||||
},
|
||||
},
|
||||
'sensor_brightness': {
|
||||
type: 'sensor',
|
||||
object_id: 'brightness',
|
||||
discovery_payload: {
|
||||
unit_of_measurement: 'brightness',
|
||||
icon: 'mdi:brightness-5',
|
||||
value_template: '{{ value_json.brightness }}',
|
||||
},
|
||||
},
|
||||
'sensor_lock': {
|
||||
type: 'sensor',
|
||||
object_id: 'lock',
|
||||
discovery_payload: {
|
||||
icon: 'mdi:lock',
|
||||
value_template: '{{ value_json.inserted }}',
|
||||
},
|
||||
},
|
||||
'sensor_battery': {
|
||||
type: 'sensor',
|
||||
object_id: 'battery',
|
||||
discovery_payload: {
|
||||
unit_of_measurement: '%',
|
||||
device_class: 'battery',
|
||||
value_template: '{{ value_json.battery }}',
|
||||
},
|
||||
},
|
||||
'sensor_linkquality': {
|
||||
type: 'sensor',
|
||||
object_id: 'linkquality',
|
||||
discovery_payload: {
|
||||
unit_of_measurement: '-',
|
||||
value_template: '{{ value_json.linkquality }}',
|
||||
},
|
||||
},
|
||||
'sensor_gas_density': {
|
||||
type: 'sensor',
|
||||
object_id: 'gas_density',
|
||||
discovery_payload: {
|
||||
value_template: '{{ value_json.gas_density }}',
|
||||
icon: 'mdi:google-circles-communities',
|
||||
},
|
||||
},
|
||||
'sensor_cover': {
|
||||
type: 'sensor',
|
||||
object_id: 'cover',
|
||||
discovery_payload: {
|
||||
value_template: '{{ value_json.position }}',
|
||||
icon: 'mdi:view-array',
|
||||
},
|
||||
},
|
||||
|
||||
// Light
|
||||
'light_brightness_colortemp_colorxy': {
|
||||
type: 'light',
|
||||
object_id: 'light',
|
||||
discovery_payload: {
|
||||
brightness: true,
|
||||
color_temp: true,
|
||||
xy: true,
|
||||
schema: 'json',
|
||||
command_topic: true,
|
||||
},
|
||||
},
|
||||
'light_brightness_colorxy': {
|
||||
type: 'light',
|
||||
object_id: 'light',
|
||||
discovery_payload: {
|
||||
brightness: true,
|
||||
xy: true,
|
||||
schema: 'json',
|
||||
command_topic: true,
|
||||
},
|
||||
},
|
||||
'light_brightness_colortemp': {
|
||||
type: 'light',
|
||||
object_id: 'light',
|
||||
discovery_payload: {
|
||||
brightness: true,
|
||||
color_temp: true,
|
||||
schema: 'json',
|
||||
command_topic: true,
|
||||
},
|
||||
},
|
||||
'light_brightness': {
|
||||
type: 'light',
|
||||
object_id: 'light',
|
||||
discovery_payload: {
|
||||
brightness: true,
|
||||
schema: 'json',
|
||||
command_topic: true,
|
||||
},
|
||||
},
|
||||
|
||||
// Switch
|
||||
'switch': {
|
||||
type: 'switch',
|
||||
object_id: 'switch',
|
||||
discovery_payload: {
|
||||
payload_off: 'OFF',
|
||||
payload_on: 'ON',
|
||||
value_template: '{{ value_json.state }}',
|
||||
command_topic: true,
|
||||
},
|
||||
},
|
||||
|
||||
// Cover
|
||||
'cover': {
|
||||
type: 'cover',
|
||||
object_id: 'cover',
|
||||
discovery_payload: {
|
||||
command_topic: true,
|
||||
optimistic: true,
|
||||
},
|
||||
},
|
||||
'cover_position': {
|
||||
type: 'cover',
|
||||
object_id: 'cover',
|
||||
discovery_payload: {
|
||||
command_topic: true,
|
||||
position_topic: true,
|
||||
set_position_topic: true,
|
||||
set_position_template: '{ "position": {{ position }} }',
|
||||
value_template: '{{ value_json.position }}',
|
||||
state_topic: false,
|
||||
},
|
||||
},
|
||||
|
||||
// Lock
|
||||
'lock': {
|
||||
type: 'lock',
|
||||
object_id: 'lock',
|
||||
discovery_payload: {
|
||||
command_topic: true,
|
||||
value_template: '{{ value_json.state }}',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const switchWithPostfix = (postfix) => {
|
||||
return {
|
||||
type: 'switch',
|
||||
object_id: `switch_${postfix}`,
|
||||
discovery_payload: {
|
||||
payload_off: 'OFF',
|
||||
payload_on: 'ON',
|
||||
value_template: `{{ value_json.state_${postfix} }}`,
|
||||
command_topic: true,
|
||||
command_topic_prefix: postfix,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// Map homeassitant configurations to devices.
|
||||
const mapping = {
|
||||
'WXKG01LM': [configurations.sensor_click, configurations.sensor_battery],
|
||||
'WXKG11LM': [configurations.sensor_click, configurations.sensor_battery],
|
||||
'WXKG12LM': [configurations.sensor_click, configurations.sensor_battery],
|
||||
'WXKG03LM': [configurations.sensor_click, configurations.sensor_battery],
|
||||
'WXKG02LM': [configurations.sensor_click, configurations.sensor_battery],
|
||||
'QBKG04LM': [configurations.switch],
|
||||
'QBKG03LM': [switchWithPostfix('left'), switchWithPostfix('right')],
|
||||
'WSDCGQ01LM': [configurations.sensor_temperature, configurations.sensor_humidity, configurations.sensor_battery],
|
||||
'WSDCGQ11LM': [
|
||||
configurations.sensor_temperature, configurations.sensor_humidity, configurations.sensor_pressure,
|
||||
configurations.sensor_battery,
|
||||
],
|
||||
'RTCGQ01LM': [configurations.binary_sensor_occupancy, configurations.sensor_battery],
|
||||
'RTCGQ11LM': [
|
||||
configurations.binary_sensor_occupancy, configurations.sensor_illuminance,
|
||||
configurations.sensor_battery,
|
||||
],
|
||||
'MCCGQ01LM': [configurations.binary_sensor_contact, configurations.sensor_battery],
|
||||
'MCCGQ11LM': [configurations.binary_sensor_contact, configurations.sensor_battery],
|
||||
'SJCGQ11LM': [configurations.binary_sensor_water_leak, configurations.sensor_battery],
|
||||
'MFKZQ01LM': [configurations.sensor_action, configurations.sensor_battery],
|
||||
'ZNCZ02LM': [configurations.switch, configurations.sensor_power],
|
||||
'QBCZ11LM': [configurations.switch, configurations.sensor_power],
|
||||
'LED1545G12': [configurations.light_brightness_colortemp],
|
||||
'LED1623G12': [configurations.light_brightness],
|
||||
'LED1622G12': [configurations.light_brightness],
|
||||
'LED1537R6': [configurations.light_brightness_colortemp],
|
||||
'LED1650R5': [configurations.light_brightness],
|
||||
'LED1536G5': [configurations.light_brightness_colortemp],
|
||||
'7299760PH': [configurations.light_brightness_colorxy],
|
||||
'7146060PH': [configurations.light_brightness_colortemp_colorxy],
|
||||
'F7C033': [configurations.light_brightness],
|
||||
'JTYJ-GD-01LM/BW': [configurations.binary_sensor_smoke, configurations.sensor_battery],
|
||||
'PLUG EDP RE:DY': [configurations.switch, configurations.sensor_power],
|
||||
'CC2530.ROUTER': [configurations.binary_sensor_router],
|
||||
'AA70155': [configurations.light_brightness_colortemp],
|
||||
'4058075816718': [configurations.light_brightness_colortemp_colorxy],
|
||||
'AA69697': [configurations.light_brightness_colortemp_colorxy],
|
||||
'HALIGHTDIMWWE27': [configurations.light_brightness],
|
||||
'HALIGHTDIMWWB22': [configurations.light_brightness],
|
||||
'AB3257001NJ': [configurations.switch],
|
||||
'8718696449691': [configurations.light_brightness],
|
||||
'RB 185 C': [configurations.light_brightness_colortemp_colorxy],
|
||||
'BY 185 C': [configurations.light_brightness_colortemp_colorxy],
|
||||
'9290012573A': [configurations.light_brightness_colortemp_colorxy],
|
||||
'LED1624G9': [configurations.light_brightness_colorxy],
|
||||
'73742': [configurations.light_brightness_colortemp],
|
||||
'73740': [configurations.light_brightness_colortemp],
|
||||
'73739': [configurations.light_brightness_colortemp_colorxy],
|
||||
'22670': [configurations.light_brightness],
|
||||
'ICTC-G-1': [configurations.sensor_brightness, configurations.sensor_battery, configurations.sensor_action],
|
||||
'ICPSHC24-30EU-IL-1': [configurations.light_brightness],
|
||||
'45852GE': [configurations.light_brightness],
|
||||
'E11-G13': [configurations.light_brightness],
|
||||
'LED1649C5': [configurations.light_brightness],
|
||||
'ICPSHC24-10EU-IL-1': [configurations.light_brightness],
|
||||
'LED1546G12': [configurations.light_brightness_colortemp],
|
||||
'L1527': [configurations.light_brightness_colortemp],
|
||||
'L1529': [configurations.light_brightness_colortemp],
|
||||
'L1528': [configurations.light_brightness_colortemp],
|
||||
'L1531': [configurations.light_brightness_colortemp],
|
||||
'RB 165': [configurations.light_brightness],
|
||||
'RB 175 W': [configurations.light_brightness],
|
||||
'RS 125': [configurations.light_brightness],
|
||||
'RS 225': [configurations.light_brightness],
|
||||
'RB 145': [configurations.light_brightness],
|
||||
'PL 110': [configurations.light_brightness],
|
||||
'ST 110': [configurations.light_brightness],
|
||||
'UC 110': [configurations.light_brightness],
|
||||
'DL 110 N': [configurations.light_brightness],
|
||||
'DL 110 W': [configurations.light_brightness],
|
||||
'SL 110 N': [configurations.light_brightness],
|
||||
'SL 110 M': [configurations.light_brightness],
|
||||
'SL 110 W': [configurations.light_brightness],
|
||||
'AA68199': [configurations.light_brightness_colortemp],
|
||||
'QBKG11LM': [configurations.switch, configurations.sensor_power],
|
||||
'QBKG12LM': [switchWithPostfix('left'), switchWithPostfix('right'), configurations.sensor_power],
|
||||
'K2RGBW01': [configurations.light_brightness_colortemp_colorxy],
|
||||
'9290011370': [configurations.light_brightness],
|
||||
'DNCKATSW001': [configurations.switch],
|
||||
'Z809A': [configurations.switch, configurations.sensor_power],
|
||||
'NL08-0800': [configurations.light_brightness],
|
||||
'915005106701': [configurations.light_brightness_colortemp_colorxy],
|
||||
'AB32840': [configurations.light_brightness_colortemp],
|
||||
'8718696485880': [configurations.light_brightness_colortemp_colorxy],
|
||||
'8718696598283': [configurations.light_brightness_colortemp],
|
||||
'8718696695203': [configurations.light_brightness_colortemp],
|
||||
'73693': [configurations.light_brightness_colortemp_colorxy],
|
||||
'324131092621': [configurations.sensor_action, configurations.sensor_battery],
|
||||
'9290012607': [
|
||||
configurations.binary_sensor_occupancy, configurations.sensor_temperature,
|
||||
configurations.sensor_illuminance, configurations.sensor_battery,
|
||||
],
|
||||
'GL-C-008': [configurations.light_brightness_colortemp_colorxy],
|
||||
'STSS-MULT-001': [configurations.binary_sensor_contact, configurations.sensor_battery],
|
||||
'E11-G23/E11-G33': [configurations.light_brightness],
|
||||
'E1ACA4ABE38A': [configurations.light_brightness],
|
||||
'AC03645': [configurations.light_brightness_colortemp_colorxy],
|
||||
'AC03641': [configurations.light_brightness],
|
||||
'AC03648': [configurations.light_brightness_colortemp],
|
||||
'FB56+ZSW05HG1.2': [configurations.switch],
|
||||
'72922-A': [configurations.switch],
|
||||
'AC03642': [configurations.light_brightness_colortemp],
|
||||
'DNCKATSW002': [switchWithPostfix('left'), switchWithPostfix('right')],
|
||||
'DNCKATSW003': [switchWithPostfix('left'), switchWithPostfix('right'), switchWithPostfix('center')],
|
||||
'DNCKATSW004': [
|
||||
switchWithPostfix('bottom_left'), switchWithPostfix('bottom_right'),
|
||||
switchWithPostfix('top_left'), switchWithPostfix('top_right'),
|
||||
],
|
||||
'BY 165': [configurations.light_brightness],
|
||||
'ZLED-2709': [configurations.light_brightness],
|
||||
'8718696548738': [configurations.light_brightness_colortemp],
|
||||
'4052899926110': [configurations.light_brightness_colortemp_colorxy],
|
||||
'Z01-CIA19NAE26': [configurations.light_brightness],
|
||||
'E11-N1EA': [configurations.light_brightness_colortemp_colorxy],
|
||||
'74283': [configurations.light_brightness],
|
||||
'JTQJ-BF-01LM/BW': [
|
||||
configurations.binary_sensor_gas,
|
||||
configurations.sensor_gas_density,
|
||||
],
|
||||
'50045': [configurations.light_brightness],
|
||||
'AV2010/22': [configurations.binary_sensor_occupancy, configurations.sensor_battery],
|
||||
'3210-L': [configurations.switch],
|
||||
'3320-L': [configurations.binary_sensor_contact],
|
||||
'3326-L': [configurations.binary_sensor_occupancy, configurations.sensor_temperature],
|
||||
'7299355PH': [configurations.light_brightness_colorxy],
|
||||
'45857GE': [configurations.light_brightness],
|
||||
'A6121': [configurations.sensor_lock],
|
||||
'433714': [configurations.light_brightness],
|
||||
'3261030P7': [configurations.light_brightness_colortemp],
|
||||
'3216431P5': [configurations.light_brightness_colortemp],
|
||||
'DJT11LM': [configurations.sensor_action, configurations.sensor_battery],
|
||||
'E1603': [configurations.switch],
|
||||
'7199960PH': [configurations.light_brightness_colorxy],
|
||||
'74696': [configurations.light_brightness],
|
||||
'AB35996': [configurations.light_brightness_colortemp_colorxy],
|
||||
'AB401130055': [configurations.light_brightness_colortemp],
|
||||
'74282': [configurations.light_brightness_colortemp],
|
||||
'RS 128 T': [configurations.light_brightness_colortemp],
|
||||
'53170161': [configurations.light_brightness_colortemp],
|
||||
'4058075036147': [configurations.light_brightness_colortemp_colorxy],
|
||||
'KS-SM001': [configurations.switch],
|
||||
'MG-AUWS01': [switchWithPostfix('left'), switchWithPostfix('right')],
|
||||
'9290002579A': [configurations.light_brightness_colortemp_colorxy],
|
||||
'4256251-RZHAC': [configurations.switch, configurations.sensor_power],
|
||||
'STS-PRS-251': [configurations.binary_sensor_presence, configurations.sensor_battery],
|
||||
'4058075816794': [configurations.light_brightness_colortemp],
|
||||
'4052899926158': [configurations.light_brightness],
|
||||
'4058075036185': [configurations.light_brightness_colortemp_colorxy],
|
||||
'50049': [configurations.light_brightness_colorxy],
|
||||
'915005733701': [configurations.light_brightness_colortemp_colorxy],
|
||||
'RB 285 C': [configurations.light_brightness_colortemp_colorxy],
|
||||
'3216331P5': [configurations.light_brightness_colortemp],
|
||||
'AC08562': [configurations.light_brightness],
|
||||
'900008-WW': [configurations.light_brightness],
|
||||
'Mega23M12': [configurations.light_brightness_colortemp_colorxy],
|
||||
'PSS-23ZBS': [configurations.switch],
|
||||
'HS1SA': [configurations.binary_sensor_smoke, configurations.binary_sensor_battery_low],
|
||||
'Z01-A19NAE26': [configurations.light_brightness_colortemp],
|
||||
'AC01353010G': [configurations.binary_sensor_occupancy, configurations.sensor_temperature],
|
||||
'SP 120': [configurations.switch, configurations.sensor_power],
|
||||
'RB 248 T': [configurations.light_brightness_colortemp],
|
||||
'HS3SA': [configurations.binary_sensor_smoke, configurations.binary_sensor_battery_low],
|
||||
'HS1DS': [configurations.binary_sensor_contact],
|
||||
'HS1WL': [configurations.binary_sensor_water_leak],
|
||||
'421786': [configurations.light_brightness],
|
||||
'ICZB-IW11D': [configurations.light_brightness],
|
||||
'HGZB-01A': [configurations.light_brightness],
|
||||
'3321-S': [configurations.binary_sensor_contact, configurations.sensor_temperature],
|
||||
'ZPIR-8000': [configurations.binary_sensor_occupancy],
|
||||
'ZCTS-808': [configurations.binary_sensor_contact],
|
||||
'ZNLDP12LM': [configurations.light_brightness_colortemp],
|
||||
'D1821': [configurations.light_brightness_colortemp_colorxy],
|
||||
'ZNCLDJ11LM': [configurations.cover_position, configurations.sensor_cover],
|
||||
'LTFY004': [configurations.light_brightness_colorxy],
|
||||
'GL-S-007Z': [configurations.light_brightness_colortemp_colorxy],
|
||||
'3325-S': [configurations.sensor_temperature, configurations.binary_sensor_occupancy],
|
||||
'4713407': [configurations.light_brightness],
|
||||
'464800': [configurations.light_brightness_colortemp],
|
||||
'3261331P7': [configurations.light_brightness_colortemp],
|
||||
'4033930P7': [configurations.light_brightness_colortemp],
|
||||
'GL-B-008Z': [configurations.light_brightness_colortemp_colorxy],
|
||||
'AV2010/25': [configurations.switch, configurations.sensor_power],
|
||||
'E12-N14': [configurations.light_brightness],
|
||||
'1TST-EU': [],
|
||||
'RB 178 T': [configurations.light_brightness_colortemp],
|
||||
'45856GE': [configurations.switch],
|
||||
'GL-D-003Z': [configurations.light_brightness_colortemp_colorxy],
|
||||
'GD-CZ-006': [configurations.light_brightness],
|
||||
'AIRAM-CTR.U': [],
|
||||
'HGZB-20-DE': [configurations.switch],
|
||||
'D1531': [configurations.light_brightness],
|
||||
'D1532': [configurations.light_brightness],
|
||||
'AV2010/32': [],
|
||||
'HGZB-07A': [configurations.light_brightness_colortemp_colorxy],
|
||||
'E1524': [configurations.sensor_action],
|
||||
'GL-C-006': [configurations.light_brightness_colortemp],
|
||||
'100.424.11': [configurations.light_brightness_colortemp],
|
||||
'AC0251100NJ': [configurations.sensor_click, configurations.sensor_brightness, configurations.sensor_battery],
|
||||
'71831': [configurations.light_brightness_colortemp],
|
||||
'404000/404005/404012': [configurations.light_brightness_colortemp_colorxy],
|
||||
'404006/404008/404004': [configurations.light_brightness_colortemp],
|
||||
'MLI-404011': [configurations.sensor_action],
|
||||
'GL-S-003Z': [configurations.light_brightness_colortemp_colorxy],
|
||||
'HS1DS-E': [configurations.binary_sensor_contact],
|
||||
'SP600': [configurations.switch, configurations.sensor_power],
|
||||
'1613V': [configurations.switch, configurations.sensor_power],
|
||||
'XVV-Mega23M12': [configurations.light_brightness_colortemp],
|
||||
'GL-B-007Z': [configurations.light_brightness_colortemp_colorxy],
|
||||
'81809': [configurations.light_brightness_colortemp_colorxy],
|
||||
'4090130P7': [configurations.light_brightness_colortemp_colorxy],
|
||||
'100.110.39': [configurations.light_brightness_colortemp_colorxy],
|
||||
'TI0001': [switchWithPostfix('left'), switchWithPostfix('right')],
|
||||
'SPZB0001': [],
|
||||
'HS3CG': [configurations.binary_sensor_gas],
|
||||
'81825': [configurations.sensor_action],
|
||||
'Z809AF': [configurations.switch, configurations.sensor_power],
|
||||
'RADON TriTech ZB': [
|
||||
configurations.binary_sensor_occupancy, configurations.sensor_temperature,
|
||||
configurations.sensor_battery, configurations.binary_sensor_battery_low,
|
||||
],
|
||||
'IM-Z3.0-DIM': [configurations.light_brightness],
|
||||
'E1746': [],
|
||||
'YRD426NRSC': [configurations.lock, configurations.sensor_battery],
|
||||
'E1743': [configurations.sensor_click, configurations.sensor_battery],
|
||||
'LED1732G11': [configurations.light_brightness_colortemp],
|
||||
'RB 265': [configurations.light_brightness],
|
||||
'9290019758': [
|
||||
configurations.binary_sensor_occupancy, configurations.sensor_temperature,
|
||||
configurations.sensor_illuminance, configurations.sensor_battery,
|
||||
],
|
||||
'HGZB-042': [switchWithPostfix('top'), switchWithPostfix('bottom')],
|
||||
'GL-FL-004TZ': [configurations.light_brightness_colortemp_colorxy],
|
||||
'IM6001-OTP05': [configurations.switch],
|
||||
'SV01': [
|
||||
configurations.cover_position, configurations.sensor_temperature, configurations.sensor_pressure,
|
||||
configurations.sensor_battery,
|
||||
],
|
||||
'316GLEDRF': [configurations.light_brightness],
|
||||
'LVS-ZB500D': [configurations.light_brightness],
|
||||
'ST218': [],
|
||||
'E1525': [configurations.binary_sensor_occupancy, configurations.sensor_battery],
|
||||
'ZYCT-202': [configurations.sensor_action],
|
||||
'GR-ZB01-W': [configurations.cover_position],
|
||||
'4090531P7': [configurations.light_brightness_colortemp_colorxy],
|
||||
'HGZB-42-UK / HGZB-41': [configurations.switch],
|
||||
'ISW-ZPR1-WP13': [
|
||||
configurations.binary_sensor_occupancy, configurations.sensor_temperature,
|
||||
configurations.sensor_battery, configurations.binary_sensor_battery_low,
|
||||
],
|
||||
'9290018195': [configurations.light_brightness],
|
||||
'HGZB-04D': [configurations.light_brightness],
|
||||
'HGZB-02A': [configurations.light_brightness],
|
||||
'HGZB-043': [switchWithPostfix('top'), switchWithPostfix('bottom'), switchWithPostfix('center')],
|
||||
'NCZ-3043-HA': [
|
||||
configurations.binary_sensor_occupancy, configurations.sensor_temperature,
|
||||
configurations.sensor_battery, configurations.binary_sensor_battery_low,
|
||||
],
|
||||
'STS-IRM-250': [
|
||||
configurations.binary_sensor_occupancy, configurations.sensor_temperature,
|
||||
configurations.sensor_battery, configurations.binary_sensor_battery_low,
|
||||
],
|
||||
'3305-S': [
|
||||
configurations.binary_sensor_occupancy, configurations.sensor_temperature,
|
||||
configurations.sensor_battery, configurations.binary_sensor_battery_low,
|
||||
],
|
||||
'3300-S': [
|
||||
configurations.sensor_temperature, configurations.binary_sensor_contact,
|
||||
configurations.sensor_battery, configurations.binary_sensor_battery_low,
|
||||
],
|
||||
};
|
||||
|
||||
Object.keys(mapping).forEach((key) => {
|
||||
mapping[key].push(configurations.sensor_linkquality);
|
||||
});
|
||||
|
||||
/**
|
||||
* This extensions handles integration with HomeAssistant
|
||||
*/
|
||||
class HomeAssistant {
|
||||
constructor(zigbee, mqtt, state, publishEntityState) {
|
||||
this.zigbee = zigbee;
|
||||
this.mqtt = mqtt;
|
||||
this.state = state;
|
||||
this.publishEntityState = publishEntityState;
|
||||
this.zigbee2mqttVersion = zigbee2mqttVersion;
|
||||
|
||||
// A map of all discoverd devices
|
||||
this.discovered = {};
|
||||
|
||||
if (!settings.get().advanced.cache_state) {
|
||||
logger.warn('In order for HomeAssistant integration to work properly set `cache_state: true');
|
||||
}
|
||||
|
||||
if (settings.get().experimental.output === 'attribute') {
|
||||
throw new Error('Home Assitant integration is not possible with attribute output!');
|
||||
}
|
||||
}
|
||||
|
||||
onMQTTConnected() {
|
||||
this.mqtt.subscribe('hass/status');
|
||||
|
||||
// MQTT discovery of all paired devices on startup.
|
||||
this.zigbee.getAllClients().forEach((device) => {
|
||||
const mappedModel = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
|
||||
if (mappedModel) {
|
||||
this.discover(device.ieeeAddr, mappedModel, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
discover(entityID, mappedModel, force=false) {
|
||||
// Check if already discoverd and check if there are configs.
|
||||
const discover = force || !this.discovered[entityID];
|
||||
if (!discover) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entity = settings.resolveEntity(entityID);
|
||||
if (entity.type === 'device' && (!mapping[mappedModel.model] || !settings.getDevice(entity.ID))) {
|
||||
return;
|
||||
} else if (entity.type === 'group' && (!settings.getGroup(entity.ID))) {
|
||||
return;
|
||||
}
|
||||
|
||||
mapping[mappedModel.model].forEach((config) => {
|
||||
const topic = `${config.type}/${entityID}/${config.object_id}/config`;
|
||||
const payload = {...config.discovery_payload};
|
||||
const stateTopic = `${settings.get().mqtt.base_topic}/${entity.friendlyName}`;
|
||||
|
||||
if (!payload.hasOwnProperty('state_topic') || payload.state_topic) {
|
||||
payload.state_topic = stateTopic;
|
||||
} else if (payload.hasOwnProperty('state_topic')) {
|
||||
delete payload.state_topic;
|
||||
}
|
||||
|
||||
if (payload.position_topic) {
|
||||
payload.position_topic = stateTopic;
|
||||
}
|
||||
|
||||
// Set json_attributes_topic for types which support this
|
||||
// https://github.com/Koenkk/zigbee2mqtt/issues/840
|
||||
if (['binary_sensor', 'sensor', 'lock'].includes(config.type)) {
|
||||
payload.json_attributes_topic = payload.state_topic;
|
||||
}
|
||||
|
||||
// Set (unique) name
|
||||
payload.name = `${entity.friendlyName}_${config.object_id}`;
|
||||
|
||||
// Set unique_id
|
||||
payload.unique_id = `${entityID}_${config.object_id}_${settings.get().mqtt.base_topic}`;
|
||||
|
||||
// Attributes for device registry
|
||||
payload.device = {
|
||||
identifiers: `zigbee2mqtt_${entityID}`,
|
||||
name: entity.friendlyName,
|
||||
sw_version: `Zigbee2mqtt ${this.zigbee2mqttVersion}`,
|
||||
model: `${mappedModel.description} (${mappedModel.model})`,
|
||||
manufacturer: mappedModel.vendor,
|
||||
};
|
||||
|
||||
// Set availability payload
|
||||
// When using availability_timeout each device has it's own availability topic.
|
||||
// If not, use the availability topic of zigbee2mqtt.
|
||||
if (settings.get().advanced.availability_timeout) {
|
||||
payload.availability_topic = `${settings.get().mqtt.base_topic}/${entity.friendlyName}/availability`;
|
||||
} else {
|
||||
payload.availability_topic = `${settings.get().mqtt.base_topic}/bridge/state`;
|
||||
}
|
||||
|
||||
// Add precision to value_template
|
||||
const device = settings.getDevice(entityID);
|
||||
if (device.hasOwnProperty(`${config.object_id}_precision`)) {
|
||||
const precision = device[`${config.object_id}_precision`];
|
||||
let template = payload.value_template;
|
||||
template = template.replace('{{ ', '').replace(' }}', '');
|
||||
template = `{{ (${template} | float) | round(${precision}) }}`;
|
||||
payload.value_template = template;
|
||||
}
|
||||
|
||||
if (payload.command_topic) {
|
||||
payload.command_topic = `${settings.get().mqtt.base_topic}/${entity.friendlyName}/`;
|
||||
|
||||
if (payload.command_topic_prefix) {
|
||||
payload.command_topic += `${payload.command_topic_prefix}/`;
|
||||
delete payload.command_topic_prefix;
|
||||
}
|
||||
|
||||
payload.command_topic += 'set';
|
||||
}
|
||||
|
||||
if (payload.set_position_topic && payload.command_topic) {
|
||||
payload.set_position_topic = payload.command_topic;
|
||||
}
|
||||
|
||||
// Override configuration with user settings.
|
||||
if (device.hasOwnProperty('homeassistant')) {
|
||||
const add = (obj) => {
|
||||
Object.keys(obj).forEach((key) => {
|
||||
if (['number', 'string'].includes(typeof obj[key])) {
|
||||
payload[key] = obj[key];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
add(device.homeassistant);
|
||||
|
||||
if (device.homeassistant.hasOwnProperty(config.object_id)) {
|
||||
add(device.homeassistant[config.object_id]);
|
||||
}
|
||||
}
|
||||
|
||||
this.mqtt.publish(topic, JSON.stringify(payload), {retain: true, qos: 0}, null, 'homeassistant');
|
||||
});
|
||||
|
||||
this.discovered[entityID] = true;
|
||||
}
|
||||
|
||||
onMQTTMessage(topic, message) {
|
||||
if (!topic === 'hass/status') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (message.toString().toLowerCase() === 'online') {
|
||||
const timer = setTimeout(() => {
|
||||
// Publish all device states.
|
||||
this.zigbee.getAllClients().forEach((device) => {
|
||||
if (this.state.exists(device.ieeeAddr)) {
|
||||
this.publishEntityState(device.ieeeAddr, this.state.get(device.ieeeAddr), false);
|
||||
}
|
||||
});
|
||||
|
||||
clearTimeout(timer);
|
||||
}, 20000);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
onZigbeeMessage(message, device, mappedModel) {
|
||||
if (device && mappedModel) {
|
||||
this.discover(device.ieeeAddr, mappedModel);
|
||||
}
|
||||
}
|
||||
|
||||
// Only for homeassistant.test.js
|
||||
_getMapping() {
|
||||
return mapping;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = HomeAssistant;
|
||||
@@ -0,0 +1,183 @@
|
||||
const utils = require('../util/utils');
|
||||
const interval = utils.secondsToMilliseconds(1);
|
||||
const logger = require('../util/logger');
|
||||
|
||||
const foundationCfg = {manufSpec: 0, disDefaultRsp: 0};
|
||||
|
||||
/**
|
||||
* Extension required for Livolo device support.
|
||||
*/
|
||||
class Livolo {
|
||||
constructor(zigbee, mqtt, state, publishEntityState) {
|
||||
this.zigbee = zigbee;
|
||||
this.timer = null;
|
||||
this.configured = {};
|
||||
}
|
||||
|
||||
onZigbeeStarted() {
|
||||
this.startTimer();
|
||||
}
|
||||
|
||||
_resetDeviceState(ieeeAddr) {
|
||||
this.configured[ieeeAddr] = {
|
||||
stage: 0,
|
||||
retry: 0,
|
||||
waitresp: false,
|
||||
};
|
||||
}
|
||||
|
||||
onZigbeeMessage(message, device, mappedDevice) {
|
||||
if (!device) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((message.type == 'devInterview') ||
|
||||
(message.type == 'devIncoming') ||
|
||||
(message.type == 'endDeviceAnnce') ||
|
||||
(message.type == 'devStatus')) {
|
||||
if (this.configured.hasOwnProperty(device.ieeeAddr)) {
|
||||
logger.info(`LIVOLO ${device.ieeeAddr}. (Re)joins in the network (after power off?)`);
|
||||
this._resetDeviceState(device.ieeeAddr);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
startTimer() {
|
||||
this.clearTimer();
|
||||
this.timer = setInterval(() => this.handleInterval(), interval);
|
||||
}
|
||||
|
||||
clearTimer() {
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.clearTimer();
|
||||
}
|
||||
|
||||
_handleCommandRespSimple(err, rsp) {
|
||||
this.ext.configured[this.ieeeAddr].waitresp = false;
|
||||
|
||||
if (err) {
|
||||
if (this.ctype === 'toggle') {
|
||||
logger.debug(`LIVOLO ${this.ieeeAddr}. Toggle command error:`, err.message);
|
||||
}
|
||||
} else {
|
||||
logger.debug(`LIVOLO ${this.ieeeAddr}. Sucessfully configured`, rsp);
|
||||
this.ext.configured[this.ieeeAddr].stage = 1; // sucessfully send command
|
||||
this.ext.configured[this.ieeeAddr].retry = 0;
|
||||
this.device.status = 'online';
|
||||
}
|
||||
}
|
||||
|
||||
_handleCommandRespWithData(err, rsp) {
|
||||
this.ext.configured[this.ieeeAddr].waitresp = false;
|
||||
|
||||
if (err) {
|
||||
logger.info(`LIVOLO ${this.ieeeAddr}. ${this.cid}.${this.ctype} response error:`, err.message);
|
||||
if (this.ext.configured[this.ieeeAddr].retry >= 3) {
|
||||
// errors in three sequental reads, stop polling, wait for a device message
|
||||
this.device.status = 'offline';
|
||||
logger.info(`LIVOLO ${this.ieeeAddr}. Stopped polling after 3 unsuccessful attempts`);
|
||||
}
|
||||
} else {
|
||||
this.ext.configured[this.ieeeAddr].retry = 0;
|
||||
this.device.status = 'online';
|
||||
if (this.ext.zigbee) {
|
||||
this.ext.zigbee.shepherd.emit('ind:reported', this.ep, this.cid, rsp, this.ep.last_af_msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// msg: { groupid, clusterid, srcaddr, srcendpoint, dstendpoint, wasbroadcast,
|
||||
// linkquality, securityuse, timestamp, transseqnumber, len, data }
|
||||
_handleAfMessage(msg, ep) {
|
||||
ep.linkquality = msg.linkquality;
|
||||
ep.last_af_msg = msg;
|
||||
}
|
||||
|
||||
_sendToggle(zdev, ieeeAddr, ep, retry) {
|
||||
this.zigbee.queue.push((queueCallback) => {
|
||||
const cfg = {};
|
||||
logger.debug(`LIVOLO ${ieeeAddr}. Sending the 'toggle' command. Retry: ${retry}`);
|
||||
ep.functional('genOnOff', 'toggle', [cfg], foundationCfg,
|
||||
this._handleCommandRespSimple.bind({
|
||||
device: zdev,
|
||||
ieeeAddr,
|
||||
cid: 'genOnOff',
|
||||
ctype: 'toggle',
|
||||
ext: this,
|
||||
}));
|
||||
|
||||
queueCallback();
|
||||
});
|
||||
}
|
||||
|
||||
_sendPoll(zdev, ieeeAddr, ep, retry) {
|
||||
this.zigbee.queue.push((queueCallback) => {
|
||||
ep.foundation('genOnOff', 'read', [{
|
||||
attrId: 0, // onOff
|
||||
}], this._handleCommandRespWithData.bind({
|
||||
device: zdev,
|
||||
ieeeAddr,
|
||||
ep,
|
||||
cid: 'genOnOff',
|
||||
ctype: 'read',
|
||||
ext: this,
|
||||
}));
|
||||
|
||||
queueCallback();
|
||||
});
|
||||
}
|
||||
|
||||
handleInterval() {
|
||||
this.zigbee.getAllClients()
|
||||
.filter((d) => d.manufName && d.manufName.startsWith('LIVOLO')) // LIVOLO
|
||||
.filter((d) => d.type === 'EndDevice') // Filter end devices
|
||||
.filter((d) => d.powerSource && d.powerSource !== 'Battery') // Remove battery powered devices
|
||||
.forEach((d) => {
|
||||
const zdev = this.zigbee.shepherd._findDevByAddr(d.ieeeAddr);
|
||||
if (zdev && zdev.endpoints) {
|
||||
const eplist = Object.keys(zdev.endpoints).filter((epId) => {
|
||||
const ep2 = zdev.getEndpoint(epId);
|
||||
const clist = ep2.getClusterList();
|
||||
return clist && clist.includes(6); // 6 - genOnOff
|
||||
});
|
||||
|
||||
if (eplist.length > 0) {
|
||||
const ep = zdev.getEndpoint(eplist[0]);
|
||||
|
||||
ep.onAfIncomingMsg = this._handleAfMessage;
|
||||
|
||||
if (!this.configured.hasOwnProperty(d.ieeeAddr)) {
|
||||
this._resetDeviceState(d.ieeeAddr);
|
||||
}
|
||||
|
||||
const state = this.configured[d.ieeeAddr];
|
||||
if (state.waitresp) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.retry < 3) {
|
||||
if (state.stage === 0) {
|
||||
state.retry += 1;
|
||||
state.waitresp = true;
|
||||
this._sendToggle(zdev, d.ieeeAddr, ep, state.retry);
|
||||
} else if (state.stage === 1) {
|
||||
state.retry += 1;
|
||||
state.waitresp = true;
|
||||
this._sendPoll(zdev, d.ieeeAddr, ep, state.retry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Livolo;
|
||||
@@ -0,0 +1,100 @@
|
||||
const settings = require('../util/settings');
|
||||
const zigbeeShepherdConverters = require('zigbee-shepherd-converters');
|
||||
|
||||
class NetworkMap {
|
||||
constructor(zigbee, mqtt, state, publishEntityState) {
|
||||
this.zigbee = zigbee;
|
||||
this.mqtt = mqtt;
|
||||
this.state = state;
|
||||
|
||||
// Subscribe to topic.
|
||||
this.topic = `${settings.get().mqtt.base_topic}/bridge/networkmap`;
|
||||
|
||||
// Set supported formats
|
||||
this.supportedFormats = {
|
||||
'raw': this.raw,
|
||||
'graphviz': this.graphviz,
|
||||
};
|
||||
}
|
||||
|
||||
onMQTTConnected() {
|
||||
this.mqtt.subscribe(this.topic);
|
||||
}
|
||||
|
||||
onMQTTMessage(topic, message) {
|
||||
message = message.toString();
|
||||
|
||||
if (topic === this.topic && this.supportedFormats.hasOwnProperty(message)) {
|
||||
this.zigbee.networkScan((result)=> {
|
||||
const converted = this.supportedFormats[message](this.zigbee, result);
|
||||
this.mqtt.publish(`bridge/networkmap/${message}`, converted, {});
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
raw(zigbee, topology) {
|
||||
return JSON.stringify(topology);
|
||||
}
|
||||
|
||||
graphviz(zigbee, topology) {
|
||||
let text = 'digraph G {\nnode[shape=record];\n';
|
||||
let devStyle = '';
|
||||
|
||||
zigbee.getDevices().forEach((device) => {
|
||||
const labels = [];
|
||||
const friendlyDevice = settings.getDevice(device.ieeeAddr);
|
||||
const friendlyName = friendlyDevice ? friendlyDevice.friendly_name : device.ieeeAddr;
|
||||
|
||||
// Add friendly name
|
||||
labels.push(friendlyName);
|
||||
|
||||
// Add the device type
|
||||
labels.push(device.type);
|
||||
|
||||
// Add the device model
|
||||
const mappedModel = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
|
||||
if (mappedModel) {
|
||||
labels.push(`${mappedModel.vendor} ${mappedModel.description} (${mappedModel.model})`);
|
||||
} else {
|
||||
// This model is not supported by zigbee-shepherd-converters, add zigbee model information, if available
|
||||
const zigbeeModel = [device.manufName, device.modelId].filter((a) => a).join(' ');
|
||||
labels.push(zigbeeModel ? zigbeeModel : 'No model information available');
|
||||
}
|
||||
|
||||
// Add the device status (online/offline)
|
||||
labels.push(device.status);
|
||||
|
||||
// Shape the record according to device type
|
||||
if (device.type == 'Coordinator') {
|
||||
devStyle = 'style="bold"';
|
||||
} else if (device.type == 'Router') {
|
||||
devStyle = 'style="rounded"';
|
||||
} else {
|
||||
devStyle = 'style="rounded, dashed"';
|
||||
}
|
||||
|
||||
// Add the device with its labels to the graph as a node.
|
||||
text += ` "${device.ieeeAddr}" [`+devStyle+`, label="{${labels.join('|')}}"];\n`;
|
||||
|
||||
/**
|
||||
* Add an edge between the device and its parent to the graph
|
||||
* NOTE: There are situations where a device is NOT in the topology, this can be e.g.
|
||||
* due to not responded to the lqi scan. In that case we do not add an edge for this device.
|
||||
*/
|
||||
topology.filter((e) => e.ieeeAddr === device.ieeeAddr).forEach((e) => {
|
||||
const lineStyle = (e.lqi==0) ? `style="dashed", ` : ``;
|
||||
text += ` "${device.ieeeAddr}" -> "${e.parent}" [`+lineStyle+`label="${e.lqi}"]\n`;
|
||||
});
|
||||
});
|
||||
|
||||
text += '}';
|
||||
|
||||
return text.replace(/\0/g, '');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = NetworkMap;
|
||||
@@ -0,0 +1,63 @@
|
||||
const settings = require('../util/settings');
|
||||
const logger = require('../util/logger');
|
||||
const utils = require('../util/utils');
|
||||
|
||||
/**
|
||||
* This extensions soft resets the ZNP after a certain timeout.
|
||||
*/
|
||||
class SoftReset {
|
||||
constructor(zigbee, mqtt, state, publishEntityState) {
|
||||
this.zigbee = zigbee;
|
||||
this.timer = null;
|
||||
this.timeout = utils.secondsToMilliseconds(settings.get().advanced.soft_reset_timeout);
|
||||
}
|
||||
|
||||
onZigbeeStarted() {
|
||||
logger.debug(`Soft reset timeout set to ${utils.millisecondsToSeconds(this.timeout)} seconds`);
|
||||
this.resetTimer();
|
||||
}
|
||||
|
||||
clearTimer() {
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
resetTimer() {
|
||||
if (this.timeout === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.clearTimer();
|
||||
this.timer = setTimeout(() => this.handleTimeout(), this.timeout);
|
||||
}
|
||||
|
||||
handleTimeout() {
|
||||
logger.warn('Soft reset timeout triggered');
|
||||
|
||||
this.zigbee.softReset((error) => {
|
||||
if (error) {
|
||||
logger.warn('Soft reset failed, trying stop/start');
|
||||
this.zigbee.stop((error) => {
|
||||
logger.warn('Zigbee stopped');
|
||||
this.zigbee.start((error) => {
|
||||
if (error) {
|
||||
logger.error('Failed to restart!');
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
logger.warn('Soft resetted ZNP due to timeout');
|
||||
}
|
||||
|
||||
this.resetTimer();
|
||||
});
|
||||
}
|
||||
|
||||
onZigbeeMessage(message, device, mappedDevice) {
|
||||
this.resetTimer();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SoftReset;
|
||||
@@ -0,0 +1,63 @@
|
||||
const utils = require('../util/utils');
|
||||
const interval = utils.secondsToMilliseconds(60);
|
||||
|
||||
/**
|
||||
* This extensions handles Xiaomi devices.
|
||||
* - Marks Xiaomi devices as online.
|
||||
* - Polls Xiaomi routers to keep them awake.
|
||||
*/
|
||||
class Xiaomi {
|
||||
constructor(zigbee, mqtt, state, publishEntityState) {
|
||||
this.zigbee = zigbee;
|
||||
this.timer = null;
|
||||
}
|
||||
|
||||
onZigbeeStarted() {
|
||||
// Set all Xiaomi devices to be online, so shepherd won't try
|
||||
// to query info from devices (which would fail because they go to sleep).
|
||||
const devices = this.zigbee.getAllClients();
|
||||
devices.forEach((d) => {
|
||||
if (utils.isXiaomiDevice(d)) {
|
||||
const device = this.zigbee.shepherd.find(d.ieeeAddr, 1);
|
||||
if (device) {
|
||||
device.getDevice().update({
|
||||
status: 'online',
|
||||
joinTime: Math.floor(Date.now() / 1000),
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.startTimer();
|
||||
}
|
||||
|
||||
startTimer() {
|
||||
this.clearTimer();
|
||||
this.timer = setInterval(() => this.handleInterval(), interval);
|
||||
}
|
||||
|
||||
clearTimer() {
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.clearTimer();
|
||||
}
|
||||
|
||||
ping(ieeeAddr) {
|
||||
this.zigbee.ping(ieeeAddr);
|
||||
}
|
||||
|
||||
handleInterval() {
|
||||
this.zigbee.getAllClients()
|
||||
.filter((d) => utils.isXiaomiDevice(d)) // Filter Xiaomi devices
|
||||
.filter((d) => d.type === 'Router') // Filter routers
|
||||
.filter((d) => d.powerSource && d.powerSource !== 'Battery') // Remove battery powered devices
|
||||
.forEach((d) => this.ping(d.ieeeAddr)); // Ping devices.
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Xiaomi;
|
||||
@@ -1,372 +0,0 @@
|
||||
const settings = require('./util/settings');
|
||||
|
||||
const configurations = {
|
||||
// Binary sensor
|
||||
'binary_sensor_occupancy': {
|
||||
type: 'binary_sensor',
|
||||
object_id: 'occupancy',
|
||||
discovery_payload: {
|
||||
payload_on: true,
|
||||
payload_off: false,
|
||||
value_template: '{{ value_json.occupancy }}',
|
||||
device_class: 'motion',
|
||||
json_attributes: ['battery', 'voltage'],
|
||||
},
|
||||
},
|
||||
'binary_sensor_contact': {
|
||||
type: 'binary_sensor',
|
||||
object_id: 'contact',
|
||||
discovery_payload: {
|
||||
payload_on: false,
|
||||
payload_off: true,
|
||||
value_template: '{{ value_json.contact }}',
|
||||
device_class: 'door',
|
||||
json_attributes: ['battery', 'voltage'],
|
||||
},
|
||||
},
|
||||
'binary_sensor_water_leak': {
|
||||
type: 'binary_sensor',
|
||||
object_id: 'water_leak',
|
||||
discovery_payload: {
|
||||
payload_on: true,
|
||||
payload_off: false,
|
||||
value_template: '{{ value_json.water_leak }}',
|
||||
device_class: 'moisture',
|
||||
json_attributes: ['battery', 'voltage'],
|
||||
},
|
||||
},
|
||||
'binary_sensor_smoke': {
|
||||
type: 'binary_sensor',
|
||||
object_id: 'smoke',
|
||||
discovery_payload: {
|
||||
payload_on: true,
|
||||
payload_off: false,
|
||||
value_template: '{{ value_json.smoke }}',
|
||||
device_class: 'smoke',
|
||||
json_attributes: ['battery', 'voltage'],
|
||||
},
|
||||
},
|
||||
'binary_sensor_router': {
|
||||
type: 'binary_sensor',
|
||||
object_id: 'router',
|
||||
discovery_payload: {
|
||||
payload_on: true,
|
||||
payload_off: false,
|
||||
value_template: '{{ value_json.state }}',
|
||||
device_class: 'connectivity',
|
||||
json_attributes: ['description', 'type', 'rssi'],
|
||||
},
|
||||
},
|
||||
|
||||
// Sensor
|
||||
'sensor_illuminance': {
|
||||
type: 'sensor',
|
||||
object_id: 'illuminance',
|
||||
discovery_payload: {
|
||||
unit_of_measurement: 'lx',
|
||||
icon: 'mdi:theme-light-dark',
|
||||
value_template: '{{ value_json.illuminance }}',
|
||||
json_attributes: ['battery', 'voltage'],
|
||||
},
|
||||
},
|
||||
'sensor_humidity': {
|
||||
type: 'sensor',
|
||||
object_id: 'humidity',
|
||||
discovery_payload: {
|
||||
unit_of_measurement: '%',
|
||||
icon: 'mdi:water-percent',
|
||||
value_template: '{{ value_json.humidity }}',
|
||||
json_attributes: ['battery', 'voltage'],
|
||||
},
|
||||
},
|
||||
'sensor_temperature': {
|
||||
type: 'sensor',
|
||||
object_id: 'temperature',
|
||||
discovery_payload: {
|
||||
unit_of_measurement: '°C',
|
||||
icon: 'mdi:temperature-celsius',
|
||||
value_template: '{{ value_json.temperature }}',
|
||||
json_attributes: ['battery', 'voltage'],
|
||||
},
|
||||
},
|
||||
'sensor_pressure': {
|
||||
type: 'sensor',
|
||||
object_id: 'pressure',
|
||||
discovery_payload: {
|
||||
unit_of_measurement: 'Pa',
|
||||
icon: 'mdi:speedometer',
|
||||
value_template: '{{ value_json.pressure }}',
|
||||
json_attributes: ['battery', 'voltage'],
|
||||
},
|
||||
},
|
||||
'sensor_click': {
|
||||
type: 'sensor',
|
||||
object_id: 'click',
|
||||
discovery_payload: {
|
||||
icon: 'mdi:toggle-switch',
|
||||
value_template: '{{ value_json.click }}',
|
||||
json_attributes: ['battery', 'voltage', 'action', 'duration'],
|
||||
},
|
||||
},
|
||||
'sensor_power': {
|
||||
type: 'sensor',
|
||||
object_id: 'power',
|
||||
discovery_payload: {
|
||||
unit_of_measurement: 'Watt',
|
||||
icon: 'mdi:flash',
|
||||
value_template: '{{ value_json.power }}',
|
||||
json_attributes: ['voltage', 'temperature', 'consumption', 'current', 'power_factor'],
|
||||
},
|
||||
},
|
||||
'sensor_action': {
|
||||
type: 'sensor',
|
||||
object_id: 'action',
|
||||
discovery_payload: {
|
||||
icon: 'mdi:gesture-double-tap',
|
||||
value_template: '{{ value_json.action }}',
|
||||
json_attributes: ['battery', 'voltage', 'angle', 'side', 'from_side', 'to_side', 'brightness'],
|
||||
},
|
||||
},
|
||||
'sensor_brightness': {
|
||||
type: 'sensor',
|
||||
object_id: 'brightness',
|
||||
discovery_payload: {
|
||||
unit_of_measurement: 'brightness',
|
||||
icon: 'mdi:brightness-5',
|
||||
value_template: '{{ value_json.brightness }}',
|
||||
json_attributes: [],
|
||||
},
|
||||
},
|
||||
|
||||
// Light
|
||||
'light_brightness_colortemp_xy': {
|
||||
type: 'light',
|
||||
object_id: 'light',
|
||||
discovery_payload: {
|
||||
brightness: true,
|
||||
color_temp: true,
|
||||
xy: true,
|
||||
platform: 'mqtt_json',
|
||||
command_topic: true,
|
||||
},
|
||||
},
|
||||
'light_brightness_xy': {
|
||||
type: 'light',
|
||||
object_id: 'light',
|
||||
discovery_payload: {
|
||||
brightness: true,
|
||||
xy: true,
|
||||
platform: 'mqtt_json',
|
||||
command_topic: true,
|
||||
},
|
||||
},
|
||||
'light_brightness_colortemp': {
|
||||
type: 'light',
|
||||
object_id: 'light',
|
||||
discovery_payload: {
|
||||
brightness: true,
|
||||
color_temp: true,
|
||||
platform: 'mqtt_json',
|
||||
command_topic: true,
|
||||
},
|
||||
},
|
||||
'light_brightness': {
|
||||
type: 'light',
|
||||
object_id: 'light',
|
||||
discovery_payload: {
|
||||
brightness: true,
|
||||
platform: 'mqtt_json',
|
||||
command_topic: true,
|
||||
},
|
||||
},
|
||||
|
||||
// Switch
|
||||
'switch': {
|
||||
type: 'switch',
|
||||
object_id: 'switch',
|
||||
discovery_payload: {
|
||||
payload_off: 'OFF',
|
||||
payload_on: 'ON',
|
||||
value_template: '{{ value_json.state }}',
|
||||
command_topic: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const switchWithPostfix = (postfix) => {
|
||||
return {
|
||||
type: 'switch',
|
||||
object_id: `switch_${postfix}`,
|
||||
discovery_payload: {
|
||||
payload_off: 'OFF',
|
||||
payload_on: 'ON',
|
||||
value_template: `{{ value_json.state_${postfix} }}`,
|
||||
command_topic: true,
|
||||
command_topic_prefix: postfix,
|
||||
json_attributes: [`button_${postfix}`],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// Map homeassitant configurations to devices.
|
||||
const mapping = {
|
||||
'WXKG01LM': [configurations.sensor_click],
|
||||
'WXKG11LM': [configurations.sensor_click],
|
||||
'WXKG12LM': [configurations.sensor_click],
|
||||
'WXKG03LM': [configurations.sensor_click],
|
||||
'WXKG02LM': [configurations.sensor_click],
|
||||
'QBKG04LM': [configurations.switch],
|
||||
'QBKG03LM': [switchWithPostfix('left'), switchWithPostfix('right')],
|
||||
'WSDCGQ01LM': [configurations.sensor_temperature, configurations.sensor_humidity],
|
||||
'WSDCGQ11LM': [configurations.sensor_temperature, configurations.sensor_humidity, configurations.sensor_pressure],
|
||||
'RTCGQ01LM': [configurations.binary_sensor_occupancy],
|
||||
'RTCGQ11LM': [configurations.binary_sensor_occupancy, configurations.sensor_illuminance],
|
||||
'MCCGQ01LM': [configurations.binary_sensor_contact],
|
||||
'MCCGQ11LM': [configurations.binary_sensor_contact],
|
||||
'SJCGQ11LM': [configurations.binary_sensor_water_leak],
|
||||
'MFKZQ01LM': [configurations.sensor_action],
|
||||
'ZNCZ02LM': [configurations.switch, configurations.sensor_power],
|
||||
'QBCZ11LM': [configurations.switch, configurations.sensor_power],
|
||||
'LED1545G12': [configurations.light_brightness_colortemp],
|
||||
'LED1623G12': [configurations.light_brightness],
|
||||
'LED1622G12': [configurations.light_brightness],
|
||||
'LED1537R6': [configurations.light_brightness_colortemp],
|
||||
'LED1650R5': [configurations.light_brightness],
|
||||
'LED1536G5': [configurations.light_brightness_colortemp],
|
||||
'7299760PH': [configurations.light_brightness_xy],
|
||||
'7146060PH': [configurations.light_brightness_colortemp_xy],
|
||||
'F7C033': [configurations.light_brightness],
|
||||
'JTYJ-GD-01LM/BW': [configurations.binary_sensor_smoke],
|
||||
'PLUG EDP RE:DY': [configurations.switch, configurations.sensor_power],
|
||||
'CC2530.ROUTER': [configurations.binary_sensor_router],
|
||||
'AA70155': [configurations.light_brightness_colortemp],
|
||||
'AA69697': [configurations.light_brightness_colortemp_xy],
|
||||
'HALIGHTDIMWWE27': [configurations.light_brightness],
|
||||
'AB3257001NJ': [configurations.switch],
|
||||
'8718696449691': [configurations.light_brightness],
|
||||
'RB 185 C': [configurations.light_brightness_colortemp_xy],
|
||||
'9290012573A': [configurations.light_brightness_colortemp_xy],
|
||||
'LED1624G9': [configurations.light_brightness_xy],
|
||||
'73742': [configurations.light_brightness_colortemp],
|
||||
'73740': [configurations.light_brightness_colortemp],
|
||||
'22670': [configurations.light_brightness],
|
||||
'ICTC-G-1': [configurations.sensor_brightness],
|
||||
'ICPSHC24-30EU-IL-1': [configurations.light_brightness],
|
||||
'45852GE': [configurations.light_brightness],
|
||||
'E11-G13': [configurations.light_brightness],
|
||||
'LED1649C5': [configurations.light_brightness],
|
||||
'ICPSHC24-10EU-IL-1': [configurations.light_brightness],
|
||||
'LED1546G12': [configurations.light_brightness_colortemp],
|
||||
'L1527': [configurations.light_brightness_colortemp],
|
||||
'L1529': [configurations.light_brightness_colortemp],
|
||||
'L1528': [configurations.light_brightness_colortemp],
|
||||
'RB 165': [configurations.light_brightness],
|
||||
'RB 175 W': [configurations.light_brightness],
|
||||
'RS 125': [configurations.light_brightness],
|
||||
'RB 145': [configurations.light_brightness],
|
||||
'PL 110': [configurations.light_brightness],
|
||||
'ST 110': [configurations.light_brightness],
|
||||
'UC 110': [configurations.light_brightness],
|
||||
'DL 110 N': [configurations.light_brightness],
|
||||
'DL 110 W': [configurations.light_brightness],
|
||||
'SL 110 N': [configurations.light_brightness],
|
||||
'SL 110 M': [configurations.light_brightness],
|
||||
'SL 110 W': [configurations.light_brightness],
|
||||
'AA68199': [configurations.light_brightness_colortemp],
|
||||
'QBKG11LM': [configurations.switch, configurations.sensor_power],
|
||||
'QBKG12LM': [switchWithPostfix('left'), switchWithPostfix('right'), configurations.sensor_power],
|
||||
'K2RGBW01': [configurations.light_brightness_colortemp_xy],
|
||||
'9290011370': [configurations.light_brightness],
|
||||
'DNCKATSW001': [configurations.switch],
|
||||
'Z809A': [configurations.switch, configurations.sensor_power],
|
||||
'NL08-0800': [configurations.light_brightness],
|
||||
'915005106701': [configurations.light_brightness_colortemp_xy],
|
||||
'AB32840': [configurations.light_brightness_colortemp],
|
||||
'8718696485880': [configurations.light_brightness_colortemp_xy],
|
||||
'8718696598283': [configurations.light_brightness_colortemp],
|
||||
'73693': [configurations.light_brightness_colortemp_xy],
|
||||
'324131092621': [configurations.sensor_action],
|
||||
'GL-C-008': [configurations.light_brightness_colortemp_xy],
|
||||
'STSS-MULT-001': [configurations.binary_sensor_contact],
|
||||
'E11-G23': [configurations.light_brightness],
|
||||
'AC03845': [configurations.light_brightness_colortemp_xy],
|
||||
'AC03641': [configurations.light_brightness],
|
||||
'FB56+ZSW05HG1.2': [configurations.switch],
|
||||
'72922-A': [configurations.switch],
|
||||
'AC03642': [configurations.light_brightness_colortemp],
|
||||
'DNCKATSW002': [switchWithPostfix('left'), switchWithPostfix('right')],
|
||||
'DNCKATSW003': [switchWithPostfix('left'), switchWithPostfix('right'), switchWithPostfix('center')],
|
||||
'DNCKATSW004': [
|
||||
switchWithPostfix('bottom_left'), switchWithPostfix('bottom_right'),
|
||||
switchWithPostfix('top_left'), switchWithPostfix('top_right'),
|
||||
],
|
||||
'BY 165': [configurations.light_brightness],
|
||||
'ZLED-2709': [configurations.light_brightness],
|
||||
'8718696548738': [configurations.light_brightness_colortemp],
|
||||
'4052899926110': [configurations.light_brightness_colortemp_xy],
|
||||
'Z01-CIA19NAE26': [configurations.light_brightness],
|
||||
};
|
||||
|
||||
// A map of all discoverd devices
|
||||
const discovered = {};
|
||||
|
||||
function discover(deviceID, model, mqtt, force) {
|
||||
// Check if already discoverd and check if there are configs.
|
||||
const discover = force || !discovered[deviceID];
|
||||
if (!discover || !mapping[model] || !settings.getDevice(deviceID)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const friendlyName = settings.getDevice(deviceID).friendly_name;
|
||||
|
||||
mapping[model].forEach((config) => {
|
||||
const topic = `${config.type}/${deviceID}/${config.object_id}/config`;
|
||||
const payload = config.discovery_payload;
|
||||
payload.state_topic = `${settings.get().mqtt.base_topic}/${friendlyName}`;
|
||||
payload.availability_topic = `${settings.get().mqtt.base_topic}/bridge/state`;
|
||||
|
||||
// Set unique names in cases this device produces multiple entities in homeassistant.
|
||||
payload.name = mapping[model].length > 1 ? `${friendlyName}_${config.object_id}` : friendlyName;
|
||||
|
||||
// Only set unique_id when user did not set a friendly_name yet,
|
||||
// see https://github.com/Koenkk/zigbee2mqtt/issues/138
|
||||
if (deviceID === friendlyName) {
|
||||
payload.unique_id = `${deviceID}_${config.object_id}_${settings.get().mqtt.base_topic}`;
|
||||
}
|
||||
|
||||
if (payload.command_topic) {
|
||||
payload.command_topic = `${settings.get().mqtt.base_topic}/${friendlyName}/`;
|
||||
|
||||
if (payload.command_topic_prefix) {
|
||||
payload.command_topic += `${payload.command_topic_prefix}/`;
|
||||
}
|
||||
|
||||
payload.command_topic += 'set';
|
||||
}
|
||||
|
||||
mqtt.publish(topic, JSON.stringify(payload), {retain: true, qos: 0}, null, 'homeassistant');
|
||||
});
|
||||
|
||||
discovered[deviceID] = true;
|
||||
}
|
||||
|
||||
function clear(deviceID, model, mqtt) {
|
||||
// Check if there are configs.
|
||||
if (!mapping[model]) {
|
||||
return;
|
||||
}
|
||||
|
||||
mapping[model].forEach((config) => {
|
||||
const topic = `${config.type}/${deviceID}/${config.object_id}/config`;
|
||||
const payload = '';
|
||||
mqtt.publish(topic, payload, {retain: true, qos: 0}, null, 'homeassistant');
|
||||
});
|
||||
|
||||
discovered[deviceID] = false;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
mapping: mapping,
|
||||
discover: (deviceID, model, mqtt, force) => discover(deviceID, model, mqtt, force),
|
||||
clear: (deviceID, model, mqtt) => clear(deviceID, model, mqtt),
|
||||
};
|
||||
+30
-22
@@ -4,17 +4,18 @@ const settings = require('./util/settings');
|
||||
|
||||
class MQTT {
|
||||
constructor() {
|
||||
this.handleConnect = this.handleConnect.bind(this);
|
||||
this.handleMessage = this.handleMessage.bind(this);
|
||||
this.onConnect = this.onConnect.bind(this);
|
||||
this.onMessage = this.onMessage.bind(this);
|
||||
this.messageHandler = null;
|
||||
}
|
||||
|
||||
connect(onMessage, subscriptions, callback) {
|
||||
connect(messageHandler, callback) {
|
||||
const mqttSettings = settings.get().mqtt;
|
||||
logger.info(`Connecting to MQTT server at ${mqttSettings.server}`);
|
||||
|
||||
const options = {
|
||||
will: {
|
||||
topic: this.prefixTopic('bridge/state'),
|
||||
topic: `${settings.get().mqtt.base_topic}/bridge/state`,
|
||||
payload: 'offline',
|
||||
retain: true,
|
||||
},
|
||||
@@ -30,15 +31,20 @@ class MQTT {
|
||||
options.clientId = mqttSettings.client_id;
|
||||
}
|
||||
|
||||
if (mqttSettings.hasOwnProperty('reject_unauthorized') && !mqttSettings.reject_unauthorized) {
|
||||
logger.debug(`MQTT reject_unauthorized set false, ignoring certificate warnings.`);
|
||||
options.rejectUnauthorized = false;
|
||||
}
|
||||
|
||||
this.client = mqtt.connect(mqttSettings.server, options);
|
||||
|
||||
// Register callbacks.
|
||||
this.client.on('connect', () => {
|
||||
this.handleConnect();
|
||||
this.onConnect();
|
||||
callback();
|
||||
});
|
||||
|
||||
this.client.on('message', this.handleMessage);
|
||||
this.client.on('message', this.onMessage);
|
||||
|
||||
// Set timer at interval to check if connected to MQTT server.
|
||||
const interval = 10 * 1000; // seconds * 1000.
|
||||
@@ -48,8 +54,7 @@ class MQTT {
|
||||
}
|
||||
}, interval);
|
||||
|
||||
this.onMessage = onMessage;
|
||||
this.subscriptions = subscriptions;
|
||||
this.messageHandler = messageHandler;
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
@@ -62,20 +67,23 @@ class MQTT {
|
||||
});
|
||||
}
|
||||
|
||||
handleConnect() {
|
||||
onConnect() {
|
||||
logger.info('Connected to MQTT server');
|
||||
this.publish('bridge/state', 'online', {retain: true, qos: 0});
|
||||
this.subscriptions.forEach((topic) => this.client.subscribe(topic));
|
||||
}
|
||||
|
||||
handleMessage(topic, message) {
|
||||
if (this.onMessage) {
|
||||
this.onMessage(topic, message);
|
||||
subscribe(topic) {
|
||||
this.client.subscribe(topic);
|
||||
}
|
||||
|
||||
onMessage(topic, message) {
|
||||
if (this.messageHandler) {
|
||||
this.messageHandler(topic, message);
|
||||
}
|
||||
}
|
||||
|
||||
publish(topic, payload, options, callback, baseTopic) {
|
||||
topic = this.prefixTopic(topic, baseTopic);
|
||||
publish(topic, payload, options, callback, base=settings.get().mqtt.base_topic) {
|
||||
topic = `${base}/${topic}`;
|
||||
|
||||
if (!this.client || this.client.reconnecting) {
|
||||
logger.error(`Not connected to MQTT server!`);
|
||||
@@ -83,17 +91,17 @@ class MQTT {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`MQTT publish, topic: '${topic}', payload: '${payload}'`);
|
||||
logger.info(`MQTT publish: topic '${topic}', payload '${payload}'`);
|
||||
this.client.publish(topic, payload, options, callback);
|
||||
}
|
||||
|
||||
prefixTopic(topic, baseTopic) {
|
||||
baseTopic = baseTopic ? baseTopic : settings.get().mqtt.base_topic;
|
||||
return `${baseTopic}/${topic}`;
|
||||
}
|
||||
log(type, message, meta=null) {
|
||||
const payload = {type, message};
|
||||
|
||||
if (meta) {
|
||||
payload.meta = meta;
|
||||
}
|
||||
|
||||
log(type, message) {
|
||||
const payload = {type: type, message: message};
|
||||
this.publish('bridge/log', JSON.stringify(payload), {retain: false});
|
||||
}
|
||||
}
|
||||
|
||||
+51
-10
@@ -1,6 +1,7 @@
|
||||
const logger = require('./util/logger');
|
||||
const data = require('./util/data');
|
||||
const fs = require('fs');
|
||||
const settings = require('./util/settings');
|
||||
|
||||
const saveInterval = 1000 * 60 * 5; // 5 minutes
|
||||
|
||||
@@ -8,10 +9,50 @@ class State {
|
||||
constructor() {
|
||||
this.state = {};
|
||||
this.file = data.joinPath('state.json');
|
||||
this.timer = null;
|
||||
|
||||
this.handleSettingsChanged = this.handleSettingsChanged.bind(this);
|
||||
}
|
||||
|
||||
start() {
|
||||
this._load();
|
||||
|
||||
// Save the state on every interval
|
||||
this.clearTimer();
|
||||
this.timer = setInterval(() => this.save(), saveInterval);
|
||||
|
||||
// Listen for on settings changed events.
|
||||
settings.addOnChangeHandler(this.handleSettingsChanged);
|
||||
|
||||
this.checkLastSeen();
|
||||
}
|
||||
|
||||
handleSettingsChanged() {
|
||||
this.checkLastSeen();
|
||||
}
|
||||
|
||||
checkLastSeen() {
|
||||
if (settings.get().advanced.last_seen === 'disable') {
|
||||
Object.values(this.state).forEach((s) => {
|
||||
if (s.hasOwnProperty('last_seen')) {
|
||||
delete s.last_seen;
|
||||
}
|
||||
});
|
||||
|
||||
this.save();
|
||||
}
|
||||
}
|
||||
|
||||
clearTimer() {
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.clearTimer();
|
||||
this.save();
|
||||
}
|
||||
|
||||
_load() {
|
||||
@@ -23,7 +64,7 @@ class State {
|
||||
logger.debug(`Failed to load state from file ${this.file} (corrupt file?)`);
|
||||
}
|
||||
} else {
|
||||
logger.debug(`Can't load state from file ${this.file} (doesn't exsist)`);
|
||||
logger.debug(`Can't load state from file ${this.file} (doesn't exist)`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,21 +74,21 @@ class State {
|
||||
fs.writeFileSync(this.file, json, 'utf8');
|
||||
}
|
||||
|
||||
exists(ID) {
|
||||
return this.state.hasOwnProperty(ID);
|
||||
exists(ieeeAddr) {
|
||||
return this.state.hasOwnProperty(ieeeAddr);
|
||||
}
|
||||
|
||||
get(ID) {
|
||||
return this.state[ID];
|
||||
get(ieeeAddr) {
|
||||
return this.state[ieeeAddr];
|
||||
}
|
||||
|
||||
set(ID, state) {
|
||||
this.state[ID] = state;
|
||||
set(ieeeAddr, state) {
|
||||
this.state[ieeeAddr] = state;
|
||||
}
|
||||
|
||||
remove(ID) {
|
||||
if (this.exists(ID)) {
|
||||
delete this.state[ID];
|
||||
remove(ieeeAddr) {
|
||||
if (this.exists(ieeeAddr)) {
|
||||
delete this.state[ieeeAddr];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
-5
@@ -2,14 +2,21 @@ const path = require('path');
|
||||
|
||||
let dataPath = null;
|
||||
|
||||
if (process.env.ZIGBEE2MQTT_DATA) {
|
||||
dataPath = process.env.ZIGBEE2MQTT_DATA;
|
||||
} else {
|
||||
dataPath = path.join(__dirname, '..', '..', 'data');
|
||||
dataPath = path.normalize(dataPath);
|
||||
function load() {
|
||||
if (process.env.ZIGBEE2MQTT_DATA) {
|
||||
dataPath = process.env.ZIGBEE2MQTT_DATA;
|
||||
} else {
|
||||
dataPath = path.join(__dirname, '..', '..', 'data');
|
||||
dataPath = path.normalize(dataPath);
|
||||
}
|
||||
}
|
||||
|
||||
load();
|
||||
|
||||
module.exports = {
|
||||
joinPath: (file) => path.join(dataPath, file),
|
||||
getPath: () => dataPath,
|
||||
|
||||
// For test only.
|
||||
_reload: () => load(),
|
||||
};
|
||||
|
||||
+27
-35
@@ -1,6 +1,5 @@
|
||||
const winston = require('winston');
|
||||
const moment = require('moment');
|
||||
const data = require('./data');
|
||||
const settings = require('./settings');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
@@ -8,43 +7,32 @@ const fx = require('mkdir-recursive');
|
||||
const rimraf = require('rimraf');
|
||||
|
||||
// Determine the log level.
|
||||
let level = winston.level.info;
|
||||
if (process.env.DEBUG) {
|
||||
level = 'debug';
|
||||
} else if (settings.get().advanced && settings.get().advanced.log_level) {
|
||||
level = settings.get().advanced.log_level;
|
||||
}
|
||||
const level = settings.get().advanced.log_level;
|
||||
|
||||
// Directoy to log to
|
||||
let rootDirectory = path.join(data.getPath(), 'log');
|
||||
if (settings.get().advanced && settings.get().advanced.log_directory) {
|
||||
rootDirectory = settings.get().advanced.log_directory;
|
||||
}
|
||||
|
||||
// Add start time to directory.
|
||||
const directory = path.join(rootDirectory, moment(Date.now()).format('YYYY-MM-DD.HH:mm:ss'));
|
||||
const timestamp = moment(Date.now()).format('YYYY-MM-DD.HH-mm-ss');
|
||||
const directory = settings.get().advanced.log_directory.replace('%TIMESTAMP%', timestamp);
|
||||
|
||||
// Make sure that log directoy exsists
|
||||
fx.mkdirSync(directory);
|
||||
|
||||
// Create logger
|
||||
const logger = new (winston.Logger)({
|
||||
const logger = new winston.Logger({
|
||||
transports: [
|
||||
new (winston.transports.File)({
|
||||
new winston.transports.File({
|
||||
filename: path.join(directory, 'log.txt'),
|
||||
json: false,
|
||||
level: level,
|
||||
maxFiles: 3, // Max 3 files per run.
|
||||
maxsize: 10000000, // 10MB // Only if Filename is static!
|
||||
level,
|
||||
maxFiles: 3, // Keep last 3 files
|
||||
maxsize: 10000000, // 10MB
|
||||
timestamp: () => new Date().toLocaleString(),
|
||||
}),
|
||||
new (winston.transports.Console)({
|
||||
new winston.transports.Console({
|
||||
timestamp: () => new Date().toLocaleString(),
|
||||
formatter: function(options) {
|
||||
return winston.config.colorize(options.level, ' zigbee2mqtt:' + options.level.toLowerCase()) + ' ' +
|
||||
options.timestamp() + ' ' + (options.message ? options.message : '') +
|
||||
(options.meta && Object.keys(options.meta).length ? '\n\t'+ JSON.stringify(options.meta) : '' );
|
||||
},
|
||||
formatter: (options) =>
|
||||
winston.config.colorize(options.level, ' zigbee2mqtt:' + options.level.toLowerCase()) + ' ' +
|
||||
options.timestamp() + ' ' + (options.message ? options.message : '') +
|
||||
(options.meta && Object.keys(options.meta).length ? '\n\t'+ JSON.stringify(options.meta) : '' ),
|
||||
}),
|
||||
],
|
||||
});
|
||||
@@ -54,16 +42,20 @@ logger.info(`Logging to directory: '${directory}'`);
|
||||
logger.transports.console.level = level;
|
||||
|
||||
// Cleanup any old log directory.
|
||||
let directories = fs.readdirSync(rootDirectory).map((d) => {
|
||||
d = path.join(rootDirectory, d);
|
||||
return {path: d, birth: fs.statSync(d).birthtimeMs};
|
||||
});
|
||||
if (settings.get().advanced.log_directory.includes('%TIMESTAMP%')) {
|
||||
const rootDirectory = path.join(directory, '..');
|
||||
|
||||
directories.sort((a, b) => b.birth - a.birth);
|
||||
directories = directories.slice(10, directories.length);
|
||||
directories.forEach((dir) => {
|
||||
logger.debug(`Removing old log directory '${dir.path}'`);
|
||||
rimraf.sync(dir.path);
|
||||
});
|
||||
let directories = fs.readdirSync(rootDirectory).map((d) => {
|
||||
d = path.join(rootDirectory, d);
|
||||
return {path: d, birth: fs.statSync(d).birthtimeMs};
|
||||
});
|
||||
|
||||
directories.sort((a, b) => b.birth - a.birth);
|
||||
directories = directories.slice(10, directories.length);
|
||||
directories.forEach((dir) => {
|
||||
logger.debug(`Removing old log directory '${dir.path}'`);
|
||||
rimraf.sync(dir.path);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = logger;
|
||||
|
||||
+228
-25
@@ -2,66 +2,269 @@ const yaml = require('js-yaml');
|
||||
const fs = require('fs');
|
||||
const data = require('./data');
|
||||
const file = data.joinPath('configuration.yaml');
|
||||
const objectAssignDeep = require(`object-assign-deep`);
|
||||
const path = require('path');
|
||||
|
||||
let settings = read();
|
||||
const onChangeHandlers = [];
|
||||
|
||||
const defaults = {
|
||||
permit_join: false,
|
||||
mqtt: {
|
||||
include_device_information: false,
|
||||
},
|
||||
groups: {},
|
||||
device_options: {},
|
||||
experimental: {
|
||||
livolo: false,
|
||||
// json or attribute
|
||||
output: 'json',
|
||||
},
|
||||
advanced: {
|
||||
log_directory: path.join(data.getPath(), 'log', '%TIMESTAMP%'),
|
||||
log_level: process.env.DEBUG ? 'debug' : 'info',
|
||||
soft_reset_timeout: 0,
|
||||
pan_id: 0x1a62,
|
||||
ext_pan_id: [0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD],
|
||||
channel: 11,
|
||||
baudrate: 115200,
|
||||
rtscts: true,
|
||||
|
||||
// Availability timeout in seconds, disabled by default.
|
||||
availability_timeout: 0,
|
||||
availability_blacklist: [],
|
||||
|
||||
/**
|
||||
* Home Assistant requires ALL attributes to be present in ALL MQTT messages send by the device.
|
||||
* https://community.home-assistant.io/t/missing-value-with-mqtt-only-last-data-set-is-shown/47070/9
|
||||
*
|
||||
* Therefore zigbee2mqtt BY DEFAULT caches all values and resend it with every message.
|
||||
* advanced.cache_state in configuration.yaml allows to configure this.
|
||||
* https://www.zigbee2mqtt.io/configuration/configuration.html
|
||||
*/
|
||||
cache_state: true,
|
||||
|
||||
/**
|
||||
* Add a last_seen attribute to mqtt messages, contains date/time of zigbee message arrival
|
||||
* "ISO_8601": ISO 8601 format
|
||||
* "ISO_8601_local": Local ISO 8601 format (instead of UTC-based)
|
||||
* "epoch": milliseconds elapsed since the UNIX epoch
|
||||
* "disable": no last_seen attribute (default)
|
||||
*/
|
||||
last_seen: 'disable',
|
||||
|
||||
// Optional: Add an elapsed attribute to MQTT messages, contains milliseconds since the previous msg
|
||||
elapsed: false,
|
||||
|
||||
/**
|
||||
* https://github.com/Koenkk/zigbee2mqtt/issues/685#issuecomment-449112250
|
||||
*
|
||||
* Network key will serve as the encryption key of your network.
|
||||
* Changing this will require you to repair your devices.
|
||||
*/
|
||||
network_key: [1, 3, 5, 7, 9, 11, 13, 15, 0, 2, 4, 6, 8, 10, 12, 13],
|
||||
|
||||
/**
|
||||
* Enables reporting feature
|
||||
*/
|
||||
report: false,
|
||||
},
|
||||
};
|
||||
|
||||
let _settings;
|
||||
|
||||
const getSettings = () => _settings||(_settings = read());
|
||||
|
||||
function writeRead() {
|
||||
write();
|
||||
settings = read();
|
||||
_settings = read();
|
||||
onChangeHandlers.forEach((handler) => handler());
|
||||
}
|
||||
|
||||
function write() {
|
||||
fs.writeFileSync(file, yaml.safeDump(settings));
|
||||
const settings = getSettings();
|
||||
const toWrite = objectAssignDeep.noMutate(settings);
|
||||
|
||||
// Read settings to check if we have to split devices/groups into seperate file.
|
||||
const actual = readYaml(file);
|
||||
if (typeof actual.devices === 'string') {
|
||||
writeYaml(data.joinPath(actual.devices), settings.devices);
|
||||
toWrite.devices = actual.devices;
|
||||
}
|
||||
|
||||
if (typeof actual.groups === 'string') {
|
||||
writeYaml(data.joinPath(actual.groups), settings.groups);
|
||||
toWrite.groups = actual.groups;
|
||||
}
|
||||
|
||||
writeYaml(file, toWrite);
|
||||
}
|
||||
|
||||
function read() {
|
||||
function readYaml(file) {
|
||||
return yaml.safeLoad(fs.readFileSync(file, 'utf8'));
|
||||
}
|
||||
|
||||
function addDevice(id) {
|
||||
function writeYaml(file, content) {
|
||||
fs.writeFileSync(file, yaml.safeDump(content));
|
||||
}
|
||||
|
||||
function read() {
|
||||
const s = readYaml(file);
|
||||
|
||||
// Read devices/groups configuration from separate file.
|
||||
if (typeof s.devices === 'string') {
|
||||
const file = data.joinPath(s.devices);
|
||||
s.devices = fs.existsSync(file) ? readYaml(file) : null;
|
||||
}
|
||||
|
||||
if (typeof s.groups === 'string') {
|
||||
const file = data.joinPath(s.groups);
|
||||
s.groups = fs.existsSync(file) ? readYaml(file) : null;
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
function set(path, value) {
|
||||
let obj = getSettings();
|
||||
|
||||
for (let i = 0; i < path.length; i++) {
|
||||
const key = path[i];
|
||||
if (i === path.length - 1) {
|
||||
obj[key] = value;
|
||||
} else {
|
||||
if (!obj[key]) {
|
||||
obj[key] = {};
|
||||
}
|
||||
|
||||
obj = obj[key];
|
||||
}
|
||||
}
|
||||
|
||||
writeRead();
|
||||
}
|
||||
|
||||
const getDevices = () => getSettings().devices || [];
|
||||
|
||||
const getDevice = (ieeeAddr) => getDevices()[ieeeAddr];
|
||||
|
||||
const getGroups = () => getSettings().groups || [];
|
||||
|
||||
const getGroup = (ID) => getGroups()[ID];
|
||||
|
||||
|
||||
function addDevice(ieeeAddr) {
|
||||
const settings = getSettings();
|
||||
if (!settings.devices) {
|
||||
settings.devices = {};
|
||||
}
|
||||
|
||||
settings.devices[id] = {friendly_name: id, retain: false};
|
||||
settings.devices[ieeeAddr] = {friendly_name: ieeeAddr, retain: false};
|
||||
writeRead();
|
||||
}
|
||||
|
||||
function removeDevice(id) {
|
||||
if (settings.devices && settings.devices[id]) {
|
||||
delete settings.devices[id];
|
||||
writeRead();
|
||||
}
|
||||
function removeDevice(ieeeAddr) {
|
||||
const settings = getSettings();
|
||||
if (!settings.devices || !settings.devices[ieeeAddr]) return;
|
||||
|
||||
delete settings.devices[ieeeAddr];
|
||||
writeRead();
|
||||
}
|
||||
|
||||
function getIDByFriendlyName(friendlyName) {
|
||||
if (!settings.devices) {
|
||||
return null;
|
||||
function getIeeeAddrByFriendlyName(friendlyName) {
|
||||
const entry = Object.entries(getDevices()).find(([ieeeAddr, device]) =>
|
||||
device.friendly_name === friendlyName
|
||||
);
|
||||
return entry && entry[0];
|
||||
}
|
||||
|
||||
function getGroupIDByFriendlyName(friendlyName) {
|
||||
const entry = Object.entries(getGroups()).find(([ID, group]) =>
|
||||
group.friendly_name === friendlyName
|
||||
);
|
||||
return entry && entry[0];
|
||||
}
|
||||
|
||||
function changeDeviceOptions(ieeeAddr, newOptions) {
|
||||
const settings = getSettings();
|
||||
const currentOptions = settings.devices[ieeeAddr];
|
||||
|
||||
if (!currentOptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
return Object.keys(settings.devices).find((id) =>
|
||||
settings.devices[id].friendly_name === friendlyName
|
||||
);
|
||||
Object.keys(currentOptions).forEach((key) => {
|
||||
if (newOptions[key]) {
|
||||
currentOptions[key] = newOptions[key];
|
||||
}
|
||||
});
|
||||
|
||||
writeRead();
|
||||
}
|
||||
|
||||
function changeFriendlyName(old, new_) {
|
||||
const ID = getIDByFriendlyName(old);
|
||||
const settings = getSettings();
|
||||
const ieeeAddr = getIeeeAddrByFriendlyName(old);
|
||||
|
||||
if (!ID) {
|
||||
if (!ieeeAddr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
settings.devices[ID].friendly_name = new_;
|
||||
settings.devices[ieeeAddr].friendly_name = new_;
|
||||
writeRead();
|
||||
return true;
|
||||
}
|
||||
|
||||
// An entity can be either a group or a device.
|
||||
function resolveEntity(ID) {
|
||||
let type = null;
|
||||
let friendlyName = null;
|
||||
|
||||
if (module.exports.getIeeeAddrByFriendlyName(ID)) {
|
||||
// Check if the ID is a friendly_name of a device.
|
||||
friendlyName = ID;
|
||||
ID = module.exports.getIeeeAddrByFriendlyName(ID);
|
||||
type = 'device';
|
||||
} else if (module.exports.getGroupIDByFriendlyName(ID)) {
|
||||
// Check if the ID is a friendly_name of a group.
|
||||
friendlyName = ID;
|
||||
ID = Number(module.exports.getGroupIDByFriendlyName(ID));
|
||||
type = 'group';
|
||||
} else if (module.exports.getGroup(ID)) {
|
||||
friendlyName = module.exports.getGroup(ID).friendly_name;
|
||||
ID = Number(ID);
|
||||
type = 'group';
|
||||
} else {
|
||||
// By default it is a device with ID as ID.
|
||||
type = 'device';
|
||||
const device = module.exports.getDevice(ID);
|
||||
friendlyName = device ? device.friendly_name : ID;
|
||||
}
|
||||
|
||||
return {ID, type, friendlyName};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
get: () => settings,
|
||||
get: () => objectAssignDeep.noMutate(defaults, getSettings()),
|
||||
write: () => write(),
|
||||
getDevice: (id) => settings.devices ? settings.devices[id] : null,
|
||||
addDevice: (id) => addDevice(id),
|
||||
removeDevice: (id) => removeDevice(id),
|
||||
getIDByFriendlyName: (friendlyName) => getIDByFriendlyName(friendlyName),
|
||||
set: (path, value) => set(path, value),
|
||||
|
||||
getDevice,
|
||||
getGroup,
|
||||
getDevices,
|
||||
addDevice: (ieeeAddr) => addDevice(ieeeAddr),
|
||||
removeDevice: (ieeeAddr) => removeDevice(ieeeAddr),
|
||||
|
||||
getIeeeAddrByFriendlyName: (friendlyName) => getIeeeAddrByFriendlyName(friendlyName),
|
||||
getGroupIDByFriendlyName: (friendlyName) => getGroupIDByFriendlyName(friendlyName),
|
||||
changeFriendlyName: (old, new_) => changeFriendlyName(old, new_),
|
||||
changeDeviceOptions: (ieeeAddr, options) => changeDeviceOptions(ieeeAddr, options),
|
||||
resolveEntity,
|
||||
|
||||
addOnChangeHandler: (handler) => onChangeHandlers.push(handler),
|
||||
|
||||
// For test
|
||||
_getDefaults: () => {
|
||||
return objectAssignDeep.noMutate(defaults);
|
||||
},
|
||||
_clear: () => _settings = undefined,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// Xiaomi uses 4151 and 4447 (lumi.plug) as manufacturer ID.
|
||||
const xiaomiManufacturerID = [4151, 4447];
|
||||
const ikeaTradfriManufacturerID = [4476];
|
||||
|
||||
// construct a local ISO8601 string (instead of UTC-based)
|
||||
// Example:
|
||||
// - ISO8601 (UTC) = 2019-03-01T15:32:45.941+0000
|
||||
// - ISO8601 (local) = 2019-03-01T16:32:45.941+0100 (for timezone GMT+1)
|
||||
function toLocalISOString(dDate) {
|
||||
const tzOffset = -dDate.getTimezoneOffset();
|
||||
const plusOrMinus = tzOffset >= 0 ? '+' : '-';
|
||||
const pad = function(num) {
|
||||
const norm = Math.floor(Math.abs(num));
|
||||
return (norm < 10 ? '0' : '') + norm;
|
||||
};
|
||||
|
||||
return dDate.getFullYear() +
|
||||
'-' + pad(dDate.getMonth() + 1) +
|
||||
'-' + pad(dDate.getDate()) +
|
||||
'T' + pad(dDate.getHours()) +
|
||||
':' + pad(dDate.getMinutes()) +
|
||||
':' + pad(dDate.getSeconds()) +
|
||||
plusOrMinus + pad(tzOffset / 60) +
|
||||
':' + pad(tzOffset % 60);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
millisecondsToSeconds: (milliseconds) => milliseconds / 1000,
|
||||
secondsToMilliseconds: (seconds) => seconds * 1000,
|
||||
isXiaomiDevice: (device) => xiaomiManufacturerID.includes(device.manufId),
|
||||
isIkeaTradfriDevice: (device) => ikeaTradfriManufacturerID.includes(device.manufId),
|
||||
isNumeric: (string) => /^\d+$/.test(string),
|
||||
toLocalISOString: (dDate) => toLocalISOString(dDate),
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
const maxSimultaneouslyRunning = 5;
|
||||
const delay = 250;
|
||||
const error17Retries = 3;
|
||||
|
||||
class ZigbeeQueue {
|
||||
constructor() {
|
||||
this.queue = [];
|
||||
this.active = [];
|
||||
this.timer = null;
|
||||
this.running = false;
|
||||
}
|
||||
|
||||
start() {
|
||||
this.running = true;
|
||||
this.resetTimer();
|
||||
}
|
||||
|
||||
push(entityID, func) {
|
||||
this.queue.push({entityID, func, attempts: 0});
|
||||
}
|
||||
|
||||
stopTimer() {
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
resetTimer() {
|
||||
this.stopTimer();
|
||||
|
||||
this.timer = setInterval(() => {
|
||||
this.executeNext();
|
||||
}, delay);
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.stopTimer();
|
||||
this.running = false;
|
||||
}
|
||||
|
||||
handleJobComplete(job, error) {
|
||||
if (error && error.message === 'rsp error: 17' && job.attempts < error17Retries) {
|
||||
// Error 17 means that the buffer of the ZNP was full,
|
||||
// retry this for a maximum of 3 times.
|
||||
job.attempts++;
|
||||
this.active.splice(this.active.indexOf(job), 1);
|
||||
} else {
|
||||
this.active.splice(this.active.indexOf(job), 1);
|
||||
this.queue.splice(this.queue.indexOf(job), 1);
|
||||
}
|
||||
}
|
||||
|
||||
executeNext() {
|
||||
if (!this.running) {
|
||||
return;
|
||||
}
|
||||
|
||||
const next = this.getNext();
|
||||
|
||||
if (next) {
|
||||
this.active.push(next);
|
||||
next.func((error) => this.handleJobComplete(next, error));
|
||||
}
|
||||
}
|
||||
|
||||
getNext() {
|
||||
if (this.active.length > (maxSimultaneouslyRunning - 1)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.queue.length; i++) {
|
||||
const job = this.queue[i];
|
||||
const activeDeviceJob = this.active.find((j) => j.entityID === job.entityID);
|
||||
if (!activeDeviceJob) {
|
||||
return job;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ZigbeeQueue;
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* This is a fake cie app which makes it possible for some iAS devices to join the network.
|
||||
* Based on: https://github.com/zigbeer/zapp-cie
|
||||
*/
|
||||
|
||||
const Ziee = require('ziee');
|
||||
const Zive = require('zive');
|
||||
const cieClusters = new Ziee();
|
||||
|
||||
cieClusters.init('genBasic', 'dir', {value: 1}); // Server Side(Input)
|
||||
cieClusters.init('ssIasAce', 'dir', {value: 1}); // Server Side(Input)
|
||||
cieClusters.init('ssIasZone', 'dir', {value: 2}); // Client Side(Output)
|
||||
cieClusters.init('ssIasWd', 'dir', {value: 2}); // Client Side(Output)
|
||||
cieClusters.init('genIdentify', 'dir', {value: 3}); // Server and Client Side(Input/Output)
|
||||
|
||||
// Init Attributes Access Control
|
||||
cieClusters.init('genBasic', 'acls', {
|
||||
zclVersion: 'R',
|
||||
hwVersion: 'R',
|
||||
manufacturerName: 'R',
|
||||
modelId: 'R',
|
||||
dateCode: 'R',
|
||||
powerSource: 'R',
|
||||
locationDesc: 'RW',
|
||||
physicalEnv: 'RW',
|
||||
deviceEnabled: 'RW',
|
||||
});
|
||||
|
||||
cieClusters.init('genIdentify', 'acls', {
|
||||
identifyTime: 'RW',
|
||||
});
|
||||
|
||||
// Init Attributes Value
|
||||
cieClusters.init('genBasic', 'attrs', {
|
||||
zclVersion: 1,
|
||||
hwVersion: 1,
|
||||
manufacturerName: 'sivann inc.',
|
||||
modelId: 'hiver0001',
|
||||
dateCode: '20170407',
|
||||
powerSource: 1,
|
||||
locationDesc: ' ',
|
||||
physicalEnv: 0,
|
||||
deviceEnabled: 1,
|
||||
});
|
||||
|
||||
cieClusters.init('genIdentify', 'attrs', {
|
||||
identifyTime: 0,
|
||||
});
|
||||
|
||||
// Init Command Response Handler
|
||||
cieClusters.init('ssIasZone', 'cmdRsps', {
|
||||
enrollReq: (zapp, argObj, cb) => {},
|
||||
statusChangeNotification: (zapp, argObj, cb) => {},
|
||||
});
|
||||
|
||||
const cieApp = new Zive({profId: 0x0104, devId: 0x0400, discCmds: []}, cieClusters);
|
||||
module.exports = cieApp;
|
||||
+234
-108
@@ -2,39 +2,60 @@ const ZShepherd = require('zigbee-shepherd');
|
||||
const logger = require('./util/logger');
|
||||
const settings = require('./util/settings');
|
||||
const data = require('./util/data');
|
||||
const zclPacket = require('zcl-packet');
|
||||
const utils = require('./util/utils');
|
||||
const ZigbeeQueue = require('./util/zigbeeQueue');
|
||||
const cieApp = require('./zapp/cie');
|
||||
const objectAssignDeep = require('object-assign-deep');
|
||||
const zclId = require('zcl-id');
|
||||
|
||||
const advancedSettings = settings.get().advanced;
|
||||
|
||||
if (advancedSettings.channel < 11 || advancedSettings.channel > 26) {
|
||||
throw new Error(`'${advancedSettings.channel}' is an invalid channel, use a channel between 11 - 26.`);
|
||||
}
|
||||
|
||||
const shepherdSettings = {
|
||||
net: {
|
||||
panId: advancedSettings && advancedSettings.pan_id ? advancedSettings.pan_id : 0x1a62,
|
||||
channelList: [advancedSettings && advancedSettings.channel ? advancedSettings.channel : 11],
|
||||
panId: advancedSettings.pan_id,
|
||||
extPanId: advancedSettings.ext_pan_id,
|
||||
channelList: [advancedSettings.channel],
|
||||
precfgkey: settings.get().advanced.network_key,
|
||||
},
|
||||
dbPath: data.joinPath('database.db'),
|
||||
sp: {
|
||||
baudRate: advancedSettings && advancedSettings.baudrate ? advancedSettings.baudrate : 115200,
|
||||
rtscts: advancedSettings && (typeof(advancedSettings.rtscts) === 'boolean') ? advancedSettings.rtscts : true,
|
||||
baudRate: advancedSettings.baudrate,
|
||||
rtscts: advancedSettings.rtscts,
|
||||
},
|
||||
};
|
||||
|
||||
logger.debug(`Using zigbee-shepherd with settings: '${JSON.stringify(shepherdSettings)}'`);
|
||||
const defaultCfg = {
|
||||
manufSpec: 0,
|
||||
disDefaultRsp: 0,
|
||||
};
|
||||
|
||||
// Don't print network key.
|
||||
const shepherdSettingsLog = objectAssignDeep.noMutate(shepherdSettings);
|
||||
shepherdSettingsLog.net.precfgkey = 'HIDDEN';
|
||||
logger.debug(`Using zigbee-shepherd with settings: '${JSON.stringify(shepherdSettingsLog)}'`);
|
||||
|
||||
class Zigbee {
|
||||
constructor() {
|
||||
this.handleReady = this.handleReady.bind(this);
|
||||
this.handleMessage = this.handleMessage.bind(this);
|
||||
this.handleError = this.handleError.bind(this);
|
||||
this.onReady = this.onReady.bind(this);
|
||||
this.onMessage = this.onMessage.bind(this);
|
||||
this.onError = this.onError.bind(this);
|
||||
this.messageHandler = null;
|
||||
|
||||
this.queue = new ZigbeeQueue();
|
||||
}
|
||||
|
||||
start(onMessage, callback) {
|
||||
start(messageHandler, callback) {
|
||||
logger.info(`Starting zigbee-shepherd`);
|
||||
|
||||
this.messageHandler = messageHandler;
|
||||
this.shepherd = new ZShepherd(settings.get().serial.port, shepherdSettings);
|
||||
|
||||
this.shepherd.start((error) => {
|
||||
if (error) {
|
||||
logger.info('Error while starting zigbee-shepherd, attemping to fix... (takes 60 seconds)');
|
||||
logger.info('Error while starting zigbee-shepherd, attempting to fix... (takes 60 seconds)');
|
||||
this.shepherd.controller._znp.close((() => null));
|
||||
|
||||
setTimeout(() => {
|
||||
@@ -47,26 +68,24 @@ class Zigbee {
|
||||
);
|
||||
callback(error);
|
||||
} else {
|
||||
this._logStartupInfo();
|
||||
this.logStartupInfo();
|
||||
callback(null);
|
||||
}
|
||||
});
|
||||
}, 60 * 1000);
|
||||
}, utils.secondsToMilliseconds(60));
|
||||
} else {
|
||||
this._logStartupInfo();
|
||||
this.logStartupInfo();
|
||||
callback(null);
|
||||
}
|
||||
});
|
||||
|
||||
// Register callbacks.
|
||||
this.shepherd.on('ready', this.handleReady);
|
||||
this.shepherd.on('ind', this.handleMessage);
|
||||
this.shepherd.on('error', this.handleError);
|
||||
|
||||
this.onMessage = onMessage;
|
||||
this.shepherd.on('ready', this.onReady);
|
||||
this.shepherd.on('ind', this.onMessage);
|
||||
this.shepherd.on('error', this.onError);
|
||||
}
|
||||
|
||||
_logStartupInfo() {
|
||||
logStartupInfo() {
|
||||
logger.info('zigbee-shepherd started');
|
||||
logger.info(`Coordinator firmware version: '${this.shepherd.info().firmware.revision}'`);
|
||||
logger.debug(`zigbee-shepherd info: ${JSON.stringify(this.shepherd.info())}`);
|
||||
@@ -77,43 +96,43 @@ class Zigbee {
|
||||
}
|
||||
|
||||
stop(callback) {
|
||||
this.queue.stop();
|
||||
|
||||
this.shepherd.stop((error) => {
|
||||
logger.info('zigbee-shepherd stopped');
|
||||
callback(error);
|
||||
});
|
||||
}
|
||||
|
||||
handleReady() {
|
||||
// Set all Xiaomi devices (manufId === 4151) to be online, so shepherd won't try
|
||||
// to query info from devices (which would fail because they go tosleep).
|
||||
// Xiaomi lumi.plug has manufId === 4447 and can be in the sleep mode too
|
||||
const devices = this.getAllClients();
|
||||
devices.forEach((d) => {
|
||||
if ((d.manufId === 4151) || (d.manufId === 4447)) {
|
||||
const device = this.shepherd.find(d.ieeeAddr, 1);
|
||||
if (device) {
|
||||
device.getDevice().update({
|
||||
status: 'online',
|
||||
joinTime: Math.floor(Date.now() / 1000),
|
||||
});
|
||||
}
|
||||
onReady() {
|
||||
// Mount cieApp
|
||||
this.shepherd.mount(cieApp, (err, epId) => {
|
||||
if (!err) {
|
||||
logger.debug(`Mounted the cieApp (epId ${epId})`);
|
||||
} else {
|
||||
logger.error(`Failed to mount the cieApp`);
|
||||
}
|
||||
});
|
||||
|
||||
// Check if we have to turn of the led
|
||||
// Check if we have to turn off the led
|
||||
if (settings.get().serial.disable_led) {
|
||||
this.shepherd.controller.request('UTIL', 'ledControl', {ledid: 3, mode: 0});
|
||||
}
|
||||
|
||||
// Wait some time before we start the queue, many calls skip this queue which hangs the stick
|
||||
setTimeout(() => {
|
||||
this.queue.start();
|
||||
}, 2000);
|
||||
|
||||
logger.info('zigbee-shepherd ready');
|
||||
}
|
||||
|
||||
handleError(message) {
|
||||
onError(message) {
|
||||
// This event may appear if zigbee-shepherd cannot decode bad packets (invalid checksum).
|
||||
logger.error(message);
|
||||
}
|
||||
|
||||
permitJoin(permit) {
|
||||
permitJoin(permit, callback) {
|
||||
if (permit) {
|
||||
logger.info('Zigbee: allowing new devices to join.');
|
||||
} else {
|
||||
@@ -124,25 +143,33 @@ class Zigbee {
|
||||
if (error) {
|
||||
logger.info(error);
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getAllClients() {
|
||||
return this.shepherd.list().filter((device) => device.type !== 'Coordinator');
|
||||
getPermitJoin() {
|
||||
return this.shepherd.controller._permitJoinTime === 255;
|
||||
}
|
||||
|
||||
removeDevice(deviceID, callback) {
|
||||
this.shepherd.remove(deviceID, (error) => {
|
||||
getAllClients() {
|
||||
return this.getDevices().filter((device) => device.type !== 'Coordinator');
|
||||
}
|
||||
|
||||
removeDevice(deviceID, ban, callback) {
|
||||
this.shepherd.remove(deviceID, {reJoin: !ban}, (error) => {
|
||||
if (error) {
|
||||
logger.warn(`Failed to remove '${deviceID}', trying force remove...`);
|
||||
this._forceRemove(deviceID, callback);
|
||||
this.forceRemove(deviceID, callback);
|
||||
} else {
|
||||
callback(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_forceRemove(deviceID, callback) {
|
||||
forceRemove(deviceID, callback) {
|
||||
const device = this.shepherd._findDevByAddr(deviceID);
|
||||
|
||||
if (device) {
|
||||
@@ -153,91 +180,190 @@ class Zigbee {
|
||||
}
|
||||
}
|
||||
|
||||
ping(deviceID) {
|
||||
let friendlyName = 'unknown';
|
||||
const device = this.shepherd._findDevByAddr(deviceID);
|
||||
const ieeeAddr = device.ieeeAddr;
|
||||
if (settings.getDevice(ieeeAddr)) {
|
||||
friendlyName = settings.getDevice(ieeeAddr).friendly_name;
|
||||
}
|
||||
|
||||
if (device) {
|
||||
// Note: checkOnline has the callback argument but does not call callback
|
||||
logger.debug(`Check online ${friendlyName} ${deviceID}`);
|
||||
this.shepherd.controller.checkOnline(device);
|
||||
onMessage(message) {
|
||||
if (this.messageHandler) {
|
||||
this.messageHandler(message);
|
||||
}
|
||||
}
|
||||
|
||||
handleMessage(message) {
|
||||
if (this.onMessage) {
|
||||
this.onMessage(message);
|
||||
}
|
||||
getDevices() {
|
||||
return this.shepherd.list();
|
||||
}
|
||||
|
||||
getDevice(deviceID) {
|
||||
return this.shepherd.list().find((d) => d.ieeeAddr === deviceID);
|
||||
getDevice(ieeeAddr) {
|
||||
return this.getDevices().find((d) => d.ieeeAddr === ieeeAddr);
|
||||
}
|
||||
|
||||
getCoordinator() {
|
||||
const device = this.shepherd.list().find((d) => d.type === 'Coordinator');
|
||||
const device = this.getDevices().find((d) => d.type === 'Coordinator');
|
||||
return this.shepherd.find(device.ieeeAddr, 1);
|
||||
}
|
||||
|
||||
publish(deviceID, cid, cmd, zclData, ep, callback) {
|
||||
const device = this._findDevice(deviceID, ep);
|
||||
if (!device) {
|
||||
logger.error(`Zigbee cannot publish message to device because '${deviceID}' not known by zigbee-shepherd`);
|
||||
return;
|
||||
}
|
||||
getGroup(ID) {
|
||||
return this.shepherd.getGroup(ID);
|
||||
}
|
||||
|
||||
logger.info(`Zigbee publish to '${deviceID}', ${cid} - ${cmd} - ${JSON.stringify(zclData)} - ${ep}`);
|
||||
device.functional(cid, cmd, zclData, (error) => {
|
||||
if (error) {
|
||||
logger.error(
|
||||
`Zigbee publish to '${deviceID}', ${cid} - ${cmd} - ${JSON.stringify(zclData)} - ${ep} ` +
|
||||
`failed with error ${error}`);
|
||||
}
|
||||
|
||||
callback(error);
|
||||
networkScan(callback) {
|
||||
logger.info('Starting network scan...');
|
||||
this.shepherd.lqiScan().then((result) => {
|
||||
logger.info('Network scan completed');
|
||||
callback(result);
|
||||
});
|
||||
}
|
||||
|
||||
read(deviceID, cid, attr, ep, callback) {
|
||||
const device = this._findDevice(deviceID, ep);
|
||||
if (!device) {
|
||||
logger.error(`Zigbee cannot read attribute from device because '${deviceID}' not known by zigbee-shepherd`);
|
||||
return;
|
||||
}
|
||||
|
||||
device.read(cid, attr, callback);
|
||||
}
|
||||
|
||||
registerOnAfIncomingMsg(ieeeAddr, ep) {
|
||||
const device = this._findDevice(ieeeAddr, ep);
|
||||
device.onAfIncomingMsg = (message) => {
|
||||
// Parse the message
|
||||
zclPacket.parse(message.data, message.clusterid, (error, zclData) => {
|
||||
const message = {
|
||||
endpoints: [device],
|
||||
data: zclData,
|
||||
};
|
||||
|
||||
this.handleMessage(message);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
_findDevice(deviceID, ep) {
|
||||
getEndpoint(ieeeAddr, ep) {
|
||||
// If no ep is given, the first endpoint will be returned
|
||||
// Find device in zigbee-shepherd
|
||||
let device = this.getDevice(deviceID);
|
||||
const device = this.getDevice(ieeeAddr);
|
||||
if (!device || !device.epList || !device.epList.length) {
|
||||
logger.error(`Zigbee cannot determine endpoint for '${deviceID}'`);
|
||||
logger.error(`Zigbee cannot determine endpoint for '${ieeeAddr}'`);
|
||||
return null;
|
||||
}
|
||||
|
||||
ep = ep ? ep : device.epList[0];
|
||||
device = this.shepherd.find(deviceID, ep);
|
||||
return device;
|
||||
const endpoint = this.shepherd.find(ieeeAddr, ep);
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
publish(entityID, entityType, cid, cmd, cmdType, zclData, cfg=defaultCfg, ep, callback) {
|
||||
let entity = null;
|
||||
if (entityType === 'device') {
|
||||
entity = this.getEndpoint(entityID, ep);
|
||||
} else if (entityType === 'group') {
|
||||
entity = this.getGroup(entityID);
|
||||
}
|
||||
|
||||
if (!entity) {
|
||||
logger.error(
|
||||
`Cannot publish message to ${entityType} because '${entityID}' is not known by zigbee-shepherd`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.queue.push(entityID, (queueCallback) => {
|
||||
logger.info(
|
||||
`Zigbee publish to ${entityType} '${entityID}', ${cid} - ${cmd} - ` +
|
||||
`${JSON.stringify(zclData)} - ${JSON.stringify(cfg)} - ${ep}`
|
||||
);
|
||||
|
||||
const callback_ = (error, rsp) => {
|
||||
if (error) {
|
||||
logger.error(
|
||||
`Zigbee publish to ${entityType} '${entityID}', ${cid} - ${cmd} - ${JSON.stringify(zclData)} ` +
|
||||
`- ${JSON.stringify(cfg)} - ${ep} ` +
|
||||
`failed with error ${error}`);
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
callback(error, rsp);
|
||||
}
|
||||
|
||||
queueCallback(error);
|
||||
};
|
||||
|
||||
if (cmdType === 'functional' && entity.functional) {
|
||||
entity.functional(cid, cmd, zclData, cfg, callback_);
|
||||
} else if (cmdType === 'foundation' && entity.foundation) {
|
||||
entity.foundation(cid, cmd, zclData, cfg, callback_);
|
||||
} else {
|
||||
logger.error(`Unknown zigbee publish cmdType ${cmdType}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ping(ieeeAddr, errorLogLevel='error', cb) {
|
||||
const device = this.shepherd._findDevByAddr(ieeeAddr);
|
||||
|
||||
if (device) {
|
||||
this.queue.push(ieeeAddr, (queueCallback) => {
|
||||
logger.debug(`Ping ${ieeeAddr}`);
|
||||
this.shepherd.controller.checkOnline(device, (error) => {
|
||||
if (error) {
|
||||
logger[errorLogLevel](`Failed to ping ${ieeeAddr}`);
|
||||
} else {
|
||||
logger.debug(`Successfully pinged ${ieeeAddr}`);
|
||||
}
|
||||
|
||||
if (cb) {
|
||||
cb(error);
|
||||
}
|
||||
|
||||
queueCallback(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
bind(ep, cluster, target=this.getCoordinator()) {
|
||||
const log = `for ${ep.device.ieeeAddr} - ${cluster}`;
|
||||
|
||||
this.queue.push(ep.device.ieeeAddr, (queueCallback) => {
|
||||
logger.debug(`Setup binding ${log}`);
|
||||
ep.bind(cluster, target, (error) => {
|
||||
if (error) {
|
||||
logger.error(`Failed to setup binding ${log} - (${error})`);
|
||||
} else {
|
||||
logger.debug(`Successfully setup binding ${log}`);
|
||||
}
|
||||
|
||||
queueCallback(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Setup reporting.
|
||||
* Attributes is an array of attribute objects.
|
||||
* each attribute object should contain the following properties:
|
||||
* attr the attribute name,
|
||||
* min the minimal time between reports in seconds,
|
||||
* max the maximum time between reports in seconds,
|
||||
* change the minimum amount of change before sending a report
|
||||
*/
|
||||
report(ep, cluster, attributes) {
|
||||
const cfgArr = attributes.map((attribute) => {
|
||||
const attrId = zclId.attr(cluster, attribute.attr).value;
|
||||
const dataType = zclId.attrType(cluster, attribute.attr).value;
|
||||
return {
|
||||
direction: 0,
|
||||
attrId,
|
||||
dataType,
|
||||
minRepIntval: attribute.min,
|
||||
maxRepIntval: attribute.max,
|
||||
repChange: attribute.change,
|
||||
};
|
||||
});
|
||||
|
||||
const log=`for ${ep.device.ieeeAddr} - ${cluster} - ${attributes.length}`;
|
||||
|
||||
const configReport = () => {
|
||||
this.queue.push(ep.device.ieeeAddr, (queueCallback) => {
|
||||
ep.foundation(cluster, 'configReport', cfgArr, defaultCfg, (error) => {
|
||||
if (error) {
|
||||
logger.error(`Failed to setup reporting ${log} - (${error})`);
|
||||
} else {
|
||||
logger.debug(`Successfully setup reporting ${log}`);
|
||||
}
|
||||
|
||||
queueCallback(error);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
this.queue.push(ep.device.ieeeAddr, (queueCallback) => {
|
||||
logger.debug(`Setup reporting ${log}`);
|
||||
|
||||
ep.bind(cluster, this.getCoordinator(), (error) => {
|
||||
if (error) {
|
||||
logger.error(`Failed to bind for reporting ${log} - (${error})`);
|
||||
} else {
|
||||
// Only if binding succeeds, setting-up reporting makes sense.
|
||||
configReport();
|
||||
}
|
||||
|
||||
queueCallback(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Generated
+3474
-4139
File diff suppressed because it is too large
Load Diff
+16
-10
@@ -1,12 +1,15 @@
|
||||
{
|
||||
"name": "zigbee2mqtt",
|
||||
"version": "0.1.3",
|
||||
"version": "1.2.1",
|
||||
"description": "Zigbee to MQTT bridge using zigbee-shepherd",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/Koenkk/zigbee2mqtt.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": "10"
|
||||
},
|
||||
"keywords": [
|
||||
"xiaomi",
|
||||
"tradfri",
|
||||
@@ -18,31 +21,33 @@
|
||||
],
|
||||
"scripts": {
|
||||
"coverage": "nyc --all report --reporter=text mocha --recursive --timeout=3000",
|
||||
"docgen": "node support/docgen.js",
|
||||
"docgen": "node docgen/docgen.js",
|
||||
"eslint": "node_modules/.bin/eslint .",
|
||||
"start": "node index.js",
|
||||
"test": "mocha --recursive",
|
||||
"verify-homeassistant-mapping": "node support/verify-homeassistant-mapping.js"
|
||||
"test": "mocha --recursive"
|
||||
},
|
||||
"author": "Koen Kanters",
|
||||
"license": "GPL-3.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/Koenkk/zigbee2mqtt/issues"
|
||||
},
|
||||
"homepage": "https://github.com/Koenkk/zigbee2mqtt/wiki",
|
||||
"homepage": "https://koenkk.github.io/zigbee2mqtt",
|
||||
"dependencies": {
|
||||
"git-last-commit": "*",
|
||||
"js-yaml": "*",
|
||||
"json2yaml": "*",
|
||||
"mkdir-recursive": "*",
|
||||
"moment": "*",
|
||||
"mqtt": "*",
|
||||
"object-assign-deep": "*",
|
||||
"queue": "*",
|
||||
"rimraf": "*",
|
||||
"semver": "*",
|
||||
"winston": "2.4.2",
|
||||
"zcl-packet": "git+https://github.com/Koenkk/zcl-packet.git#b7d5b4478a88cd3fc65328c6dc94572c277c7d3e",
|
||||
"zigbee-shepherd": "git+https://github.com/Koenkk/zigbee-shepherd.git#7673fc5a285dc93d2e466bc2aa7134220e1fb134",
|
||||
"zigbee-shepherd-converters": "*"
|
||||
"zcl-id": "git+https://github.com/Koenkk/zcl-id.git#77ba3058c6c4b09a9958761ae5e3046765c12872",
|
||||
"ziee": "*",
|
||||
"zigbee-shepherd": "git+https://github.com/Koenkk/zigbee-shepherd.git#f1f3701c20a125c939504e46c82ac4dda692ff41",
|
||||
"zigbee-shepherd-converters": "7.2.7",
|
||||
"zive": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "*",
|
||||
@@ -52,6 +57,7 @@
|
||||
"eslint-plugin-mocha": "*",
|
||||
"mocha": "*",
|
||||
"nyc": "*",
|
||||
"proxyquire": "*"
|
||||
"sinon": "*",
|
||||
"tmp": "*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
#!/bin/bash
|
||||
function zigbee2mqtt-show-short-info {
|
||||
echo "Setup for Zigbee2mqtt bridge."
|
||||
}
|
||||
|
||||
function zigbee2mqtt-show-long-info {
|
||||
echo "This script installs the Zigbee2mqtt bridge"
|
||||
}
|
||||
|
||||
function zigbee2mqtt-show-copyright-info {
|
||||
echo "Original concept by Landrash <https://github.com/landrash>."
|
||||
}
|
||||
|
||||
function zigbee2mqtt-install-package {
|
||||
echo -n "Installing dependencies : "
|
||||
node=$(which npm)
|
||||
if [ -z "${node}" ]; then #Installing NodeJS if not already installed.
|
||||
printf "Downloading and installing NodeJS...\\n"
|
||||
curl -sL https://deb.nodesource.com/setup_10.x | bash -
|
||||
apt install -y nodejs
|
||||
fi
|
||||
|
||||
echo "Cloning Zigbee2mqtt git repository"
|
||||
git clone https://github.com/Koenkk/zigbee2mqtt.git /opt/zigbee2mqtt
|
||||
chown -R pi:pi /opt/zigbee2mqtt
|
||||
|
||||
echo "Running install. This might take a while and can produce som expected errors"
|
||||
cd /opt/zigbee2mqtt || exit
|
||||
su pi -c "npm install"
|
||||
|
||||
echo "Creating service file zigbee2mqtt.service"
|
||||
service_path="/etc/systemd/system/zigbee2mqtt.service"
|
||||
|
||||
echo "[Unit]
|
||||
Description=zigbee2mqtt
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/npm start
|
||||
WorkingDirectory=/opt/zigbee2mqtt
|
||||
StandardOutput=inherit
|
||||
StandardError=inherit
|
||||
Restart=always
|
||||
User=pi
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target" > $service_path
|
||||
|
||||
echo "Checking the installation..."
|
||||
if [ ! -f /opt/zigbee2mqtt/data/configuration.yaml ]; then
|
||||
validation=""
|
||||
else
|
||||
validation="ok"
|
||||
fi
|
||||
|
||||
if [ ! -z "${validation}" ]; then
|
||||
echo
|
||||
echo -e "\\e[32mInstallation done..\\e[0m"
|
||||
echo -e "Update of configuration.yaml is required found at /opt/zigbee2mqtt/data/"
|
||||
echo -e "Some further configuration is required and details can be found here https://www.zigbee2mqtt.io"
|
||||
echo
|
||||
echo -e "Service can be started after configuration but running sudo systemctl start zigbee2mqtt"
|
||||
echo
|
||||
else
|
||||
echo
|
||||
echo -e "\\e[31mInstallation failed..."
|
||||
echo
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
[[ "$_" == "$0" ]] && zigbee2mqtt-install-package
|
||||
@@ -0,0 +1,181 @@
|
||||
#!/bin/bash
|
||||
|
||||
######################################
|
||||
## INITIALIZE ##
|
||||
######################################
|
||||
|
||||
set -o errexit -o pipefail -o noclobber -o nounset
|
||||
|
||||
! getopt --test > /dev/null
|
||||
if [[ ${PIPESTATUS[0]} -ne 4 ]]; then
|
||||
echo "Unfortunately getopt failed in this environment."
|
||||
exit 2
|
||||
fi
|
||||
|
||||
|
||||
PROGRAM_NAME="zigbee2socat_installer"
|
||||
VERSION="1.0"
|
||||
|
||||
|
||||
######################################
|
||||
## PARSE ARGUMENTS ##
|
||||
######################################
|
||||
|
||||
OPTS="a:p:uvmhV"
|
||||
LONG="addr:,port:,uninstall,verbose,man,help,version"
|
||||
|
||||
# Concern: possible to escape getopt and execute commands as root?
|
||||
! PARSED=$(getopt -n $PROGRAM_NAME \
|
||||
-o $OPTS \
|
||||
-l $LONG \
|
||||
-- "$@")
|
||||
if [[ ${PIPESTATUS[0]} -ne 0 ]]; then
|
||||
printf "Error parsing arguments. Try %s --help\n" "$PROGRAM_NAME"
|
||||
exit 3
|
||||
fi
|
||||
eval set -- "$PARSED" # Use remaining arguments that weren't parsed
|
||||
|
||||
|
||||
function handle_opts {
|
||||
if [[ $# = 1 ]]; then
|
||||
usage
|
||||
fi
|
||||
while true; do
|
||||
case $1 in
|
||||
-a|--addr)
|
||||
ip=$2; shift 2; continue ;;
|
||||
|
||||
-p|--port)
|
||||
port=$2; shift 2; continue ;;
|
||||
|
||||
-u|--uninstall)
|
||||
uninstall ;;
|
||||
|
||||
-v|--verbose)
|
||||
verbose=1; shift; continue ;;
|
||||
|
||||
-m|--man)
|
||||
makemanual ;;
|
||||
|
||||
-h|--help)
|
||||
usage ;;
|
||||
|
||||
-V|--version)
|
||||
version ;;
|
||||
|
||||
--) # No more arguments to parse
|
||||
shift; break ;;
|
||||
|
||||
*)
|
||||
printf "Programming error! Option: %s\n" "$1"
|
||||
exit 4 ;;
|
||||
esac;done
|
||||
}
|
||||
|
||||
|
||||
######################################
|
||||
## START OF MAIN ##
|
||||
######################################
|
||||
|
||||
verbose=0
|
||||
ip=""
|
||||
port=""
|
||||
|
||||
function main {
|
||||
handle_opts "$@"
|
||||
|
||||
# TODO: Check IP-address valid?
|
||||
if [[ "$ip" = "" ]]; then
|
||||
echo "IP-address not specified"
|
||||
exit 5
|
||||
fi
|
||||
|
||||
# Is the port valid?
|
||||
if [[ $port -lt 1 || $port -gt 65535 ]]; then
|
||||
printf "Port %s, is outside range: [1-65535]\n" "$port"
|
||||
exit 6
|
||||
fi
|
||||
|
||||
zigbee-socatvusb-install-package
|
||||
}
|
||||
|
||||
|
||||
######################################
|
||||
## FUNCTIONS BELOW ##
|
||||
######################################
|
||||
|
||||
function zigbee-socatvusb-install-package {
|
||||
echo "Installing socat:"
|
||||
sudo apt-get install socat
|
||||
echo
|
||||
|
||||
echo "Make dir for zigbee vusb"
|
||||
sudo mkdir -p /opt/zigbee2mqtt/vusb/ || die "Couldn't mkdir /opt/zigbee2mqtt/vusb/"
|
||||
sudo chown -R pi:pi /opt/zigbee2mqtt/vusb/ || die "Couldn't chown /opt/zigbee2mqtt/vusb/"
|
||||
|
||||
echo "Creating service file zigbee-socatvusb.service"
|
||||
service_path="/etc/systemd/system/zigbee-socatvusb.service"
|
||||
|
||||
[[ -f $service_path ]] && sudo rm $service_path
|
||||
echo "[Unit]
|
||||
Description=socat-vusb
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
User=pi
|
||||
ExecStart=/usr/bin/socat -d -d pty,raw,echo=0,link=/opt/zigbee2mqtt/vusb/zigbee_cc2530 tcp:$ip:$port,reuseaddr
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target" > $service_path || die "Couldn't create service /etc/systemd/system/zigbee-socatvusb.service"
|
||||
|
||||
sudo systemctl --system daemon-reload
|
||||
|
||||
echo "Installation is now complete"
|
||||
echo
|
||||
echo "Service can be started after configuration by running: sudo systemctl start zigbee-socatvusb"
|
||||
}
|
||||
|
||||
function uninstall {
|
||||
service_path="systemctl status socat-vusb.service"
|
||||
[[ -f $service_path ]] && sudo rm $service_path
|
||||
sudo systemctl --system daemon-reload
|
||||
echo "Uninstalled successfully"
|
||||
exit 0
|
||||
}
|
||||
|
||||
function makemanual {
|
||||
[[ -f "$PROGRAM_NAME.man" ]] && sudo rm $PROGRAM_NAME.man
|
||||
help2man -N ./$PROGRAM_NAME.sh > $PROGRAM_NAME.man ; man ./$PROGRAM_NAME.man
|
||||
exit 0
|
||||
}
|
||||
|
||||
function usage {
|
||||
echo -e "\
|
||||
\rSetup for development version of Zigbee2Socat
|
||||
|
||||
-a, --addr IP Listen on this IP-address
|
||||
-p, --port PORT Listen on this port
|
||||
-u, --uninstall Uninstall zigbee-socatvusb.service
|
||||
-v, --verbose Print more information
|
||||
-m, --man Make and display manual
|
||||
-h, --help Display this help message
|
||||
|
||||
\rOriginal concept by JFLN\
|
||||
"
|
||||
exit 0
|
||||
}
|
||||
|
||||
function version {
|
||||
echo "$PROGRAM_NAME $VERSION"
|
||||
exit 0
|
||||
}
|
||||
|
||||
function die {
|
||||
printf "%s\n" "$1"
|
||||
exit 1
|
||||
}
|
||||
|
||||
|
||||
main "$@" # Call main-function last
|
||||
@@ -1,127 +0,0 @@
|
||||
/**
|
||||
* This script generates the supported devices page.
|
||||
* Run by executing: npm run docs
|
||||
*/
|
||||
|
||||
const deviceMapping = require('zigbee-shepherd-converters').devices;
|
||||
const fs = require('fs');
|
||||
const YAML = require('json2yaml');
|
||||
const homeassistant = require('../lib/homeassistant');
|
||||
|
||||
const outputdir = process.argv[2];
|
||||
|
||||
if (!outputdir) {
|
||||
console.error("Please specify an output directory");
|
||||
}
|
||||
|
||||
let file = 'Supported-devices.md';
|
||||
let text = '*NOTE: Automatically generated by `npm run docgen`*\n';
|
||||
text += `
|
||||
In case you own a Zigbee device which is **NOT** listed here, please see [How to support new devices](https://github.com/Koenkk/zigbee2mqtt/wiki/How-to-support-new-devices).
|
||||
\n`;
|
||||
|
||||
const logDevices = (devices) => {
|
||||
let result = '';
|
||||
result += '| Model | Description | Picture |\n';
|
||||
result += '| ------------- | ------------- | -------------------------- |\n';
|
||||
devices = new Map(devices.map((d) => [d.model, d]));
|
||||
devices.forEach((device) => {
|
||||
const pathFriendly = device.model.replace(new RegExp("/", "g"), '-').replace(new RegExp(":", "g"), '-').replace(new RegExp(" ", "g"), '-');
|
||||
result += `| ${device.model} | ${device.vendor} ${device.description} (${device.supports}) |  |\n`;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const vendors = Array.from(new Set(deviceMapping.map((d) => d.vendor)));
|
||||
vendors.sort();
|
||||
vendors.forEach((vendor) => {
|
||||
text += `### ${vendor}\n`;
|
||||
text += logDevices(deviceMapping.filter((d) => d.vendor === vendor));
|
||||
text += '\n';
|
||||
})
|
||||
|
||||
fs.writeFileSync(outputdir + '/' + file, text);
|
||||
|
||||
|
||||
file = 'Integrating-with-Home-Assistant.md';
|
||||
text = '*NOTE: Automatically generated by `npm run docgen`*\n\n';
|
||||
text += 'If you\'re hosting zigbee2mqtt using [this hassio addon-on](https://github.com/danielwelch/hassio-zigbee2mqtt) use their documentation on how to configure.\n\n'
|
||||
text += 'The easiest way to integrate zigbee2mqtt with Home Assistant is by using [MQTT discovery](https://www.home-assistant.io/docs/mqtt/discovery/).\n\n'
|
||||
text += 'To achieve the best possible integration (including MQTT discovery):\n';
|
||||
text += '- In your **zigbee2mqtt** `configuration.yaml` set `homeassistant: true`\n'
|
||||
text += '- In your **Home Assistant** `configuration.yaml`:\n';
|
||||
|
||||
text += '```yaml\n';
|
||||
text += 'mqtt:\n';
|
||||
text += ' discovery: true\n';
|
||||
text += ' broker: [YOUR MQTT BROKER] # Remove if you want to use builtin-in MQTT broker\n';
|
||||
text += ' birth_message:\n';
|
||||
text += ` topic: 'hass/status'\n`;
|
||||
text += ` payload: 'online'\n`;
|
||||
text += ' will_message:\n';
|
||||
text += ` topic: 'hass/status'\n`;
|
||||
text += ` payload: 'offline'\n`;
|
||||
text += '```';
|
||||
|
||||
text += '\n\n'
|
||||
|
||||
text += 'To respond to button clicks (e.g. WXKG01LM) you can use the following Home Assistant configuration:\n'
|
||||
text += '```yaml'
|
||||
text += `
|
||||
automation:
|
||||
- alias: Respond to button clicks
|
||||
trigger:
|
||||
platform: mqtt
|
||||
topic: 'zigbee2mqtt/<FRIENDLY_NAME'
|
||||
condition:
|
||||
condition: template
|
||||
value_template: "{{ 'single' == trigger.payload_json.click }}"
|
||||
action:
|
||||
entity_id: light.bedroom
|
||||
service: light.toggle
|
||||
`
|
||||
text += '```\n'
|
||||
text += '**When changing a `friendly_name` for a device you first have to start zigbee2mqtt and after that restart Home Assistant in order to discover the new device ID.**\n\n';
|
||||
text += 'In case you **dont** want to use Home Assistant MQTT discovery you can use the configuration below.\n\n'
|
||||
|
||||
const homeassistantConfig = (device) => {
|
||||
const payload = {
|
||||
platform: 'mqtt',
|
||||
state_topic: "zigbee2mqtt/<FRIENDLY_NAME>",
|
||||
availability_topic: "zigbee2mqtt/bridge/state",
|
||||
...device.discovery_payload,
|
||||
};
|
||||
|
||||
if (payload.command_topic) {
|
||||
if (payload.command_topic_prefix) {
|
||||
payload.command_topic = `zigbee2mqtt/<FRIENDLY_NAME>/${payload.command_topic_prefix}/set`;
|
||||
} else {
|
||||
payload.command_topic = `zigbee2mqtt/<FRIENDLY_NAME>/set`;
|
||||
}
|
||||
}
|
||||
|
||||
delete payload.command_topic_prefix;
|
||||
|
||||
let yml = YAML.stringify([payload]);
|
||||
yml = yml.replace(/(-) \n /g, '- ');
|
||||
yml = yml.replace('---', `${device.type}:`)
|
||||
return yml;
|
||||
}
|
||||
|
||||
deviceMapping.forEach((device) => {
|
||||
text += `### ${device.model}\n`;
|
||||
text += '```yaml\n'
|
||||
|
||||
const configurations = homeassistant.mapping[device.model]
|
||||
configurations.forEach((d, i) => {
|
||||
text += homeassistantConfig(d);
|
||||
if (configurations.length > 1 && i < configurations.length - 1) {
|
||||
text += '\n';
|
||||
}
|
||||
})
|
||||
|
||||
text += '```\n\n';
|
||||
});
|
||||
|
||||
fs.writeFileSync(outputdir + '/' + file, text);
|
||||
@@ -1,13 +0,0 @@
|
||||
// Verify that there are homeassistant mappings for every device.
|
||||
const devices = require('zigbee-shepherd-converters').devices;
|
||||
const homeassistant = require('../lib/homeassistant');
|
||||
|
||||
let failed = false;
|
||||
devices.forEach((d) => {
|
||||
if (!homeassistant.mapping[d.model]) {
|
||||
console.error(`Missing homeassistant mapping for '${d.model}'`);
|
||||
failed = true;
|
||||
}
|
||||
});
|
||||
|
||||
process.exit(failed ? 1 : 0);
|
||||
@@ -0,0 +1,105 @@
|
||||
const assert = require('chai').assert;
|
||||
const sinon = require('sinon');
|
||||
const Controller = require('../lib/controller');
|
||||
const settings = require('../lib/util/settings');
|
||||
const mqtt = require('../lib/mqtt');
|
||||
const utils = require('./utils');
|
||||
|
||||
describe('Controller', () => {
|
||||
let controller;
|
||||
let mqttPublish;
|
||||
|
||||
beforeEach(() => {
|
||||
utils.stubLogger(sinon);
|
||||
sinon.stub(settings, 'getDevice').callsFake((ieeeAddr) => {
|
||||
return {friendly_name: 'test'};
|
||||
});
|
||||
mqttPublish = sinon.stub(mqtt.prototype, 'publish').callsFake(() => {});
|
||||
controller = new Controller();
|
||||
controller.zigbee = {
|
||||
getDevice: () => {
|
||||
return {
|
||||
modelId: 'TRADFRI bulb E27 CWS opal 600lm',
|
||||
manufName: 'IKEA',
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
describe('Handling zigbee messages', () => {
|
||||
it('Should handle a zigbee message', () => {
|
||||
const device = {ieeeAddr: '0x12345678', modelId: 'TRADFRI bulb E27 CWS opal 600lm'};
|
||||
const message = utils.zigbeeMessage(device, 'genOnOff', 'devChange', {onOff: 1}, 1);
|
||||
controller.onZigbeeMessage(message);
|
||||
assert.isTrue(mqttPublish.calledOnce);
|
||||
assert.strictEqual(
|
||||
mqttPublish.getCall(0).args[1],
|
||||
JSON.stringify({state: 'ON'})
|
||||
);
|
||||
});
|
||||
|
||||
it('Should handle a zigbee message when include_device_information is set', () => {
|
||||
sinon.stub(settings, 'get').callsFake(() => {
|
||||
return {
|
||||
mqtt: {
|
||||
include_device_information: true,
|
||||
},
|
||||
advanced: {
|
||||
cache_state: false,
|
||||
},
|
||||
experimental: {
|
||||
output: 'json',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const device = {ieeeAddr: '0x12345678', modelId: 'TRADFRI bulb E27 CWS opal 600lm'};
|
||||
const message = utils.zigbeeMessage(device, 'genOnOff', 'devChange', {onOff: 1}, 1);
|
||||
controller.onZigbeeMessage(message);
|
||||
assert.isTrue(mqttPublish.calledOnce);
|
||||
assert.strictEqual(
|
||||
mqttPublish.getCall(0).args[1],
|
||||
`{"state":"ON","device":{"ieeeAddr":"0x12345678","friendlyName":"test",` +
|
||||
`"manufName":"IKEA","modelId":"TRADFRI bulb E27 CWS opal 600lm"}}`
|
||||
);
|
||||
});
|
||||
|
||||
it('Should output to json by default', () => {
|
||||
const payload = {temperature: 1, humidity: 2};
|
||||
controller.publishEntityState('0x12345678', payload);
|
||||
assert.isTrue(mqttPublish.calledOnce);
|
||||
assert.deepEqual(
|
||||
JSON.parse(mqttPublish.getCall(0).args[1]),
|
||||
payload
|
||||
);
|
||||
});
|
||||
|
||||
it('Should output to attribute', () => {
|
||||
sinon.stub(settings, 'get').callsFake(() => {
|
||||
return {
|
||||
mqtt: {
|
||||
include_device_information: false,
|
||||
},
|
||||
advanced: {
|
||||
cache_state: false,
|
||||
},
|
||||
experimental: {
|
||||
output: 'attribute',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const payload = {temperature: 1, humidity: 2};
|
||||
controller.publishEntityState('0x12345678', payload);
|
||||
assert.isTrue(mqttPublish.calledTwice);
|
||||
assert.deepEqual(mqttPublish.getCall(0).args[0], 'test/temperature');
|
||||
assert.deepEqual(mqttPublish.getCall(0).args[1], '1');
|
||||
assert.deepEqual(mqttPublish.getCall(1).args[0], 'test/humidity');
|
||||
assert.deepEqual(mqttPublish.getCall(1).args[1], '2');
|
||||
});
|
||||
});
|
||||
});
|
||||
+9
-9
@@ -1,23 +1,23 @@
|
||||
/* global describe, it */
|
||||
|
||||
const chai = require('chai');
|
||||
const proxyquire = require('proxyquire').noPreserveCache();
|
||||
const data = () => proxyquire('../lib/util/data.js', {});
|
||||
const assert = require('chai').assert;
|
||||
const data = require('../lib/util/data.js');
|
||||
const path = require('path');
|
||||
|
||||
describe('Data', () => {
|
||||
describe('Get path', () => {
|
||||
it('Should return correct path', () => {
|
||||
const expected = path.normalize(path.join(__dirname, '..', 'data'));
|
||||
const actual = data().getPath();
|
||||
chai.assert.strictEqual(actual, expected);
|
||||
const actual = data.getPath();
|
||||
assert.strictEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('Should return correct path when ZIGBEE2MQTT_DATA set', () => {
|
||||
const expected = path.join('var', 'zigbee2mqtt');
|
||||
process.env.ZIGBEE2MQTT_DATA = expected;
|
||||
const actual = data().getPath();
|
||||
chai.assert.strictEqual(actual, expected);
|
||||
data._reload();
|
||||
const actual = data.getPath();
|
||||
assert.strictEqual(actual, expected);
|
||||
delete process.env.ZIGBEE2MQTT_DATA;
|
||||
data._reload();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
const assert = require('chai').assert;
|
||||
const sinon = require('sinon');
|
||||
const Availability = require('../lib/extension/deviceAvailability');
|
||||
const utils = require('./utils');
|
||||
|
||||
describe('Availability', () => {
|
||||
let availability;
|
||||
|
||||
beforeEach(() => {
|
||||
utils.stubLogger(sinon);
|
||||
availability = new Availability(null, null, null, () => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
describe('Determine pingable devices', () => {
|
||||
it('Router device should be a pingable device', () => {
|
||||
const device = {
|
||||
powerSource: 'Mains (single phase)',
|
||||
type: 'Router',
|
||||
};
|
||||
|
||||
assert.isTrue(availability.isPingable(device));
|
||||
});
|
||||
|
||||
it('Battery device should not be a pingable device', () => {
|
||||
const device = {
|
||||
powerSource: 'Battery',
|
||||
type: 'EndDevice',
|
||||
};
|
||||
|
||||
assert.isFalse(availability.isPingable(device));
|
||||
});
|
||||
|
||||
it('E11-G13 should be a pingable device', () => {
|
||||
const device = {
|
||||
powerSource: 'Mains (single phase)',
|
||||
type: 'EndDevice',
|
||||
modelId: 'E11-G13',
|
||||
manufId: 4448,
|
||||
};
|
||||
|
||||
assert.isTrue(availability.isPingable(device));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,727 @@
|
||||
const assert = require('chai').assert;
|
||||
const sinon = require('sinon');
|
||||
const DevicePublish = require('../lib/extension/devicePublish');
|
||||
const settings = require('../lib/util/settings');
|
||||
const utils = require('./utils');
|
||||
|
||||
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
const mqtt = {
|
||||
subscribe: (topic) => {},
|
||||
};
|
||||
|
||||
const zigbee = {
|
||||
getDevice: null,
|
||||
publish: sinon.stub().callsFake((entityID, entityType, cid, cmd, cmdType, zclData, cfg, ep, callback) => {
|
||||
callback(false, null);
|
||||
}),
|
||||
};
|
||||
|
||||
const publishEntityState = sinon.stub().callsFake((entityID, payload, cache) => {
|
||||
});
|
||||
|
||||
const cfg = {
|
||||
default: {
|
||||
manufSpec: 0,
|
||||
disDefaultRsp: 0,
|
||||
},
|
||||
};
|
||||
|
||||
describe('DevicePublish', () => {
|
||||
let devicePublish;
|
||||
|
||||
beforeEach(() => {
|
||||
utils.stubLogger(sinon);
|
||||
devicePublish = new DevicePublish(zigbee, mqtt, null, publishEntityState);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
describe('Parse topic', () => {
|
||||
it('Should publish messages to zigbee devices', async () => {
|
||||
zigbee.publish.resetHistory();
|
||||
publishEntityState.resetHistory();
|
||||
zigbee.getDevice = sinon.fake.returns({modelId: 'TRADFRI bulb E27 CWS opal 600lm'});
|
||||
devicePublish.onMQTTMessage('zigbee2mqtt/0x00000001/set', JSON.stringify({brightness: '200'}));
|
||||
assert.strictEqual(zigbee.publish.callCount, 1);
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[0], '0x00000001');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[1], 'device');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[2], 'genLevelCtrl');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[3], 'moveToLevelWithOnOff');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[4], 'functional');
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[5], {level: 200, transtime: 0});
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[6], cfg.default);
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[7], null);
|
||||
assert.strictEqual(publishEntityState.callCount, 1);
|
||||
assert.strictEqual(publishEntityState.getCall(0).args[0], '0x00000001');
|
||||
assert.deepEqual(publishEntityState.getCall(0).args[1], {state: 'ON', brightness: 200});
|
||||
assert.strictEqual(publishEntityState.getCall(0).args[2], true);
|
||||
await wait(10);
|
||||
assert.strictEqual(zigbee.publish.callCount, 2);
|
||||
});
|
||||
|
||||
it('Should publish messages to zigbee devices', async () => {
|
||||
zigbee.publish.resetHistory();
|
||||
publishEntityState.resetHistory();
|
||||
sinon.stub(settings, 'getIeeeAddrByFriendlyName').callsFake(() => '0x00000002');
|
||||
zigbee.getDevice = sinon.fake.returns({modelId: 'LCT003'});
|
||||
devicePublish.onMQTTMessage('zigbee2mqtt/wohnzimmer.light.wall.right/set', JSON.stringify({state: 'ON'}));
|
||||
assert.strictEqual(zigbee.publish.callCount, 1);
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[0], '0x00000002');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[1], 'device');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[2], 'genOnOff');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[3], 'on');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[4], 'functional');
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[5], {});
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[6], cfg.default);
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[7], null);
|
||||
assert.strictEqual(publishEntityState.callCount, 1);
|
||||
assert.strictEqual(publishEntityState.getCall(0).args[0], '0x00000002');
|
||||
assert.deepEqual(publishEntityState.getCall(0).args[1], {state: 'ON'});
|
||||
assert.strictEqual(publishEntityState.getCall(0).args[2], true);
|
||||
await wait(10);
|
||||
assert.strictEqual(zigbee.publish.callCount, 1);
|
||||
});
|
||||
|
||||
it('Should publish messages to zigbee devices when brightness is in %', async () => {
|
||||
zigbee.publish.resetHistory();
|
||||
publishEntityState.resetHistory();
|
||||
zigbee.getDevice = sinon.fake.returns({modelId: 'TRADFRI bulb E27 CWS opal 600lm'});
|
||||
devicePublish.onMQTTMessage('zigbee2mqtt/0x00000003/set', JSON.stringify({brightness_percent: '92'}));
|
||||
assert.strictEqual(zigbee.publish.callCount, 1);
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[0], '0x00000003');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[1], 'device');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[2], 'genLevelCtrl');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[3], 'moveToLevelWithOnOff');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[4], 'functional');
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[5], {level: 235, transtime: 0});
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[6], cfg.default);
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[7], null);
|
||||
assert.strictEqual(publishEntityState.callCount, 1);
|
||||
assert.strictEqual(publishEntityState.getCall(0).args[0], '0x00000003');
|
||||
assert.deepEqual(publishEntityState.getCall(0).args[1], {state: 'ON', brightness: 235});
|
||||
assert.strictEqual(publishEntityState.getCall(0).args[2], true);
|
||||
await wait(10);
|
||||
assert.strictEqual(zigbee.publish.callCount, 2);
|
||||
});
|
||||
|
||||
it('Should publish messages to zigbee devices when brightness is in number', async () => {
|
||||
zigbee.publish.resetHistory();
|
||||
publishEntityState.resetHistory();
|
||||
zigbee.getDevice = sinon.fake.returns({modelId: 'TRADFRI bulb E27 CWS opal 600lm'});
|
||||
devicePublish.onMQTTMessage('zigbee2mqtt/0x00000004/set', JSON.stringify({brightness: 230}));
|
||||
assert.strictEqual(zigbee.publish.callCount, 1);
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[0], '0x00000004');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[1], 'device');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[2], 'genLevelCtrl');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[3], 'moveToLevelWithOnOff');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[4], 'functional');
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[5], {level: 230, transtime: 0});
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[6], cfg.default);
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[7], null);
|
||||
assert.strictEqual(publishEntityState.callCount, 1);
|
||||
assert.strictEqual(publishEntityState.getCall(0).args[0], '0x00000004');
|
||||
assert.deepEqual(publishEntityState.getCall(0).args[1], {state: 'ON', brightness: 230});
|
||||
assert.strictEqual(publishEntityState.getCall(0).args[2], true);
|
||||
await wait(10);
|
||||
assert.strictEqual(zigbee.publish.callCount, 2);
|
||||
});
|
||||
|
||||
it('Should publish messages to zigbee devices with color_temp', async () => {
|
||||
zigbee.publish.resetHistory();
|
||||
zigbee.getDevice = sinon.fake.returns({modelId: 'TRADFRI bulb E27 WS opal 980lm'});
|
||||
devicePublish.onMQTTMessage('zigbee2mqtt/0x00000005/set', JSON.stringify({color_temp: '222'}));
|
||||
assert.strictEqual(zigbee.publish.callCount, 1);
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[0], '0x00000005');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[1], 'device');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[2], 'lightingColorCtrl');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[3], 'moveToColorTemp');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[4], 'functional');
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[5], {colortemp: '222', transtime: 0});
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[6], cfg.default);
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[7], null);
|
||||
await wait(10);
|
||||
assert.strictEqual(zigbee.publish.callCount, 2);
|
||||
});
|
||||
|
||||
it('Should publish messages to zigbee devices with color_temp in %', async () => {
|
||||
publishEntityState.resetHistory();
|
||||
zigbee.publish.resetHistory();
|
||||
zigbee.getDevice = sinon.fake.returns({modelId: 'TRADFRI bulb E27 WS opal 980lm'});
|
||||
devicePublish.onMQTTMessage('zigbee2mqtt/0x00000006/set', JSON.stringify({color_temp_percent: '100'}));
|
||||
assert.strictEqual(zigbee.publish.callCount, 1);
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[0], '0x00000006');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[1], 'device');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[2], 'lightingColorCtrl');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[3], 'moveToColorTemp');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[4], 'functional');
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[5], {colortemp: '500', transtime: 0});
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[6], cfg.default);
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[7], null);
|
||||
assert.isTrue(publishEntityState.notCalled);
|
||||
await wait(10);
|
||||
assert.strictEqual(zigbee.publish.callCount, 2);
|
||||
});
|
||||
|
||||
it('Should publish messages to zigbee devices with non-default ep', async () => {
|
||||
zigbee.publish.resetHistory();
|
||||
zigbee.getDevice = sinon.fake.returns({modelId: 'lumi.ctrl_neutral1'});
|
||||
devicePublish.onMQTTMessage('zigbee2mqtt/0x00000007/set', JSON.stringify({state: 'OFF'}));
|
||||
assert.strictEqual(zigbee.publish.callCount, 1);
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[0], '0x00000007');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[1], 'device');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[2], 'genOnOff');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[3], 'off');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[4], 'functional');
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[5], {});
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[6], cfg.default);
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[7], 2);
|
||||
await wait(10);
|
||||
assert.strictEqual(zigbee.publish.callCount, 1);
|
||||
});
|
||||
|
||||
it('Should publish messages to zigbee devices with non-default ep and postfix', async () => {
|
||||
zigbee.publish.resetHistory();
|
||||
publishEntityState.resetHistory();
|
||||
zigbee.getDevice = sinon.fake.returns({modelId: 'lumi.ctrl_neutral2'});
|
||||
devicePublish.onMQTTMessage('zigbee2mqtt/0x00000008/right/set', JSON.stringify({state: 'OFF'}));
|
||||
assert.strictEqual(zigbee.publish.callCount, 1);
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[0], '0x00000008');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[1], 'device');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[2], 'genOnOff');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[3], 'off');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[4], 'functional');
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[5], {});
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[6], cfg.default);
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[7], 3);
|
||||
assert.strictEqual(publishEntityState.callCount, 1);
|
||||
assert.strictEqual(publishEntityState.getCall(0).args[0], '0x00000008');
|
||||
assert.deepEqual(publishEntityState.getCall(0).args[1], {state_right: 'OFF'});
|
||||
assert.strictEqual(publishEntityState.getCall(0).args[2], true);
|
||||
await wait(10);
|
||||
assert.strictEqual(zigbee.publish.callCount, 1);
|
||||
});
|
||||
|
||||
it('Should publish messages to zigbee gledopto with [11,13]', async () => {
|
||||
zigbee.publish.resetHistory();
|
||||
publishEntityState.resetHistory();
|
||||
zigbee.getDevice = sinon.fake.returns({modelId: 'GLEDOPTO', epList: [11, 13]});
|
||||
devicePublish.onMQTTMessage('zigbee2mqtt/0x00000009/set', JSON.stringify({state: 'OFF'}));
|
||||
assert.strictEqual(zigbee.publish.callCount, 1);
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[0], '0x00000009');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[1], 'device');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[2], 'genOnOff');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[3], 'off');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[4], 'functional');
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[5], {});
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[6], cfg.default);
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[7], 11);
|
||||
assert.strictEqual(publishEntityState.callCount, 1);
|
||||
assert.strictEqual(publishEntityState.getCall(0).args[0], '0x00000009');
|
||||
assert.deepEqual(publishEntityState.getCall(0).args[1], {state: 'OFF'});
|
||||
assert.strictEqual(publishEntityState.getCall(0).args[2], true);
|
||||
await wait(10);
|
||||
assert.strictEqual(zigbee.publish.callCount, 1);
|
||||
});
|
||||
|
||||
it('Should publish messages to zigbee gledopto with [11,12,13]', async () => {
|
||||
zigbee.publish.resetHistory();
|
||||
publishEntityState.resetHistory();
|
||||
zigbee.getDevice = sinon.fake.returns({modelId: 'GLEDOPTO', epList: [11, 12, 13]});
|
||||
devicePublish.onMQTTMessage('zigbee2mqtt/0x00000010/set', JSON.stringify({state: 'OFF', brightness: 50}));
|
||||
assert.strictEqual(zigbee.publish.callCount, 1);
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[0], '0x00000010');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[1], 'device');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[2], 'genOnOff');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[3], 'off');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[4], 'functional');
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[5], {});
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[6], cfg.default);
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[7], 12);
|
||||
assert.strictEqual(publishEntityState.callCount, 1);
|
||||
assert.strictEqual(publishEntityState.getCall(0).args[0], '0x00000010');
|
||||
assert.deepEqual(publishEntityState.getCall(0).args[1], {state: 'OFF'});
|
||||
assert.strictEqual(publishEntityState.getCall(0).args[2], true);
|
||||
await wait(10);
|
||||
assert.strictEqual(zigbee.publish.callCount, 1);
|
||||
});
|
||||
|
||||
it('Should publish messages to zigbee devices with color xy', async () => {
|
||||
zigbee.publish.resetHistory();
|
||||
zigbee.getDevice = sinon.fake.returns({modelId: 'TRADFRI bulb E27 CWS opal 600lm'});
|
||||
devicePublish.onMQTTMessage('zigbee2mqtt/0x00000011/set', JSON.stringify({color: {x: 100, y: 50}}));
|
||||
assert.strictEqual(zigbee.publish.callCount, 1);
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[0], '0x00000011');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[1], 'device');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[2], 'lightingColorCtrl');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[3], 'moveToColor');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[4], 'functional');
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[5], {colorx: 6553500, colory: 3276750, transtime: 0});
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[6], cfg.default);
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[7], null);
|
||||
await wait(10);
|
||||
assert.strictEqual(zigbee.publish.callCount, 2);
|
||||
});
|
||||
|
||||
it('Should publish messages to zigbee devices with color xy and state', async () => {
|
||||
zigbee.publish.resetHistory();
|
||||
publishEntityState.resetHistory();
|
||||
zigbee.getDevice = sinon.fake.returns({modelId: 'TRADFRI bulb E27 CWS opal 600lm'});
|
||||
const payload = {color: {x: 100, y: 50}, state: 'ON'};
|
||||
devicePublish.onMQTTMessage('zigbee2mqtt/0x00000012/set', JSON.stringify(payload));
|
||||
assert.strictEqual(zigbee.publish.callCount, 2);
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[0], '0x00000012');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[1], 'device');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[2], 'genOnOff');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[3], 'on');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[4], 'functional');
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[5], {});
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[6], cfg.default);
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[7], null);
|
||||
assert.strictEqual(zigbee.publish.getCall(1).args[0], '0x00000012');
|
||||
assert.strictEqual(zigbee.publish.getCall(1).args[1], 'device');
|
||||
assert.strictEqual(zigbee.publish.getCall(1).args[2], 'lightingColorCtrl');
|
||||
assert.strictEqual(zigbee.publish.getCall(1).args[3], 'moveToColor');
|
||||
assert.strictEqual(zigbee.publish.getCall(1).args[4], 'functional');
|
||||
assert.deepEqual(zigbee.publish.getCall(1).args[5], {colorx: 6553500, colory: 3276750, transtime: 0});
|
||||
assert.deepEqual(zigbee.publish.getCall(1).args[6], cfg.default);
|
||||
assert.deepEqual(zigbee.publish.getCall(1).args[7], null);
|
||||
assert.strictEqual(publishEntityState.callCount, 1);
|
||||
assert.strictEqual(publishEntityState.getCall(0).args[0], '0x00000012');
|
||||
assert.deepEqual(publishEntityState.getCall(0).args[1], {state: 'ON'});
|
||||
assert.strictEqual(publishEntityState.getCall(0).args[2], true);
|
||||
await wait(10);
|
||||
assert.strictEqual(zigbee.publish.callCount, 3);
|
||||
});
|
||||
|
||||
it('Should publish messages to zigbee devices with color xy and brightness', async () => {
|
||||
zigbee.publish.resetHistory();
|
||||
publishEntityState.resetHistory();
|
||||
zigbee.getDevice = sinon.fake.returns({modelId: 'TRADFRI bulb E27 CWS opal 600lm'});
|
||||
const payload = {color: {x: 100, y: 50}, brightness: 20};
|
||||
devicePublish.onMQTTMessage('zigbee2mqtt/0x00000013/set', JSON.stringify(payload));
|
||||
assert.strictEqual(zigbee.publish.callCount, 2);
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[0], '0x00000013');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[1], 'device');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[2], 'genLevelCtrl');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[3], 'moveToLevelWithOnOff');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[4], 'functional');
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[5], {level: 20, transtime: 0});
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[6], cfg.default);
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[7], null);
|
||||
assert.strictEqual(zigbee.publish.getCall(1).args[0], '0x00000013');
|
||||
assert.strictEqual(zigbee.publish.getCall(1).args[1], 'device');
|
||||
assert.strictEqual(zigbee.publish.getCall(1).args[2], 'lightingColorCtrl');
|
||||
assert.strictEqual(zigbee.publish.getCall(1).args[3], 'moveToColor');
|
||||
assert.strictEqual(zigbee.publish.getCall(1).args[4], 'functional');
|
||||
assert.deepEqual(zigbee.publish.getCall(1).args[5], {colorx: 6553500, colory: 3276750, transtime: 0});
|
||||
assert.deepEqual(zigbee.publish.getCall(1).args[6], cfg.default);
|
||||
assert.deepEqual(zigbee.publish.getCall(1).args[7], null);
|
||||
assert.strictEqual(publishEntityState.callCount, 1);
|
||||
assert.strictEqual(publishEntityState.getCall(0).args[0], '0x00000013');
|
||||
assert.deepEqual(publishEntityState.getCall(0).args[1], {state: 'ON', brightness: 20});
|
||||
assert.strictEqual(publishEntityState.getCall(0).args[2], true);
|
||||
await wait(10);
|
||||
assert.strictEqual(zigbee.publish.callCount, 4);
|
||||
});
|
||||
|
||||
it('Should publish messages to zigbee devices with color xy, brightness and state on', async () => {
|
||||
zigbee.publish.resetHistory();
|
||||
publishEntityState.resetHistory();
|
||||
zigbee.getDevice = sinon.fake.returns({modelId: 'TRADFRI bulb E27 CWS opal 600lm'});
|
||||
const payload = {color: {x: 100, y: 50}, brightness: 20, state: 'oN'};
|
||||
devicePublish.onMQTTMessage('zigbee2mqtt/0x00000014/set', JSON.stringify(payload));
|
||||
assert.strictEqual(zigbee.publish.callCount, 2);
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[0], '0x00000014');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[1], 'device');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[2], 'genLevelCtrl');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[3], 'moveToLevelWithOnOff');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[4], 'functional');
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[5], {level: 20, transtime: 0});
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[6], cfg.default);
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[7], null);
|
||||
assert.strictEqual(zigbee.publish.getCall(1).args[0], '0x00000014');
|
||||
assert.strictEqual(zigbee.publish.getCall(1).args[1], 'device');
|
||||
assert.strictEqual(zigbee.publish.getCall(1).args[2], 'lightingColorCtrl');
|
||||
assert.strictEqual(zigbee.publish.getCall(1).args[3], 'moveToColor');
|
||||
assert.strictEqual(zigbee.publish.getCall(1).args[4], 'functional');
|
||||
assert.deepEqual(zigbee.publish.getCall(1).args[5], {colorx: 6553500, colory: 3276750, transtime: 0});
|
||||
assert.deepEqual(zigbee.publish.getCall(1).args[6], cfg.default);
|
||||
assert.deepEqual(zigbee.publish.getCall(1).args[7], null);
|
||||
assert.strictEqual(publishEntityState.callCount, 1);
|
||||
assert.strictEqual(publishEntityState.getCall(0).args[0], '0x00000014');
|
||||
assert.deepEqual(publishEntityState.getCall(0).args[1], {state: 'ON', brightness: 20});
|
||||
assert.strictEqual(publishEntityState.getCall(0).args[2], true);
|
||||
await wait(10);
|
||||
assert.strictEqual(zigbee.publish.callCount, 4);
|
||||
});
|
||||
|
||||
it('Should publish messages to zigbee devices with color xy, brightness and state off', async () => {
|
||||
zigbee.publish.resetHistory();
|
||||
publishEntityState.resetHistory();
|
||||
zigbee.getDevice = sinon.fake.returns({modelId: 'TRADFRI bulb E27 CWS opal 600lm'});
|
||||
const payload = {color: {x: 100, y: 50}, brightness: 20, state: 'oFF'};
|
||||
devicePublish.onMQTTMessage('zigbee2mqtt/0x00000015/set', JSON.stringify(payload));
|
||||
assert.strictEqual(zigbee.publish.callCount, 2);
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[0], '0x00000015');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[1], 'device');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[2], 'genOnOff');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[3], 'off');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[4], 'functional');
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[5], {});
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[6], cfg.default);
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[7], null);
|
||||
assert.strictEqual(zigbee.publish.getCall(1).args[0], '0x00000015');
|
||||
assert.strictEqual(zigbee.publish.getCall(1).args[1], 'device');
|
||||
assert.strictEqual(zigbee.publish.getCall(1).args[2], 'lightingColorCtrl');
|
||||
assert.strictEqual(zigbee.publish.getCall(1).args[3], 'moveToColor');
|
||||
assert.strictEqual(zigbee.publish.getCall(1).args[4], 'functional');
|
||||
assert.deepEqual(zigbee.publish.getCall(1).args[5], {colorx: 6553500, colory: 3276750, transtime: 0});
|
||||
assert.deepEqual(zigbee.publish.getCall(1).args[6], cfg.default);
|
||||
assert.deepEqual(zigbee.publish.getCall(1).args[7], null);
|
||||
assert.strictEqual(publishEntityState.callCount, 1);
|
||||
assert.strictEqual(publishEntityState.getCall(0).args[0], '0x00000015');
|
||||
assert.deepEqual(publishEntityState.getCall(0).args[1], {state: 'OFF'});
|
||||
assert.strictEqual(publishEntityState.getCall(0).args[2], true);
|
||||
await wait(10);
|
||||
assert.strictEqual(zigbee.publish.callCount, 3);
|
||||
});
|
||||
|
||||
it('Should publish messages to zigbee devices with color rgb', async () => {
|
||||
zigbee.publish.resetHistory();
|
||||
zigbee.getDevice = sinon.fake.returns({modelId: 'TRADFRI bulb E27 CWS opal 600lm'});
|
||||
devicePublish.onMQTTMessage('zigbee2mqtt/0x00000016/set', JSON.stringify({color: {r: 100, g: 200, b: 10}}));
|
||||
assert.strictEqual(zigbee.publish.callCount, 1);
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[0], '0x00000016');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[1], 'device');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[2], 'lightingColorCtrl');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[3], 'moveToColor');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[4], 'functional');
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[5], {colorx: 17085, colory: 44000, transtime: 0});
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[6], cfg.default);
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[7], null);
|
||||
await wait(10);
|
||||
assert.strictEqual(zigbee.publish.callCount, 2);
|
||||
});
|
||||
|
||||
it('Should publish messages to zigbee devices with color rgb string', async () => {
|
||||
zigbee.publish.resetHistory();
|
||||
zigbee.getDevice = sinon.fake.returns({modelId: 'TRADFRI bulb E27 CWS opal 600lm'});
|
||||
devicePublish.onMQTTMessage('zigbee2mqtt/0x00000017/set', JSON.stringify({color: {rgb: '100,200,10'}}));
|
||||
assert.strictEqual(zigbee.publish.callCount, 1);
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[0], '0x00000017');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[1], 'device');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[2], 'lightingColorCtrl');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[3], 'moveToColor');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[4], 'functional');
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[5], {colorx: 17085, colory: 44000, transtime: 0});
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[6], cfg.default);
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[7], null);
|
||||
await wait(10);
|
||||
assert.strictEqual(zigbee.publish.callCount, 2);
|
||||
});
|
||||
|
||||
it('Should publish 1 message when brightness with state is send', async () => {
|
||||
zigbee.publish.resetHistory();
|
||||
publishEntityState.resetHistory();
|
||||
zigbee.getDevice = sinon.fake.returns({modelId: 'TRADFRI bulb E27 CWS opal 600lm'});
|
||||
devicePublish.onMQTTMessage('zigbee2mqtt/0x00000018/set', JSON.stringify({state: 'ON', brightness: '50'}));
|
||||
assert.strictEqual(zigbee.publish.callCount, 1);
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[0], '0x00000018');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[1], 'device');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[2], 'genLevelCtrl');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[3], 'moveToLevelWithOnOff');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[4], 'functional');
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[5], {level: 50, transtime: 0});
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[6], cfg.default);
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[7], null);
|
||||
assert.strictEqual(publishEntityState.callCount, 1);
|
||||
assert.strictEqual(publishEntityState.getCall(0).args[0], '0x00000018');
|
||||
assert.deepEqual(publishEntityState.getCall(0).args[1], {state: 'ON', brightness: 50});
|
||||
assert.strictEqual(publishEntityState.getCall(0).args[2], true);
|
||||
await wait(10);
|
||||
assert.strictEqual(zigbee.publish.callCount, 2);
|
||||
});
|
||||
|
||||
it('Should publish messages to groups', async () => {
|
||||
sinon.stub(settings, 'getGroupIDByFriendlyName').callsFake(() => '1');
|
||||
zigbee.publish.resetHistory();
|
||||
publishEntityState.resetHistory();
|
||||
devicePublish.onMQTTMessage('zigbee2mqtt/group/group_1/set', JSON.stringify({state: 'ON'}));
|
||||
assert.strictEqual(zigbee.publish.callCount, 1);
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[0], 1);
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[1], 'group');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[2], 'genOnOff');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[3], 'on');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[4], 'functional');
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[5], {});
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[6], cfg.default);
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[7], null);
|
||||
assert.strictEqual(publishEntityState.callCount, 1);
|
||||
assert.strictEqual(publishEntityState.getCall(0).args[0], 1);
|
||||
assert.deepEqual(publishEntityState.getCall(0).args[1], {state: 'ON'});
|
||||
assert.strictEqual(publishEntityState.getCall(0).args[2], true);
|
||||
await wait(10);
|
||||
assert.strictEqual(zigbee.publish.callCount, 1);
|
||||
});
|
||||
|
||||
it('Should publish messages to groups with brightness_percent', async () => {
|
||||
sinon.stub(settings, 'getGroupIDByFriendlyName').callsFake(() => '1');
|
||||
zigbee.publish.resetHistory();
|
||||
publishEntityState.resetHistory();
|
||||
devicePublish.onMQTTMessage('zigbee2mqtt/group/group_1/set', JSON.stringify({brightness_percent: 50}));
|
||||
assert.strictEqual(zigbee.publish.callCount, 1);
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[0], 1);
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[1], 'group');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[2], 'genLevelCtrl');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[3], 'moveToLevelWithOnOff');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[4], 'functional');
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[5], {level: 127, transtime: 0});
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[6], cfg.default);
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[7], null);
|
||||
assert.strictEqual(publishEntityState.callCount, 1);
|
||||
assert.strictEqual(publishEntityState.getCall(0).args[0], 1);
|
||||
assert.deepEqual(publishEntityState.getCall(0).args[1], {state: 'ON', brightness: 127});
|
||||
assert.strictEqual(publishEntityState.getCall(0).args[2], true);
|
||||
await wait(10);
|
||||
assert.strictEqual(zigbee.publish.callCount, 1);
|
||||
});
|
||||
|
||||
it('Should publish messages to groups with on and brightness', async () => {
|
||||
sinon.stub(settings, 'getGroupIDByFriendlyName').callsFake(() => '1');
|
||||
zigbee.publish.resetHistory();
|
||||
publishEntityState.resetHistory();
|
||||
devicePublish.onMQTTMessage('zigbee2mqtt/group/group_1/set', JSON.stringify({state: 'ON', brightness: 50}));
|
||||
assert.strictEqual(zigbee.publish.callCount, 1);
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[0], 1);
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[1], 'group');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[2], 'genLevelCtrl');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[3], 'moveToLevelWithOnOff');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[4], 'functional');
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[5], {level: 50, transtime: 0});
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[6], cfg.default);
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[7], null);
|
||||
assert.strictEqual(publishEntityState.callCount, 1);
|
||||
assert.strictEqual(publishEntityState.getCall(0).args[0], 1);
|
||||
assert.deepEqual(publishEntityState.getCall(0).args[1], {state: 'ON', brightness: 50});
|
||||
assert.strictEqual(publishEntityState.getCall(0).args[2], true);
|
||||
await wait(10);
|
||||
assert.strictEqual(zigbee.publish.callCount, 1);
|
||||
});
|
||||
|
||||
it('Should publish messages to groups with off and brightness', async () => {
|
||||
sinon.stub(settings, 'getGroupIDByFriendlyName').callsFake(() => '1');
|
||||
zigbee.publish.resetHistory();
|
||||
publishEntityState.resetHistory();
|
||||
devicePublish.onMQTTMessage('zigbee2mqtt/group/group_1/set', JSON.stringify({state: 'OFF', brightness: 5}));
|
||||
assert.strictEqual(zigbee.publish.callCount, 1);
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[0], 1);
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[1], 'group');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[2], 'genOnOff');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[3], 'off');
|
||||
assert.strictEqual(zigbee.publish.getCall(0).args[4], 'functional');
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[5], {});
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[6], cfg.default);
|
||||
assert.deepEqual(zigbee.publish.getCall(0).args[7], null);
|
||||
assert.strictEqual(publishEntityState.callCount, 1);
|
||||
assert.strictEqual(publishEntityState.getCall(0).args[0], 1);
|
||||
assert.deepEqual(publishEntityState.getCall(0).args[1], {state: 'OFF'});
|
||||
assert.strictEqual(publishEntityState.getCall(0).args[2], true);
|
||||
await wait(10);
|
||||
assert.strictEqual(zigbee.publish.callCount, 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Parse topic', () => {
|
||||
it('Should handle non-valid topics', () => {
|
||||
const topic = 'zigbee2mqtt1/my_device_id/set';
|
||||
const parsed = devicePublish.parseTopic(topic);
|
||||
assert.strictEqual(parsed, null);
|
||||
});
|
||||
|
||||
it('Should handle non-valid topics', () => {
|
||||
const topic = 'zigbee2mqtt1/my_device_id/sett';
|
||||
const parsed = devicePublish.parseTopic(topic);
|
||||
assert.strictEqual(parsed, null);
|
||||
});
|
||||
|
||||
it('Should handle non-valid topics', () => {
|
||||
const topic = 'zigbee2mqtt/my_device_id/write';
|
||||
const parsed = devicePublish.parseTopic(topic);
|
||||
assert.strictEqual(parsed, null);
|
||||
});
|
||||
|
||||
it('Should handle non-valid topics', () => {
|
||||
const topic = 'zigbee2mqtt/set';
|
||||
const parsed = devicePublish.parseTopic(topic);
|
||||
assert.strictEqual(parsed, null);
|
||||
});
|
||||
|
||||
it('Should handle non-valid topics', () => {
|
||||
const topic = 'set';
|
||||
const parsed = devicePublish.parseTopic(topic);
|
||||
assert.strictEqual(parsed, null);
|
||||
});
|
||||
|
||||
it('Should parse set topic', () => {
|
||||
const topic = 'zigbee2mqtt/my_device_id/set';
|
||||
const parsed = devicePublish.parseTopic(topic);
|
||||
assert.strictEqual(parsed.type, 'set');
|
||||
assert.strictEqual(parsed.ID, 'my_device_id');
|
||||
assert.strictEqual(parsed.postfix, '');
|
||||
});
|
||||
|
||||
it('Should parse get topic', () => {
|
||||
const topic = 'zigbee2mqtt/my_device_id2/get';
|
||||
const parsed = devicePublish.parseTopic(topic);
|
||||
assert.strictEqual(parsed.type, 'get');
|
||||
assert.strictEqual(parsed.ID, 'my_device_id2');
|
||||
assert.strictEqual(parsed.postfix, '');
|
||||
});
|
||||
|
||||
it('Should parse topic with when base topic has multiple slashes', () => {
|
||||
sinon.stub(settings, 'get').callsFake(() => {
|
||||
return {
|
||||
mqtt: {
|
||||
base_topic: 'zigbee2mqtt/at/my/home',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const topic = 'zigbee2mqtt/at/my/home/my_device_id2/get';
|
||||
const parsed = devicePublish.parseTopic(topic);
|
||||
assert.strictEqual(parsed.type, 'get');
|
||||
assert.strictEqual(parsed.ID, 'my_device_id2');
|
||||
assert.strictEqual(parsed.postfix, '');
|
||||
});
|
||||
|
||||
it('Should parse topic with when deviceID has multiple slashes', () => {
|
||||
const topic = 'zigbee2mqtt/floor0/basement/my_device_id2/set';
|
||||
const parsed = devicePublish.parseTopic(topic);
|
||||
assert.strictEqual(parsed.type, 'set');
|
||||
assert.strictEqual(parsed.ID, 'floor0/basement/my_device_id2');
|
||||
assert.strictEqual(parsed.postfix, '');
|
||||
});
|
||||
|
||||
it('Should parse topic with when base and deviceID have multiple slashes', () => {
|
||||
sinon.stub(settings, 'get').callsFake(() => {
|
||||
return {
|
||||
mqtt: {
|
||||
base_topic: 'zigbee2mqtt/at/my/basement',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const topic = 'zigbee2mqtt/at/my/basement/floor0/basement/my_device_id2/set';
|
||||
const parsed = devicePublish.parseTopic(topic);
|
||||
assert.strictEqual(parsed.type, 'set');
|
||||
assert.strictEqual(parsed.ID, 'floor0/basement/my_device_id2');
|
||||
assert.strictEqual(parsed.postfix, '');
|
||||
});
|
||||
|
||||
it('Should parse set with ieeAddr topic', () => {
|
||||
const topic = 'zigbee2mqtt/0x12345689/set';
|
||||
const parsed = devicePublish.parseTopic(topic);
|
||||
assert.strictEqual(parsed.type, 'set');
|
||||
assert.strictEqual(parsed.ID, '0x12345689');
|
||||
assert.strictEqual(parsed.postfix, '');
|
||||
});
|
||||
|
||||
it('Should parse set with postfix topic', () => {
|
||||
const topic = 'zigbee2mqtt/0x12345689/left/set';
|
||||
const parsed = devicePublish.parseTopic(topic);
|
||||
assert.strictEqual(parsed.type, 'set');
|
||||
assert.strictEqual(parsed.ID, '0x12345689');
|
||||
assert.strictEqual(parsed.postfix, 'left');
|
||||
});
|
||||
|
||||
it('Should parse set with almost postfix topic', () => {
|
||||
const topic = 'zigbee2mqtt/wohnzimmer.light.wall.right/set';
|
||||
const parsed = devicePublish.parseTopic(topic);
|
||||
assert.strictEqual(parsed.type, 'set');
|
||||
assert.strictEqual(parsed.ID, 'wohnzimmer.light.wall.right');
|
||||
assert.strictEqual(parsed.postfix, '');
|
||||
});
|
||||
|
||||
it('Should parse set with postfix topic', () => {
|
||||
const topic = 'zigbee2mqtt/0x12345689/right/set';
|
||||
const parsed = devicePublish.parseTopic(topic);
|
||||
assert.strictEqual(parsed.type, 'set');
|
||||
assert.strictEqual(parsed.ID, '0x12345689');
|
||||
assert.strictEqual(parsed.postfix, 'right');
|
||||
});
|
||||
|
||||
it('Should parse set with postfix topic', () => {
|
||||
const topic = 'zigbee2mqtt/0x12345689/bottom_left/set';
|
||||
const parsed = devicePublish.parseTopic(topic);
|
||||
assert.strictEqual(parsed.type, 'set');
|
||||
assert.strictEqual(parsed.ID, '0x12345689');
|
||||
assert.strictEqual(parsed.postfix, 'bottom_left');
|
||||
});
|
||||
|
||||
it('Shouldnt parse set with invalid postfix topic', () => {
|
||||
const topic = 'zigbee2mqtt/0x12345689/invalid/set';
|
||||
const parsed = devicePublish.parseTopic(topic);
|
||||
assert.strictEqual(parsed.type, 'set');
|
||||
assert.strictEqual(parsed.ID, '0x12345689/invalid');
|
||||
assert.strictEqual(parsed.postfix, '');
|
||||
});
|
||||
|
||||
it('Should parse set with and slashes in base and deviceID postfix topic', () => {
|
||||
sinon.stub(settings, 'get').callsFake(() => {
|
||||
return {
|
||||
mqtt: {
|
||||
base_topic: 'zigbee2mqtt/at/my/home',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const topic = 'zigbee2mqtt/at/my/home/my/device/in/basement/sensor/bottom_left/get';
|
||||
const parsed = devicePublish.parseTopic(topic);
|
||||
assert.strictEqual(parsed.type, 'get');
|
||||
assert.strictEqual(parsed.ID, 'my/device/in/basement/sensor');
|
||||
assert.strictEqual(parsed.postfix, 'bottom_left');
|
||||
});
|
||||
});
|
||||
|
||||
it('Should not publish messages to zigbee devices when payload is invalid', async () => {
|
||||
zigbee.publish.resetHistory();
|
||||
zigbee.getDevice = sinon.fake.returns({modelId: 'lumi.ctrl_neutral1'});
|
||||
devicePublish.onMQTTMessage('zigbee2mqtt/0x00000019/set', JSON.stringify({state: true}));
|
||||
await wait(10);
|
||||
assert.isTrue(zigbee.publish.notCalled);
|
||||
devicePublish.onMQTTMessage('zigbee2mqtt/0x00000019/set', JSON.stringify({state: 1}));
|
||||
await wait(10);
|
||||
assert.isTrue(zigbee.publish.notCalled);
|
||||
});
|
||||
|
||||
it('Should set state before color', async () => {
|
||||
zigbee.publish.resetHistory();
|
||||
zigbee.getDevice = sinon.fake.returns({modelId: 'LCT001'});
|
||||
const msg = {'state': 'ON', 'color': {'x': 0.701, 'y': 0.299}};
|
||||
devicePublish.onMQTTMessage('zigbee2mqtt/0x00000020/set', JSON.stringify(msg));
|
||||
assert.equal(zigbee.publish.callCount, 2);
|
||||
assert.equal(zigbee.publish.getCall(0).args[2], 'genOnOff');
|
||||
assert.equal(zigbee.publish.getCall(0).args[3], 'on');
|
||||
assert.equal(zigbee.publish.getCall(1).args[2], 'lightingColorCtrl');
|
||||
assert.equal(zigbee.publish.getCall(1).args[3], 'moveToColor');
|
||||
await wait(10);
|
||||
assert.equal(zigbee.publish.callCount, 3);
|
||||
assert.equal(zigbee.publish.getCall(2).args[2], 'lightingColorCtrl');
|
||||
assert.equal(zigbee.publish.getCall(2).args[3], 'read');
|
||||
});
|
||||
|
||||
it('Should set state with brightness before color', async () => {
|
||||
zigbee.publish.resetHistory();
|
||||
zigbee.getDevice = sinon.fake.returns({modelId: 'LCT001'});
|
||||
const msg = {'state': 'ON', 'color': {'x': 0.701, 'y': 0.299}, 'transition': 3, 'brightness': 100};
|
||||
devicePublish.onMQTTMessage('zigbee2mqtt/0x00000021/set', JSON.stringify(msg));
|
||||
assert.strictEqual(zigbee.publish.callCount, 2);
|
||||
assert.equal(zigbee.publish.getCall(0).args[2], 'genLevelCtrl');
|
||||
assert.equal(zigbee.publish.getCall(1).args[2], 'lightingColorCtrl');
|
||||
await wait(10);
|
||||
assert.strictEqual(zigbee.publish.callCount, 2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,243 @@
|
||||
const assert = require('chai').assert;
|
||||
const sinon = require('sinon');
|
||||
const DeviceReceive = require('../lib/extension/deviceReceive');
|
||||
const settings = require('../lib/util/settings');
|
||||
const devices = require('zigbee-shepherd-converters').devices;
|
||||
const utils = require('./utils');
|
||||
|
||||
// Devices
|
||||
const WXKG11LM = devices.find((d) => d.model === 'WXKG11LM');
|
||||
const WXKG02LM = devices.find((d) => d.model === 'WXKG02LM');
|
||||
const WSDCGQ11LM = devices.find((d) => d.model === 'WSDCGQ11LM');
|
||||
const RTCGQ11LM = devices.find((d) => d.model === 'RTCGQ11LM');
|
||||
|
||||
const mqtt = {
|
||||
log: () => {},
|
||||
};
|
||||
|
||||
describe('DeviceReceive', () => {
|
||||
let deviceReceive;
|
||||
let publishEntityState;
|
||||
|
||||
beforeEach(() => {
|
||||
utils.stubLogger(sinon);
|
||||
sinon.stub(settings, 'addDevice').callsFake(() => {});
|
||||
publishEntityState = sinon.spy();
|
||||
deviceReceive = new DeviceReceive(null, mqtt, null, publishEntityState);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
describe('Handling zigbee messages', () => {
|
||||
it('Should handle a zigbee message', () => {
|
||||
const device = {ieeeAddr: '0x12345678'};
|
||||
const message = utils.zigbeeMessage(device, 'genOnOff', 'attReport', {onOff: 1}, 1);
|
||||
deviceReceive.onZigbeeMessage(message, device, WXKG11LM);
|
||||
assert.isTrue(publishEntityState.calledOnce);
|
||||
assert.deepEqual(publishEntityState.getCall(0).args[1], {click: 'single'});
|
||||
});
|
||||
|
||||
it('Should handle a zigbee message which uses ep (left)', () => {
|
||||
const device = {ieeeAddr: '0x12345678'};
|
||||
const message = utils.zigbeeMessage(device, 'genOnOff', 'attReport', {onOff: 1}, 1);
|
||||
deviceReceive.onZigbeeMessage(message, device, WXKG02LM);
|
||||
assert.isTrue(publishEntityState.calledOnce);
|
||||
assert.deepEqual(publishEntityState.getCall(0).args[1], {click: 'left'});
|
||||
});
|
||||
|
||||
it('Should handle a zigbee message which uses ep (right)', () => {
|
||||
const device = {ieeeAddr: '0x12345678'};
|
||||
const message = utils.zigbeeMessage(device, 'genOnOff', 'attReport', {onOff: 1}, 2);
|
||||
deviceReceive.onZigbeeMessage(message, device, WXKG02LM);
|
||||
assert.isTrue(publishEntityState.calledOnce);
|
||||
assert.deepEqual(publishEntityState.getCall(0).args[1], {click: 'right'});
|
||||
});
|
||||
|
||||
it('Should handle a zigbee message with default precision', () => {
|
||||
const device = {ieeeAddr: '0x12345678'};
|
||||
const message = utils.zigbeeMessage(
|
||||
device, 'msTemperatureMeasurement', 'attReport', {measuredValue: -85}, 1
|
||||
);
|
||||
deviceReceive.onZigbeeMessage(message, device, WSDCGQ11LM);
|
||||
assert.isTrue(publishEntityState.calledOnce);
|
||||
assert.deepEqual(publishEntityState.getCall(0).args[1], {temperature: -0.85});
|
||||
});
|
||||
|
||||
it('Should handle a zigbee message with 1 precision', () => {
|
||||
const device = {ieeeAddr: '0x12345678'};
|
||||
sinon.stub(settings, 'getDevice').callsFake(() => {
|
||||
return {temperature_precision: 1};
|
||||
});
|
||||
const message = utils.zigbeeMessage(
|
||||
device, 'msTemperatureMeasurement', 'attReport', {measuredValue: -85}, 1
|
||||
);
|
||||
deviceReceive.onZigbeeMessage(message, device, WSDCGQ11LM);
|
||||
assert.isTrue(publishEntityState.calledOnce);
|
||||
assert.deepEqual(publishEntityState.getCall(0).args[1], {temperature: -0.8});
|
||||
});
|
||||
|
||||
it('Should handle a zigbee message with 0 precision', () => {
|
||||
const device = {ieeeAddr: '0x12345678'};
|
||||
sinon.stub(settings, 'getDevice').callsFake(() => {
|
||||
return {temperature_precision: 0};
|
||||
});
|
||||
const message = utils.zigbeeMessage(
|
||||
device, 'msTemperatureMeasurement', 'attReport', {measuredValue: -85}, 1
|
||||
);
|
||||
deviceReceive.onZigbeeMessage(message, device, WSDCGQ11LM);
|
||||
assert.isTrue(publishEntityState.calledOnce);
|
||||
assert.deepEqual(publishEntityState.getCall(0).args[1], {temperature: -1});
|
||||
});
|
||||
|
||||
it('Should handle a zigbee message with 1 precision when set via device_options', () => {
|
||||
const device = {ieeeAddr: '0x12345678'};
|
||||
sinon.stub(settings, 'get').callsFake(() => {
|
||||
return {
|
||||
device_options: {
|
||||
temperature_precision: 1,
|
||||
},
|
||||
advanced: {
|
||||
last_seen: 'disable',
|
||||
},
|
||||
};
|
||||
});
|
||||
sinon.stub(settings, 'getDevice').callsFake(() => {
|
||||
return {};
|
||||
});
|
||||
const message = utils.zigbeeMessage(
|
||||
device, 'msTemperatureMeasurement', 'attReport', {measuredValue: -85}, 1
|
||||
);
|
||||
deviceReceive.onZigbeeMessage(message, device, WSDCGQ11LM);
|
||||
assert.isTrue(publishEntityState.calledOnce);
|
||||
assert.deepEqual(publishEntityState.getCall(0).args[1], {temperature: -0.8});
|
||||
});
|
||||
|
||||
it('Should handle a zigbee message with 2 precision when overrides device_options', () => {
|
||||
const device = {ieeeAddr: '0x12345678'};
|
||||
sinon.stub(settings, 'get').callsFake(() => {
|
||||
return {
|
||||
device_options: {
|
||||
temperature_precision: 1,
|
||||
},
|
||||
advanced: {
|
||||
last_seen: 'disable',
|
||||
},
|
||||
};
|
||||
});
|
||||
sinon.stub(settings, 'getDevice').callsFake(() => {
|
||||
return {
|
||||
temperature_precision: 2,
|
||||
};
|
||||
});
|
||||
const message = utils.zigbeeMessage(
|
||||
device, 'msTemperatureMeasurement', 'attReport', {measuredValue: -85}, 1
|
||||
);
|
||||
deviceReceive.onZigbeeMessage(message, device, WSDCGQ11LM);
|
||||
assert.isTrue(publishEntityState.calledOnce);
|
||||
assert.deepEqual(publishEntityState.getCall(0).args[1], {temperature: -0.85});
|
||||
});
|
||||
|
||||
it('Should handle a zigbee message with voltage 3010', () => {
|
||||
const device = {ieeeAddr: '0x12345678'};
|
||||
const message = utils.zigbeeMessage(device, 'genBasic', 'attReport', {'65281': {'1': 3010}}, 1);
|
||||
deviceReceive.onZigbeeMessage(message, device, WXKG02LM);
|
||||
assert.isTrue(publishEntityState.calledOnce);
|
||||
const expected = {battery: 100, voltage: 3010};
|
||||
assert.deepEqual(publishEntityState.getCall(0).args[1], expected);
|
||||
});
|
||||
|
||||
it('Should handle a zigbee message with voltage 2850', () => {
|
||||
const device = {ieeeAddr: '0x12345678'};
|
||||
const message = utils.zigbeeMessage(device, 'genBasic', 'attReport', {'65281': {'1': 2850}}, 1);
|
||||
deviceReceive.onZigbeeMessage(message, device, WXKG02LM);
|
||||
assert.isTrue(publishEntityState.calledOnce);
|
||||
const expected = {battery: 35, voltage: 2850};
|
||||
assert.deepEqual(publishEntityState.getCall(0).args[1], expected);
|
||||
});
|
||||
|
||||
it('Should handle a zigbee message with voltage 2650', () => {
|
||||
const device = {ieeeAddr: '0x12345678'};
|
||||
const message = utils.zigbeeMessage(device, 'genBasic', 'attReport', {'65281': {'1': 2650}}, 1);
|
||||
deviceReceive.onZigbeeMessage(message, device, WXKG02LM);
|
||||
assert.isTrue(publishEntityState.calledOnce);
|
||||
const expected = {battery: 14, voltage: 2650};
|
||||
assert.deepEqual(publishEntityState.getCall(0).args[1], expected);
|
||||
});
|
||||
|
||||
it('Should handle a zigbee message with voltage 2000', () => {
|
||||
const device = {ieeeAddr: '0x12345678'};
|
||||
const message = utils.zigbeeMessage(device, 'genBasic', 'attReport', {'65281': {'1': 2000}}, 1);
|
||||
deviceReceive.onZigbeeMessage(message, device, WXKG02LM);
|
||||
assert.isTrue(publishEntityState.calledOnce);
|
||||
const expected = {battery: 0, voltage: 2000};
|
||||
assert.deepEqual(publishEntityState.getCall(0).args[1], expected);
|
||||
});
|
||||
|
||||
it('Should publish 1 message when converted twice', () => {
|
||||
const device = {ieeeAddr: '0x12345678'};
|
||||
const payload = {
|
||||
'65281': {'1': 3045, '3': 19, '4': 17320, '5': 35, '6': [0, 3], '10': 51107, '11': 381, '100': 0},
|
||||
};
|
||||
const message = utils.zigbeeMessage(device, 'genBasic', 'attReport', payload, 1);
|
||||
deviceReceive.onZigbeeMessage(message, device, RTCGQ11LM);
|
||||
assert.isTrue(publishEntityState.calledOnce);
|
||||
const expected = {'battery': 100, 'illuminance': 381, 'voltage': 3045};
|
||||
assert.deepEqual(publishEntityState.getCall(0).args[1], expected);
|
||||
});
|
||||
|
||||
it('Should publish no message when converted without result', () => {
|
||||
const device = {ieeeAddr: '0x12345678'};
|
||||
const payload = {'9999': {'1': 3045}};
|
||||
const message = utils.zigbeeMessage(device, 'genBasic', 'attReport', payload, 1);
|
||||
deviceReceive.onZigbeeMessage(message, device, RTCGQ11LM);
|
||||
assert.isTrue(publishEntityState.notCalled);
|
||||
});
|
||||
|
||||
it('Should publish last_seen epoch', () => {
|
||||
const device = {ieeeAddr: '0x12345678'};
|
||||
const message = utils.zigbeeMessage(device, 'genOnOff', 'attReport', {onOff: 1}, 1);
|
||||
sinon.stub(settings, 'get').callsFake(() => {
|
||||
return {
|
||||
advanced: {
|
||||
last_seen: 'epoch',
|
||||
},
|
||||
};
|
||||
});
|
||||
deviceReceive.onZigbeeMessage(message, device, WXKG02LM);
|
||||
assert.isTrue(publishEntityState.calledOnce);
|
||||
assert.equal(typeof publishEntityState.getCall(0).args[1].last_seen, 'number');
|
||||
});
|
||||
|
||||
it('Should publish last_seen ISO_8601', () => {
|
||||
const device = {ieeeAddr: '0x12345678'};
|
||||
const message = utils.zigbeeMessage(device, 'genOnOff', 'attReport', {onOff: 1}, 1);
|
||||
sinon.stub(settings, 'get').callsFake(() => {
|
||||
return {
|
||||
advanced: {
|
||||
last_seen: 'ISO_8601',
|
||||
},
|
||||
};
|
||||
});
|
||||
deviceReceive.onZigbeeMessage(message, device, WXKG02LM);
|
||||
assert.isTrue(publishEntityState.calledOnce);
|
||||
assert.equal(typeof publishEntityState.getCall(0).args[1].last_seen, 'string');
|
||||
});
|
||||
|
||||
it('Should publish last_seen ISO_8601_local', () => {
|
||||
const device = {ieeeAddr: '0x12345678'};
|
||||
const message = utils.zigbeeMessage(device, 'genOnOff', 'attReport', {onOff: 1}, 1);
|
||||
sinon.stub(settings, 'get').callsFake(() => {
|
||||
return {
|
||||
advanced: {
|
||||
last_seen: 'ISO_8601_local',
|
||||
},
|
||||
};
|
||||
});
|
||||
deviceReceive.onZigbeeMessage(message, device, WXKG02LM);
|
||||
assert.isTrue(publishEntityState.calledOnce);
|
||||
assert.equal(typeof publishEntityState.getCall(0).args[1].last_seen, 'string');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,483 @@
|
||||
const devices = require('zigbee-shepherd-converters').devices;
|
||||
const HomeassistantExtension = require('../lib/extension/homeassistant');
|
||||
const assert = require('chai').assert;
|
||||
const sinon = require('sinon');
|
||||
const settings = require('../lib/util/settings');
|
||||
|
||||
const WSDCGQ11LM = devices.find((d) => d.model === 'WSDCGQ11LM');
|
||||
const SV01 = devices.find((d) => d.model === 'SV01');
|
||||
|
||||
describe('HomeAssistant extension', () => {
|
||||
let homeassistant = null;
|
||||
let mqtt = null;
|
||||
|
||||
beforeEach(() => {
|
||||
mqtt = {
|
||||
publish: sinon.spy(),
|
||||
};
|
||||
|
||||
homeassistant = new HomeassistantExtension(null, mqtt, null, null);
|
||||
homeassistant.zigbee2mqttVersion = 'test';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it('Should have mapping for all devices supported by zigbee-shepherd-converters', () => {
|
||||
const missing = [];
|
||||
|
||||
devices.forEach((d) => {
|
||||
if (!homeassistant._getMapping()[d.model]) {
|
||||
missing.push(d.model);
|
||||
}
|
||||
});
|
||||
|
||||
assert.strictEqual(missing.length, 0, `Missing HomeAssistant mapping for: ${missing.join(', ')}`);
|
||||
});
|
||||
|
||||
it('Should discover devices', () => {
|
||||
let payload = null;
|
||||
sinon.stub(settings, 'getDevice').callsFake(() => {
|
||||
return {friendly_name: 'my_device'};
|
||||
});
|
||||
|
||||
homeassistant.discover('0x12345678', WSDCGQ11LM, false);
|
||||
assert.equal(mqtt.publish.callCount, 5);
|
||||
|
||||
// 1
|
||||
payload = {
|
||||
'unit_of_measurement': '°C',
|
||||
'device_class': 'temperature',
|
||||
'value_template': '{{ value_json.temperature }}',
|
||||
'json_attributes_topic': 'zigbee2mqtt/my_device',
|
||||
'state_topic': 'zigbee2mqtt/my_device',
|
||||
'name': 'my_device_temperature',
|
||||
'unique_id': '0x12345678_temperature_zigbee2mqtt',
|
||||
'device': {
|
||||
'identifiers': 'zigbee2mqtt_0x12345678',
|
||||
'name': 'my_device',
|
||||
'sw_version': 'Zigbee2mqtt test',
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
},
|
||||
'availability_topic': 'zigbee2mqtt/bridge/state',
|
||||
};
|
||||
|
||||
assert.deepEqual(JSON.parse(mqtt.publish.getCall(0).args[1]), payload);
|
||||
assert.deepEqual(mqtt.publish.getCall(0).args[2], {retain: true, qos: 0});
|
||||
assert.equal(mqtt.publish.getCall(0).args[3], null);
|
||||
assert.equal(mqtt.publish.getCall(0).args[4], 'homeassistant');
|
||||
|
||||
// 2
|
||||
payload = {
|
||||
'unit_of_measurement': '%',
|
||||
'device_class': 'humidity',
|
||||
'value_template': '{{ value_json.humidity }}',
|
||||
'json_attributes_topic': 'zigbee2mqtt/my_device',
|
||||
'state_topic': 'zigbee2mqtt/my_device',
|
||||
'name': 'my_device_humidity',
|
||||
'unique_id': '0x12345678_humidity_zigbee2mqtt',
|
||||
'device': {
|
||||
'identifiers': 'zigbee2mqtt_0x12345678',
|
||||
'name': 'my_device',
|
||||
'sw_version': 'Zigbee2mqtt test',
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
},
|
||||
'availability_topic': 'zigbee2mqtt/bridge/state',
|
||||
};
|
||||
|
||||
assert.deepEqual(JSON.parse(mqtt.publish.getCall(1).args[1]), payload);
|
||||
assert.deepEqual(mqtt.publish.getCall(1).args[2], {retain: true, qos: 0});
|
||||
assert.equal(mqtt.publish.getCall(1).args[3], null);
|
||||
assert.equal(mqtt.publish.getCall(1).args[4], 'homeassistant');
|
||||
|
||||
// 3
|
||||
payload = {
|
||||
'unit_of_measurement': 'hPa',
|
||||
'device_class': 'pressure',
|
||||
'value_template': '{{ value_json.pressure }}',
|
||||
'json_attributes_topic': 'zigbee2mqtt/my_device',
|
||||
'state_topic': 'zigbee2mqtt/my_device',
|
||||
'name': 'my_device_pressure',
|
||||
'unique_id': '0x12345678_pressure_zigbee2mqtt',
|
||||
'device': {
|
||||
'identifiers': 'zigbee2mqtt_0x12345678',
|
||||
'name': 'my_device',
|
||||
'sw_version': 'Zigbee2mqtt test',
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
},
|
||||
'availability_topic': 'zigbee2mqtt/bridge/state',
|
||||
};
|
||||
|
||||
assert.deepEqual(JSON.parse(mqtt.publish.getCall(2).args[1]), payload);
|
||||
assert.deepEqual(mqtt.publish.getCall(2).args[2], {retain: true, qos: 0});
|
||||
assert.equal(mqtt.publish.getCall(2).args[3], null);
|
||||
assert.equal(mqtt.publish.getCall(2).args[4], 'homeassistant');
|
||||
|
||||
// 4
|
||||
payload = {
|
||||
'unit_of_measurement': '%',
|
||||
'device_class': 'battery',
|
||||
'value_template': '{{ value_json.battery }}',
|
||||
'json_attributes_topic': 'zigbee2mqtt/my_device',
|
||||
'state_topic': 'zigbee2mqtt/my_device',
|
||||
'name': 'my_device_battery',
|
||||
'unique_id': '0x12345678_battery_zigbee2mqtt',
|
||||
'device': {
|
||||
'identifiers': 'zigbee2mqtt_0x12345678',
|
||||
'name': 'my_device',
|
||||
'sw_version': 'Zigbee2mqtt test',
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
},
|
||||
'availability_topic': 'zigbee2mqtt/bridge/state',
|
||||
};
|
||||
|
||||
assert.deepEqual(JSON.parse(mqtt.publish.getCall(3).args[1]), payload);
|
||||
assert.deepEqual(mqtt.publish.getCall(3).args[2], {retain: true, qos: 0});
|
||||
assert.equal(mqtt.publish.getCall(3).args[3], null);
|
||||
assert.equal(mqtt.publish.getCall(3).args[4], 'homeassistant');
|
||||
|
||||
// 5
|
||||
payload = {
|
||||
'unit_of_measurement': '-',
|
||||
'value_template': '{{ value_json.linkquality }}',
|
||||
'state_topic': 'zigbee2mqtt/my_device',
|
||||
'json_attributes_topic': 'zigbee2mqtt/my_device',
|
||||
'name': 'my_device_linkquality',
|
||||
'unique_id': '0x12345678_linkquality_zigbee2mqtt',
|
||||
'device': {
|
||||
'identifiers': 'zigbee2mqtt_0x12345678',
|
||||
'name': 'my_device',
|
||||
'sw_version': 'Zigbee2mqtt test',
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
},
|
||||
'availability_topic': 'zigbee2mqtt/bridge/state',
|
||||
};
|
||||
|
||||
assert.deepEqual(JSON.parse(mqtt.publish.getCall(4).args[1]), payload);
|
||||
assert.deepEqual(mqtt.publish.getCall(4).args[2], {retain: true, qos: 0});
|
||||
assert.equal(mqtt.publish.getCall(4).args[3], null);
|
||||
assert.equal(mqtt.publish.getCall(4).args[4], 'homeassistant');
|
||||
});
|
||||
|
||||
it('Should discover devices with precision', () => {
|
||||
let payload = null;
|
||||
sinon.stub(settings, 'getDevice').callsFake(() => {
|
||||
return {
|
||||
friendly_name: 'my_device',
|
||||
humidity_precision: 0,
|
||||
temperature_precision: 1,
|
||||
pressure_precision: 2,
|
||||
};
|
||||
});
|
||||
|
||||
homeassistant.discover('0x12345678', WSDCGQ11LM, false);
|
||||
assert.equal(mqtt.publish.callCount, 5);
|
||||
|
||||
// 1
|
||||
payload = {
|
||||
'unit_of_measurement': '°C',
|
||||
'device_class': 'temperature',
|
||||
'value_template': '{{ (value_json.temperature | float) | round(1) }}',
|
||||
'json_attributes_topic': 'zigbee2mqtt/my_device',
|
||||
'state_topic': 'zigbee2mqtt/my_device',
|
||||
'name': 'my_device_temperature',
|
||||
'unique_id': '0x12345678_temperature_zigbee2mqtt',
|
||||
'device': {
|
||||
'identifiers': 'zigbee2mqtt_0x12345678',
|
||||
'name': 'my_device',
|
||||
'sw_version': 'Zigbee2mqtt test',
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
},
|
||||
'availability_topic': 'zigbee2mqtt/bridge/state',
|
||||
};
|
||||
|
||||
assert.deepEqual(JSON.parse(mqtt.publish.getCall(0).args[1]), payload);
|
||||
assert.deepEqual(mqtt.publish.getCall(0).args[2], {retain: true, qos: 0});
|
||||
assert.equal(mqtt.publish.getCall(0).args[3], null);
|
||||
assert.equal(mqtt.publish.getCall(0).args[4], 'homeassistant');
|
||||
|
||||
// 2
|
||||
payload = {
|
||||
'unit_of_measurement': '%',
|
||||
'device_class': 'humidity',
|
||||
'value_template': '{{ (value_json.humidity | float) | round(0) }}',
|
||||
'json_attributes_topic': 'zigbee2mqtt/my_device',
|
||||
'state_topic': 'zigbee2mqtt/my_device',
|
||||
'name': 'my_device_humidity',
|
||||
'unique_id': '0x12345678_humidity_zigbee2mqtt',
|
||||
'device': {
|
||||
'identifiers': 'zigbee2mqtt_0x12345678',
|
||||
'name': 'my_device',
|
||||
'sw_version': 'Zigbee2mqtt test',
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
},
|
||||
'availability_topic': 'zigbee2mqtt/bridge/state',
|
||||
};
|
||||
|
||||
assert.deepEqual(JSON.parse(mqtt.publish.getCall(1).args[1]), payload);
|
||||
assert.deepEqual(mqtt.publish.getCall(1).args[2], {retain: true, qos: 0});
|
||||
assert.equal(mqtt.publish.getCall(1).args[3], null);
|
||||
assert.equal(mqtt.publish.getCall(1).args[4], 'homeassistant');
|
||||
|
||||
// 3
|
||||
payload = {
|
||||
'unit_of_measurement': 'hPa',
|
||||
'device_class': 'pressure',
|
||||
'value_template': '{{ (value_json.pressure | float) | round(2) }}',
|
||||
'json_attributes_topic': 'zigbee2mqtt/my_device',
|
||||
'state_topic': 'zigbee2mqtt/my_device',
|
||||
'name': 'my_device_pressure',
|
||||
'unique_id': '0x12345678_pressure_zigbee2mqtt',
|
||||
'device': {
|
||||
'identifiers': 'zigbee2mqtt_0x12345678',
|
||||
'name': 'my_device',
|
||||
'sw_version': 'Zigbee2mqtt test',
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
},
|
||||
'availability_topic': 'zigbee2mqtt/bridge/state',
|
||||
};
|
||||
|
||||
assert.deepEqual(JSON.parse(mqtt.publish.getCall(2).args[1]), payload);
|
||||
assert.deepEqual(mqtt.publish.getCall(2).args[2], {retain: true, qos: 0});
|
||||
assert.equal(mqtt.publish.getCall(2).args[3], null);
|
||||
assert.equal(mqtt.publish.getCall(2).args[4], 'homeassistant');
|
||||
|
||||
// 4
|
||||
payload = {
|
||||
'unit_of_measurement': '%',
|
||||
'device_class': 'battery',
|
||||
'value_template': '{{ value_json.battery }}',
|
||||
'json_attributes_topic': 'zigbee2mqtt/my_device',
|
||||
'state_topic': 'zigbee2mqtt/my_device',
|
||||
'name': 'my_device_battery',
|
||||
'unique_id': '0x12345678_battery_zigbee2mqtt',
|
||||
'device': {
|
||||
'identifiers': 'zigbee2mqtt_0x12345678',
|
||||
'name': 'my_device',
|
||||
'sw_version': 'Zigbee2mqtt test',
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
},
|
||||
'availability_topic': 'zigbee2mqtt/bridge/state',
|
||||
};
|
||||
|
||||
assert.deepEqual(JSON.parse(mqtt.publish.getCall(3).args[1]), payload);
|
||||
assert.deepEqual(mqtt.publish.getCall(3).args[2], {retain: true, qos: 0});
|
||||
assert.equal(mqtt.publish.getCall(3).args[3], null);
|
||||
assert.equal(mqtt.publish.getCall(3).args[4], 'homeassistant');
|
||||
|
||||
// 5
|
||||
payload = {
|
||||
'unit_of_measurement': '-',
|
||||
'value_template': '{{ value_json.linkquality }}',
|
||||
'state_topic': 'zigbee2mqtt/my_device',
|
||||
'json_attributes_topic': 'zigbee2mqtt/my_device',
|
||||
'name': 'my_device_linkquality',
|
||||
'unique_id': '0x12345678_linkquality_zigbee2mqtt',
|
||||
'device': {
|
||||
'identifiers': 'zigbee2mqtt_0x12345678',
|
||||
'name': 'my_device',
|
||||
'sw_version': 'Zigbee2mqtt test',
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
},
|
||||
'availability_topic': 'zigbee2mqtt/bridge/state',
|
||||
};
|
||||
|
||||
assert.deepEqual(JSON.parse(mqtt.publish.getCall(4).args[1]), payload);
|
||||
assert.deepEqual(mqtt.publish.getCall(4).args[2], {retain: true, qos: 0});
|
||||
assert.equal(mqtt.publish.getCall(4).args[3], null);
|
||||
assert.equal(mqtt.publish.getCall(4).args[4], 'homeassistant');
|
||||
});
|
||||
|
||||
it('Should discover devices with overriden user configuration', () => {
|
||||
let payload = null;
|
||||
sinon.stub(settings, 'getDevice').callsFake(() => {
|
||||
return {
|
||||
friendly_name: 'my_device',
|
||||
homeassistant: {
|
||||
expire_after: 30,
|
||||
icon: 'mdi:test',
|
||||
temperature: {
|
||||
expire_after: 90,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
homeassistant.discover('0x12345678', WSDCGQ11LM, false);
|
||||
assert.equal(mqtt.publish.callCount, 5);
|
||||
|
||||
// 1
|
||||
payload = {
|
||||
'unit_of_measurement': '°C',
|
||||
'device_class': 'temperature',
|
||||
'value_template': '{{ value_json.temperature }}',
|
||||
'json_attributes_topic': 'zigbee2mqtt/my_device',
|
||||
'state_topic': 'zigbee2mqtt/my_device',
|
||||
'name': 'my_device_temperature',
|
||||
'unique_id': '0x12345678_temperature_zigbee2mqtt',
|
||||
'expire_after': 90,
|
||||
'icon': 'mdi:test',
|
||||
'device': {
|
||||
'identifiers': 'zigbee2mqtt_0x12345678',
|
||||
'name': 'my_device',
|
||||
'sw_version': 'Zigbee2mqtt test',
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
},
|
||||
'availability_topic': 'zigbee2mqtt/bridge/state',
|
||||
};
|
||||
|
||||
assert.deepEqual(JSON.parse(mqtt.publish.getCall(0).args[1]), payload);
|
||||
assert.deepEqual(mqtt.publish.getCall(0).args[2], {retain: true, qos: 0});
|
||||
assert.equal(mqtt.publish.getCall(0).args[3], null);
|
||||
assert.equal(mqtt.publish.getCall(0).args[4], 'homeassistant');
|
||||
|
||||
// 2
|
||||
payload = {
|
||||
'unit_of_measurement': '%',
|
||||
'device_class': 'humidity',
|
||||
'value_template': '{{ value_json.humidity }}',
|
||||
'json_attributes_topic': 'zigbee2mqtt/my_device',
|
||||
'state_topic': 'zigbee2mqtt/my_device',
|
||||
'name': 'my_device_humidity',
|
||||
'unique_id': '0x12345678_humidity_zigbee2mqtt',
|
||||
'expire_after': 30,
|
||||
'icon': 'mdi:test',
|
||||
'device': {
|
||||
'identifiers': 'zigbee2mqtt_0x12345678',
|
||||
'name': 'my_device',
|
||||
'sw_version': 'Zigbee2mqtt test',
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
},
|
||||
'availability_topic': 'zigbee2mqtt/bridge/state',
|
||||
};
|
||||
|
||||
assert.deepEqual(JSON.parse(mqtt.publish.getCall(1).args[1]), payload);
|
||||
assert.deepEqual(mqtt.publish.getCall(1).args[2], {retain: true, qos: 0});
|
||||
assert.equal(mqtt.publish.getCall(1).args[3], null);
|
||||
assert.equal(mqtt.publish.getCall(1).args[4], 'homeassistant');
|
||||
|
||||
// 3
|
||||
payload = {
|
||||
'unit_of_measurement': 'hPa',
|
||||
'device_class': 'pressure',
|
||||
'value_template': '{{ value_json.pressure }}',
|
||||
'json_attributes_topic': 'zigbee2mqtt/my_device',
|
||||
'state_topic': 'zigbee2mqtt/my_device',
|
||||
'name': 'my_device_pressure',
|
||||
'unique_id': '0x12345678_pressure_zigbee2mqtt',
|
||||
'expire_after': 30,
|
||||
'icon': 'mdi:test',
|
||||
'device': {
|
||||
'identifiers': 'zigbee2mqtt_0x12345678',
|
||||
'name': 'my_device',
|
||||
'sw_version': 'Zigbee2mqtt test',
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
},
|
||||
'availability_topic': 'zigbee2mqtt/bridge/state',
|
||||
};
|
||||
|
||||
assert.deepEqual(JSON.parse(mqtt.publish.getCall(2).args[1]), payload);
|
||||
assert.deepEqual(mqtt.publish.getCall(2).args[2], {retain: true, qos: 0});
|
||||
assert.equal(mqtt.publish.getCall(2).args[3], null);
|
||||
assert.equal(mqtt.publish.getCall(2).args[4], 'homeassistant');
|
||||
|
||||
// 4
|
||||
payload = {
|
||||
'unit_of_measurement': '%',
|
||||
'device_class': 'battery',
|
||||
'value_template': '{{ value_json.battery }}',
|
||||
'json_attributes_topic': 'zigbee2mqtt/my_device',
|
||||
'state_topic': 'zigbee2mqtt/my_device',
|
||||
'name': 'my_device_battery',
|
||||
'unique_id': '0x12345678_battery_zigbee2mqtt',
|
||||
'expire_after': 30,
|
||||
'icon': 'mdi:test',
|
||||
'device': {
|
||||
'identifiers': 'zigbee2mqtt_0x12345678',
|
||||
'name': 'my_device',
|
||||
'sw_version': 'Zigbee2mqtt test',
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
},
|
||||
'availability_topic': 'zigbee2mqtt/bridge/state',
|
||||
};
|
||||
|
||||
assert.deepEqual(JSON.parse(mqtt.publish.getCall(3).args[1]), payload);
|
||||
assert.deepEqual(mqtt.publish.getCall(3).args[2], {retain: true, qos: 0});
|
||||
assert.equal(mqtt.publish.getCall(3).args[3], null);
|
||||
assert.equal(mqtt.publish.getCall(3).args[4], 'homeassistant');
|
||||
|
||||
// 5
|
||||
payload = {
|
||||
'unit_of_measurement': '-',
|
||||
'value_template': '{{ value_json.linkquality }}',
|
||||
'state_topic': 'zigbee2mqtt/my_device',
|
||||
'json_attributes_topic': 'zigbee2mqtt/my_device',
|
||||
'name': 'my_device_linkquality',
|
||||
'unique_id': '0x12345678_linkquality_zigbee2mqtt',
|
||||
'expire_after': 30,
|
||||
'icon': 'mdi:test',
|
||||
'device': {
|
||||
'identifiers': 'zigbee2mqtt_0x12345678',
|
||||
'name': 'my_device',
|
||||
'sw_version': 'Zigbee2mqtt test',
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
},
|
||||
'availability_topic': 'zigbee2mqtt/bridge/state',
|
||||
};
|
||||
|
||||
assert.deepEqual(JSON.parse(mqtt.publish.getCall(4).args[1]), payload);
|
||||
assert.deepEqual(mqtt.publish.getCall(4).args[2], {retain: true, qos: 0});
|
||||
assert.equal(mqtt.publish.getCall(4).args[3], null);
|
||||
assert.equal(mqtt.publish.getCall(4).args[4], 'homeassistant');
|
||||
});
|
||||
|
||||
it('Should discover devices with cover_position', () => {
|
||||
let payload = null;
|
||||
sinon.stub(settings, 'getDevice').callsFake(() => {
|
||||
return {friendly_name: 'my_device'};
|
||||
});
|
||||
|
||||
homeassistant.discover('0x12345678', SV01, false);
|
||||
assert.equal(mqtt.publish.callCount, 5);
|
||||
|
||||
// 1
|
||||
payload = {
|
||||
name: 'my_device_cover',
|
||||
command_topic: 'zigbee2mqtt/my_device/set',
|
||||
position_topic: 'zigbee2mqtt/my_device',
|
||||
set_position_topic: 'zigbee2mqtt/my_device/set',
|
||||
set_position_template: '{ "position": {{ position }} }',
|
||||
value_template: '{{ value_json.position }}',
|
||||
unique_id: '0x12345678_cover_zigbee2mqtt',
|
||||
device: {
|
||||
'identifiers': 'zigbee2mqtt_0x12345678',
|
||||
'name': 'my_device',
|
||||
'sw_version': 'Zigbee2mqtt test',
|
||||
'model': 'Smart vent (SV01)',
|
||||
'manufacturer': 'Keen Home',
|
||||
},
|
||||
availability_topic: 'zigbee2mqtt/bridge/state',
|
||||
};
|
||||
|
||||
assert.deepEqual(JSON.parse(mqtt.publish.getCall(0).args[1]), payload);
|
||||
assert.deepEqual(mqtt.publish.getCall(0).args[2], {retain: true, qos: 0});
|
||||
assert.equal(mqtt.publish.getCall(0).args[3], null);
|
||||
assert.equal(mqtt.publish.getCall(0).args[4], 'homeassistant');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,286 @@
|
||||
const assert = require('chai').assert;
|
||||
const sinon = require('sinon');
|
||||
const data = require('../lib/util/data');
|
||||
const tmp = require('tmp');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const yaml = require('js-yaml');
|
||||
const rimraf = require('rimraf');
|
||||
|
||||
describe('Settings', () => {
|
||||
let dir = null;
|
||||
|
||||
const write = (file, json) => {
|
||||
fs.writeFileSync(file, yaml.safeDump(json));
|
||||
};
|
||||
|
||||
const read = (file) => {
|
||||
return yaml.safeLoad(fs.readFileSync(file, 'utf8'));
|
||||
};
|
||||
|
||||
const setup = (configuration) => {
|
||||
const configurationFile = path.join(dir.name, 'configuration.yaml');
|
||||
write(configurationFile, configuration);
|
||||
delete require.cache[require.resolve('../lib/util/settings.js')];
|
||||
const settings = require('../lib/util/settings.js');
|
||||
return {configurationFile, settings};
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
dir = tmp.dirSync();
|
||||
sinon.stub(data, 'joinPath').callsFake((file) => {
|
||||
return path.join(dir.name, file);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rimraf.sync(dir.name);
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
describe('Settings', () => {
|
||||
it('Should return default settings', () => {
|
||||
const {settings} = setup({});
|
||||
const s = settings.get();
|
||||
assert.deepEqual(s, settings._getDefaults());
|
||||
});
|
||||
|
||||
it('Should return settings', () => {
|
||||
const {settings} = setup({permit_join: true});
|
||||
const s = settings.get();
|
||||
const expected = settings._getDefaults();
|
||||
expected.permit_join = true;
|
||||
assert.deepEqual(s, expected);
|
||||
});
|
||||
|
||||
it('Should add devices', () => {
|
||||
const {settings, configurationFile} = setup({});
|
||||
settings.addDevice('0x12345678');
|
||||
|
||||
const actual = read(configurationFile);
|
||||
const expected = {
|
||||
devices: {
|
||||
'0x12345678': {
|
||||
friendly_name: '0x12345678',
|
||||
retain: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('Should read devices', () => {
|
||||
const content = {
|
||||
devices: {
|
||||
'0x12345678': {
|
||||
friendly_name: '0x12345678',
|
||||
retain: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const {settings} = setup(content);
|
||||
|
||||
const device = settings.getDevice('0x12345678');
|
||||
const expected = {
|
||||
friendly_name: '0x12345678',
|
||||
retain: false,
|
||||
};
|
||||
|
||||
assert.deepEqual(device, expected);
|
||||
});
|
||||
|
||||
it('Should read devices form a seperate file', () => {
|
||||
const contentConfiguration = {
|
||||
devices: 'devices.yaml',
|
||||
};
|
||||
|
||||
const contentDevices = {
|
||||
'0x12345678': {
|
||||
friendly_name: '0x12345678',
|
||||
retain: false,
|
||||
},
|
||||
};
|
||||
|
||||
write(path.join(dir.name, 'devices.yaml'), contentDevices);
|
||||
const {settings} = setup(contentConfiguration);
|
||||
|
||||
const device = settings.getDevice('0x12345678');
|
||||
const expected = {
|
||||
friendly_name: '0x12345678',
|
||||
retain: false,
|
||||
};
|
||||
|
||||
assert.deepEqual(device, expected);
|
||||
});
|
||||
|
||||
it('Should add devices to a seperate file', () => {
|
||||
const contentConfiguration = {
|
||||
devices: 'devices.yaml',
|
||||
};
|
||||
|
||||
const contentDevices = {
|
||||
'0x12345678': {
|
||||
friendly_name: '0x12345678',
|
||||
retain: false,
|
||||
},
|
||||
};
|
||||
|
||||
const devicesFile = path.join(dir.name, 'devices.yaml');
|
||||
write(devicesFile, contentDevices);
|
||||
const {settings, configurationFile} = setup(contentConfiguration);
|
||||
|
||||
settings.addDevice('0x1234');
|
||||
|
||||
assert.deepEqual(read(configurationFile), {devices: 'devices.yaml'});
|
||||
|
||||
const expected = {
|
||||
'0x12345678': {
|
||||
friendly_name: '0x12345678',
|
||||
retain: false,
|
||||
},
|
||||
'0x1234': {
|
||||
friendly_name: '0x1234',
|
||||
retain: false,
|
||||
},
|
||||
};
|
||||
|
||||
assert.deepEqual(read(devicesFile), expected);
|
||||
});
|
||||
|
||||
it('Should add devices to a seperate file if devices.yaml doesnt exist', () => {
|
||||
const contentConfiguration = {
|
||||
devices: 'devices.yaml',
|
||||
};
|
||||
|
||||
const devicesFile = path.join(dir.name, 'devices.yaml');
|
||||
const {settings, configurationFile} = setup(contentConfiguration);
|
||||
|
||||
settings.addDevice('0x1234');
|
||||
|
||||
assert.deepEqual(read(configurationFile), {devices: 'devices.yaml'});
|
||||
|
||||
const expected = {
|
||||
'0x1234': {
|
||||
friendly_name: '0x1234',
|
||||
retain: false,
|
||||
},
|
||||
};
|
||||
|
||||
assert.deepEqual(read(devicesFile), expected);
|
||||
});
|
||||
|
||||
it('Should add and remove devices to a seperate file if devices.yaml doesnt exist', () => {
|
||||
const contentConfiguration = {
|
||||
devices: 'devices.yaml',
|
||||
};
|
||||
|
||||
const devicesFile = path.join(dir.name, 'devices.yaml');
|
||||
const {settings, configurationFile} = setup(contentConfiguration);
|
||||
|
||||
settings.addDevice('0x1234');
|
||||
assert.deepEqual(read(configurationFile), {devices: 'devices.yaml'});
|
||||
|
||||
settings.removeDevice('0x1234');
|
||||
assert.deepEqual(read(configurationFile), {devices: 'devices.yaml'});
|
||||
|
||||
assert.deepEqual(read(devicesFile), {});
|
||||
});
|
||||
|
||||
it('Should read groups', () => {
|
||||
const content = {
|
||||
groups: {
|
||||
'1': {
|
||||
friendly_name: '123',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const {settings} = setup(content);
|
||||
|
||||
const group = settings.getGroup('1');
|
||||
const expected = {
|
||||
friendly_name: '123',
|
||||
};
|
||||
|
||||
assert.deepEqual(group, expected);
|
||||
});
|
||||
|
||||
it('Should read groups form a seperate file', () => {
|
||||
const contentConfiguration = {
|
||||
groups: 'groups.yaml',
|
||||
};
|
||||
|
||||
const contentGroups = {
|
||||
'1': {
|
||||
friendly_name: '123',
|
||||
},
|
||||
};
|
||||
|
||||
write(path.join(dir.name, 'groups.yaml'), contentGroups);
|
||||
const {settings} = setup(contentConfiguration);
|
||||
|
||||
const group = settings.getGroup('1');
|
||||
const expected = {
|
||||
friendly_name: '123',
|
||||
};
|
||||
|
||||
assert.deepEqual(group, expected);
|
||||
});
|
||||
|
||||
it('Combine everything! groups and devices from separte file :)', () => {
|
||||
const contentConfiguration = {
|
||||
devices: 'devices.yaml',
|
||||
groups: 'groups.yaml',
|
||||
};
|
||||
|
||||
const contentGroups = {
|
||||
'1': {
|
||||
friendly_name: '123',
|
||||
},
|
||||
};
|
||||
|
||||
write(path.join(dir.name, 'groups.yaml'), contentGroups);
|
||||
const {settings, configurationFile} = setup(contentConfiguration);
|
||||
|
||||
const devicesFile = path.join(dir.name, 'devices.yaml');
|
||||
|
||||
const expectedConfiguration = {
|
||||
devices: 'devices.yaml',
|
||||
groups: 'groups.yaml',
|
||||
};
|
||||
|
||||
assert.deepEqual(read(configurationFile), expectedConfiguration);
|
||||
|
||||
settings.addDevice('0x1234');
|
||||
|
||||
assert.deepEqual(read(configurationFile), expectedConfiguration);
|
||||
|
||||
const expectedDevice = {
|
||||
'0x1234': {
|
||||
friendly_name: '0x1234',
|
||||
retain: false,
|
||||
},
|
||||
};
|
||||
|
||||
assert.deepEqual(read(devicesFile), expectedDevice);
|
||||
|
||||
const group = settings.getGroup('1');
|
||||
const expectedGroup = {
|
||||
friendly_name: '123',
|
||||
};
|
||||
|
||||
assert.deepEqual(group, expectedGroup);
|
||||
|
||||
assert.deepEqual(read(configurationFile), expectedConfiguration);
|
||||
|
||||
const expectedDevice2 = {
|
||||
friendly_name: '0x1234',
|
||||
retain: false,
|
||||
};
|
||||
|
||||
assert.deepEqual(settings.getDevice('0x1234'), expectedDevice2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
const logger = require('../lib/util/logger');
|
||||
|
||||
module.exports = {
|
||||
stubLogger: (sandbox) => {
|
||||
sandbox.stub(logger, 'info').callsFake(() => {});
|
||||
sandbox.stub(logger, 'warn').callsFake(() => {});
|
||||
sandbox.stub(logger, 'debug').callsFake(() => {});
|
||||
sandbox.stub(logger, 'error').callsFake(() => {});
|
||||
},
|
||||
zigbeeMessage: (device, cid, type, data, epId) => {
|
||||
return {data: {cid, data}, type, endpoints: [{device, epId}]};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
echo "Stopping zigbee2mqtt..."
|
||||
sudo systemctl stop zigbee2mqtt
|
||||
|
||||
echo "Creating backup of configuration..."
|
||||
cp -R data data-backup
|
||||
|
||||
echo "Updating..."
|
||||
git checkout HEAD -- npm-shrinkwrap.json
|
||||
git pull
|
||||
|
||||
echo "Installing dependencies..."
|
||||
rm -rf node_modules
|
||||
npm install
|
||||
|
||||
echo "Restore configuration..."
|
||||
cp -R data-backup/* data
|
||||
rm -rf data-backup
|
||||
|
||||
echo "Starting zigbee2mqtt..."
|
||||
sudo systemctl start zigbee2mqtt
|
||||
|
||||
echo "Done!"
|
||||
Reference in New Issue
Block a user