mirror of
https://github.com/mtan93/homebridge.git
synced 2026-03-08 05:31:55 +00:00
Compare commits
666 Commits
provider-a
...
v0.4.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3e08414f6 | ||
|
|
ea9df45d2d | ||
|
|
7425f8beca | ||
|
|
dbb7b89cf9 | ||
|
|
4f000485db | ||
|
|
c0884f484e | ||
|
|
363c997058 | ||
|
|
0ccd80c8e7 | ||
|
|
ef8fe5ced2 | ||
|
|
4a531ede5c | ||
|
|
ff35ece65c | ||
|
|
66ea6e640d | ||
|
|
ecd06d7239 | ||
|
|
c89ff97ac5 | ||
|
|
ceec4c74fd | ||
|
|
925c1630c4 | ||
|
|
4eabc4ad52 | ||
|
|
c0859a29d3 | ||
|
|
c15707e875 | ||
|
|
8c476b45a0 | ||
|
|
f49229d73c | ||
|
|
fbccc031f4 | ||
|
|
d70fa741d8 | ||
|
|
4740bf1fc5 | ||
|
|
da57b29972 | ||
|
|
5944365bc6 | ||
|
|
a8908fd9b8 | ||
|
|
8ef7e62094 | ||
|
|
15c8eaaf29 | ||
|
|
e6648375c7 | ||
|
|
a52bc9e437 | ||
|
|
b78c081cd4 | ||
|
|
87050a2267 | ||
|
|
35dfaabc69 | ||
|
|
77ce39e157 | ||
|
|
0af8a43dc9 | ||
|
|
f203a2ac6f | ||
|
|
620c8473b8 | ||
|
|
b2f476f833 | ||
|
|
c6d2f889fc | ||
|
|
2ea2052769 | ||
|
|
64e8c83d9c | ||
|
|
13333999f3 | ||
|
|
87c48d7267 | ||
|
|
9b42fafdaf | ||
|
|
842ec105be | ||
|
|
df8508a38f | ||
|
|
9d7c1de9dd | ||
|
|
195255bf0d | ||
|
|
6b182fc4e7 | ||
|
|
c7b2500518 | ||
|
|
1f1030766a | ||
|
|
8cb22efb83 | ||
|
|
f6df85695d | ||
|
|
32e776203f | ||
|
|
c3c2f8815d | ||
|
|
6500912f54 | ||
|
|
2e2c8eb207 | ||
|
|
012005ddc7 | ||
|
|
27ffd6e944 | ||
|
|
0b28387cb1 | ||
|
|
815ea7abea | ||
|
|
cf80e4f2da | ||
|
|
40266af8b2 | ||
|
|
57beabf0b4 | ||
|
|
d3c77a4cda | ||
|
|
287aa6d034 | ||
|
|
1da98bb19f | ||
|
|
aea485c33e | ||
|
|
386cf34827 | ||
|
|
8e360491cf | ||
|
|
e546440575 | ||
|
|
902fdded65 | ||
|
|
8de375a4b0 | ||
|
|
d04c20ca2f | ||
|
|
c53f890a8c | ||
|
|
c02e212b4c | ||
|
|
7436be9b44 | ||
|
|
2ad7932fbc | ||
|
|
7dd8e12791 | ||
|
|
c93b0b0df1 | ||
|
|
b49fd2d6a5 | ||
|
|
9c8812da70 | ||
|
|
9e6bf028ba | ||
|
|
aebd152ff9 | ||
|
|
5b9c5192fe | ||
|
|
e1334c5196 | ||
|
|
1a356a1783 | ||
|
|
da8d5fd267 | ||
|
|
798275774a | ||
|
|
74e1cf257a | ||
|
|
f4aa57314b | ||
|
|
66505457c7 | ||
|
|
ae24e28ac4 | ||
|
|
c8c84c08f2 | ||
|
|
2213997acc | ||
|
|
3616d61fed | ||
|
|
e52aff802b | ||
|
|
15bf22805c | ||
|
|
f5b790ff35 | ||
|
|
08f8ae53c5 | ||
|
|
2f0c60984a | ||
|
|
507835986b | ||
|
|
9567c41a58 | ||
|
|
ec839b2dbc | ||
|
|
2d73e70670 | ||
|
|
65b74253df | ||
|
|
78b8c265e1 | ||
|
|
affe3fde63 | ||
|
|
e3a5c5a96a | ||
|
|
b3dbe1165c | ||
|
|
0b9e931fe1 | ||
|
|
50efd29b6d | ||
|
|
6d2cb11eae | ||
|
|
7bfc5ee84d | ||
|
|
8f1a043121 | ||
|
|
a5de3f820f | ||
|
|
8e795f48cf | ||
|
|
1c4bf23b60 | ||
|
|
988c6ef6d0 | ||
|
|
52aa6bba2c | ||
|
|
f2ad339a13 | ||
|
|
9883eb7189 | ||
|
|
974eef3ea8 | ||
|
|
752a5b7854 | ||
|
|
15357a5fe8 | ||
|
|
e3178e5fa4 | ||
|
|
2470f26451 | ||
|
|
52d51c0fcf | ||
|
|
3e7d2aedcc | ||
|
|
54cff454dd | ||
|
|
46c4b6f589 | ||
|
|
abc6ccfae7 | ||
|
|
eda8e91d0f | ||
|
|
269a8cf43a | ||
|
|
2df1ebed21 | ||
|
|
36ed2ab0ad | ||
|
|
193810691b | ||
|
|
7db78a50b5 | ||
|
|
eabf45526d | ||
|
|
52d3a9a96e | ||
|
|
a682e1dee5 | ||
|
|
5440694243 | ||
|
|
6011ad3d2e | ||
|
|
15cc98217b | ||
|
|
108602a6b0 | ||
|
|
98d8158d04 | ||
|
|
a62137477a | ||
|
|
386eac8094 | ||
|
|
9dcebb7320 | ||
|
|
91044f0d92 | ||
|
|
82c62f78ed | ||
|
|
bc1c879412 | ||
|
|
c1cc8be8fa | ||
|
|
bccdbd1755 | ||
|
|
9f42689256 | ||
|
|
73b61fd730 | ||
|
|
90feacf53f | ||
|
|
127741a737 | ||
|
|
5c92157009 | ||
|
|
70f2dc6230 | ||
|
|
1dfa190b18 | ||
|
|
7623bf4204 | ||
|
|
6b36ec9dc4 | ||
|
|
b277b76c39 | ||
|
|
3b19613528 | ||
|
|
48bec8a1bc | ||
|
|
cfed5d7a66 | ||
|
|
75417fb322 | ||
|
|
04f48ecbab | ||
|
|
ec015e58a3 | ||
|
|
bee6d03d80 | ||
|
|
255a064c08 | ||
|
|
727809e9b4 | ||
|
|
ea1f75abb0 | ||
|
|
ead491fb4c | ||
|
|
7362b48df7 | ||
|
|
5f82fa6fb6 | ||
|
|
fd49b96d78 | ||
|
|
cbc34897f9 | ||
|
|
98c61bc72a | ||
|
|
fe4c63fd75 | ||
|
|
2e6a487f80 | ||
|
|
5cf54d7ff6 | ||
|
|
94c5bd6de0 | ||
|
|
e944d3ed2a | ||
|
|
3d6ddfc520 | ||
|
|
528c83bd1a | ||
|
|
9740a50520 | ||
|
|
af53950080 | ||
|
|
909900d025 | ||
|
|
06c7356aef | ||
|
|
c591cf6899 | ||
|
|
890f6acff0 | ||
|
|
debbc14fef | ||
|
|
5e9c3a3b62 | ||
|
|
1f8e6e5c8b | ||
|
|
35ecb9e170 | ||
|
|
b65acecb28 | ||
|
|
e78a68352f | ||
|
|
b21c37712d | ||
|
|
65d732415e | ||
|
|
dc6faa9784 | ||
|
|
3da52388e8 | ||
|
|
ecdffbef23 | ||
|
|
76136589f4 | ||
|
|
b4acdf3865 | ||
|
|
9a798855f5 | ||
|
|
6e156e48fb | ||
|
|
d2fd510aa4 | ||
|
|
45af486cfd | ||
|
|
d866f9abb6 | ||
|
|
e72309fab0 | ||
|
|
1ad323b0e8 | ||
|
|
1f7db5e661 | ||
|
|
e2157aed9c | ||
|
|
48e0f8b812 | ||
|
|
b09835600d | ||
|
|
afef06467d | ||
|
|
386636975b | ||
|
|
bfc151e70a | ||
|
|
8d7f986f84 | ||
|
|
997906bab8 | ||
|
|
de06a2b12d | ||
|
|
1779ddd754 | ||
|
|
d443286dcb | ||
|
|
74f0f00321 | ||
|
|
1a95978e0a | ||
|
|
13e8846138 | ||
|
|
b5210f424f | ||
|
|
7c7ac5d828 | ||
|
|
70ec9a8891 | ||
|
|
00574f60b4 | ||
|
|
2c36f1745d | ||
|
|
8ebd6ecdd2 | ||
|
|
e9e637c43d | ||
|
|
d7bca45e70 | ||
|
|
e8553c7203 | ||
|
|
626871680c | ||
|
|
42f88864f9 | ||
|
|
0cb57d2a44 | ||
|
|
92667b5666 | ||
|
|
7b24d142b4 | ||
|
|
27b39cbfa0 | ||
|
|
74e37cc488 | ||
|
|
8fcd5d39a6 | ||
|
|
6604c2b69a | ||
|
|
a3c0df1c7c | ||
|
|
6e5c35ec88 | ||
|
|
e30b504583 | ||
|
|
7b110191f9 | ||
|
|
74a72c1096 | ||
|
|
5066b108d2 | ||
|
|
6008374b9e | ||
|
|
3c53b319cb | ||
|
|
2d80e5f838 | ||
|
|
b2aa70d40a | ||
|
|
a45779cf48 | ||
|
|
bf125078a1 | ||
|
|
7ce190a7d0 | ||
|
|
915e674583 | ||
|
|
ad62bd4b15 | ||
|
|
c897913005 | ||
|
|
15e9219c80 | ||
|
|
38cb94c012 | ||
|
|
ffc4edbc2a | ||
|
|
196cda8063 | ||
|
|
3d03824e6a | ||
|
|
385908fa89 | ||
|
|
f2d22584fc | ||
|
|
97cb18389f | ||
|
|
896d6b40d9 | ||
|
|
c5499c122e | ||
|
|
831480d035 | ||
|
|
286f700622 | ||
|
|
ecf9afee64 | ||
|
|
bb1c193bc0 | ||
|
|
c22c14584d | ||
|
|
c6b248e0ea | ||
|
|
2df47b9893 | ||
|
|
23f190e3d8 | ||
|
|
7233c5bf74 | ||
|
|
aad811fe6e | ||
|
|
bab1eb730e | ||
|
|
6fa69c5c4b | ||
|
|
9ad8a111c6 | ||
|
|
e40f10b82e | ||
|
|
d5e4714551 | ||
|
|
81dbb285c7 | ||
|
|
99910960ae | ||
|
|
a056b16c35 | ||
|
|
a05ba696b6 | ||
|
|
60c48fc4cb | ||
|
|
d8a3596326 | ||
|
|
7c7ceb6453 | ||
|
|
fdbd33f29d | ||
|
|
c48c750917 | ||
|
|
d1e186ed99 | ||
|
|
1f8db79324 | ||
|
|
d4279c5ef5 | ||
|
|
e7ce658c3d | ||
|
|
ff5762ad70 | ||
|
|
c59b87d17d | ||
|
|
41e6c20a61 | ||
|
|
c506c44d60 | ||
|
|
6030928c12 | ||
|
|
40a96808b2 | ||
|
|
59692df1a0 | ||
|
|
ac1da10ea8 | ||
|
|
55a17f7919 | ||
|
|
c6ed47cf38 | ||
|
|
a435cf2b00 | ||
|
|
e4fa276de2 | ||
|
|
a274ae4eda | ||
|
|
05e811cdd6 | ||
|
|
25299a7863 | ||
|
|
4a831422eb | ||
|
|
b4f4f58519 | ||
|
|
4fbd7eb775 | ||
|
|
c0dfc9a8cd | ||
|
|
d5c4dfce1f | ||
|
|
fd420c7795 | ||
|
|
8fe5f8b2aa | ||
|
|
d8a21133e9 | ||
|
|
490ed5db40 | ||
|
|
9096e64891 | ||
|
|
772f35efac | ||
|
|
d914b334a9 | ||
|
|
2b84fc1783 | ||
|
|
64864bac85 | ||
|
|
ed8a3c8062 | ||
|
|
a15c026f2f | ||
|
|
d66ab79e8e | ||
|
|
af79ea4fbf | ||
|
|
e6afda55d6 | ||
|
|
b9fb57dd7a | ||
|
|
feca9fbf0d | ||
|
|
b66610ce93 | ||
|
|
c24a94c072 | ||
|
|
2652f33a0a | ||
|
|
2eb82bcc98 | ||
|
|
ac62a44ce2 | ||
|
|
a55b166131 | ||
|
|
d3f0c73393 | ||
|
|
c36ad9d631 | ||
|
|
3031ba7e0f | ||
|
|
fd72307384 | ||
|
|
d04d417344 | ||
|
|
98519c84dd | ||
|
|
3bca9f70f5 | ||
|
|
1bb8d00884 | ||
|
|
45f40874ef | ||
|
|
fe5db08081 | ||
|
|
1fce5c8754 | ||
|
|
8e23024ef0 | ||
|
|
98ee9f8f95 | ||
|
|
bd979ea1f8 | ||
|
|
54f0c2f0cb | ||
|
|
ba5722c517 | ||
|
|
2d88dafa11 | ||
|
|
84cac442e2 | ||
|
|
99da61d30a | ||
|
|
0b3930e458 | ||
|
|
ce35600b34 | ||
|
|
7aa758cb04 | ||
|
|
bd6b8478a9 | ||
|
|
2e24e98227 | ||
|
|
817f7ecf20 | ||
|
|
2a66eac058 | ||
|
|
b54d37fb2d | ||
|
|
c6c45d9e3d | ||
|
|
78394bc95d | ||
|
|
bfbaa74375 | ||
|
|
bb3381e10a | ||
|
|
5323b0afe1 | ||
|
|
ca232f3111 | ||
|
|
c9166f6ff0 | ||
|
|
afcb86ef0e | ||
|
|
2a63a9ff2d | ||
|
|
1b69cffeb2 | ||
|
|
7695eced0d | ||
|
|
625b9f47df | ||
|
|
c3ad5f26da | ||
|
|
67fb6e8b4d | ||
|
|
b24a611fab | ||
|
|
80b2a047d4 | ||
|
|
48f613f8ae | ||
|
|
84c51aa27a | ||
|
|
eec663a5c8 | ||
|
|
387e7ec9ce | ||
|
|
6b0d701570 | ||
|
|
656a8057ac | ||
|
|
24fbe4158d | ||
|
|
1dcfe4a1fb | ||
|
|
32877abc98 | ||
|
|
91f6ccb2d4 | ||
|
|
93ea0deded | ||
|
|
f802d84509 | ||
|
|
24a8dcb8cc | ||
|
|
76a5fd3b7f | ||
|
|
c7ab475dd4 | ||
|
|
6f77e55c7b | ||
|
|
e7f35dfcbb | ||
|
|
ddf9fa01cf | ||
|
|
73148b060d | ||
|
|
5720e82abd | ||
|
|
6f14108905 | ||
|
|
6fe4fe8249 | ||
|
|
a05a4b6f71 | ||
|
|
2135e7eccb | ||
|
|
dbb130be02 | ||
|
|
72b9175b78 | ||
|
|
65ec517fd8 | ||
|
|
aa823baa8e | ||
|
|
118a4529b9 | ||
|
|
9580d259db | ||
|
|
ace364644e | ||
|
|
0da4fe5d22 | ||
|
|
cd33f2e6c8 | ||
|
|
68f764864d | ||
|
|
2c118e9649 | ||
|
|
66439f6353 | ||
|
|
773eb8fd0e | ||
|
|
651cdfa786 | ||
|
|
0f89a6ae36 | ||
|
|
ec8b556618 | ||
|
|
eb6c881d28 | ||
|
|
7f753f79f6 | ||
|
|
f3e08b0a15 | ||
|
|
e06e982063 | ||
|
|
f9465ebc8e | ||
|
|
2a430ad75c | ||
|
|
8ceaccfca4 | ||
|
|
d6b3fc7667 | ||
|
|
abd848fd31 | ||
|
|
a041407104 | ||
|
|
d36827f92d | ||
|
|
983f136271 | ||
|
|
13d1ed75cf | ||
|
|
030fec8cbf | ||
|
|
be589d4fb5 | ||
|
|
1df32fca3d | ||
|
|
e894b1a1a1 | ||
|
|
f4160c2d01 | ||
|
|
9f39b49dd5 | ||
|
|
f658ca90c1 | ||
|
|
7f14df0434 | ||
|
|
c88d01c9a9 | ||
|
|
b0e3829dfb | ||
|
|
adbe116a5a | ||
|
|
bb39f5f73e | ||
|
|
db3f32c577 | ||
|
|
13347d1879 | ||
|
|
4feec29d5c | ||
|
|
0df809e395 | ||
|
|
293f56e826 | ||
|
|
c1e3d45fa1 | ||
|
|
69d948e0fa | ||
|
|
488456c108 | ||
|
|
025bca7a43 | ||
|
|
544124fbab | ||
|
|
178430d25c | ||
|
|
167a983068 | ||
|
|
b39b33726d | ||
|
|
a6d61cc93a | ||
|
|
ec550d1638 | ||
|
|
f5cc6cf6fb | ||
|
|
b1d0ef57ac | ||
|
|
80f73f8324 | ||
|
|
45ae56cf12 | ||
|
|
1f4ae9e875 | ||
|
|
6c34301c13 | ||
|
|
428186eb86 | ||
|
|
5358ef94a8 | ||
|
|
ca941bd350 | ||
|
|
b5c2ba2c14 | ||
|
|
197311534b | ||
|
|
a246472401 | ||
|
|
7e9793e20e | ||
|
|
2b1aa5e296 | ||
|
|
17573524ce | ||
|
|
be31cce972 | ||
|
|
7d5a992c98 | ||
|
|
3da6fcb510 | ||
|
|
17fc8f1829 | ||
|
|
e15fc7d4e8 | ||
|
|
4ca372f9d9 | ||
|
|
fdadd068b8 | ||
|
|
7e6df6191e | ||
|
|
3c35311c4a | ||
|
|
b2ea770afe | ||
|
|
a1343ed57e | ||
|
|
09f5e2bed0 | ||
|
|
4b16371522 | ||
|
|
7866b582f9 | ||
|
|
c73e22984d | ||
|
|
e9cca92dee | ||
|
|
d6e31b4aa7 | ||
|
|
e75726436e | ||
|
|
62cabc23f3 | ||
|
|
4170b8a533 | ||
|
|
7dc168e9dc | ||
|
|
18333242ff | ||
|
|
5cccd3f916 | ||
|
|
abe88b7502 | ||
|
|
cd8ee6a7b9 | ||
|
|
1df411d916 | ||
|
|
b05ee83e9f | ||
|
|
2710412ca6 | ||
|
|
a4c3f73eb5 | ||
|
|
78987a775f | ||
|
|
08fa203027 | ||
|
|
6614705d6c | ||
|
|
5ba7b46c26 | ||
|
|
7c6d6ba0ac | ||
|
|
1f44801f45 | ||
|
|
9d8afc4bcb | ||
|
|
09516acaf3 | ||
|
|
86d548b8d9 | ||
|
|
dd0aa6e2c8 | ||
|
|
6f5e6b6a0b | ||
|
|
2c1d9f6efb | ||
|
|
70b5a9142a | ||
|
|
94ef18c94d | ||
|
|
3cc1f381a3 | ||
|
|
6c6b5bf85f | ||
|
|
816728c0cf | ||
|
|
e2ef8fc0b6 | ||
|
|
085ae0a22e | ||
|
|
64635833d6 | ||
|
|
ea1c1f6fce | ||
|
|
fe4cd285d0 | ||
|
|
03417e249a | ||
|
|
116dd1b315 | ||
|
|
1a98a6c9ac | ||
|
|
bad0ba0c3b | ||
|
|
a677edb2cf | ||
|
|
a3cbf5a380 | ||
|
|
86e17a3922 | ||
|
|
2a5c4c76fe | ||
|
|
30a705e79f | ||
|
|
01d2c21aac | ||
|
|
dcc224f898 | ||
|
|
47f000ecff | ||
|
|
f287c9ee36 | ||
|
|
b56d9346c8 | ||
|
|
d067711974 | ||
|
|
2c50d76cb2 | ||
|
|
5bc1109c19 | ||
|
|
bf2216209d | ||
|
|
2ca6410e0a | ||
|
|
76eaca8f78 | ||
|
|
2bab8bad8b | ||
|
|
5ecfb25314 | ||
|
|
b170e81059 | ||
|
|
4c088d235c | ||
|
|
9c8a6ee90f | ||
|
|
d7d80b8618 | ||
|
|
1050b4e550 | ||
|
|
13d983aa28 | ||
|
|
7d5caae96d | ||
|
|
f81e594ff0 | ||
|
|
11f71e076c | ||
|
|
b3675f4be3 | ||
|
|
7e84806846 | ||
|
|
77fbe5b53c | ||
|
|
f72cb43043 | ||
|
|
9d7a6768b8 | ||
|
|
c6877193cc | ||
|
|
5782ff997f | ||
|
|
ea9801fa77 | ||
|
|
0dd76134ee | ||
|
|
76e7ef2677 | ||
|
|
dfdbc865c8 | ||
|
|
d8e27910cc | ||
|
|
d2547f7ae6 | ||
|
|
9ebf41415c | ||
|
|
7b873bdf48 | ||
|
|
64c665cebb | ||
|
|
5f1df2792f | ||
|
|
b45318c3a3 | ||
|
|
84fe474ccf | ||
|
|
5df7ffb40e | ||
|
|
9c63ce5017 | ||
|
|
8a58328ad7 | ||
|
|
bf3d746089 | ||
|
|
84c08d360c | ||
|
|
867aec36d8 | ||
|
|
9f0aaa53d6 | ||
|
|
c98e7a1d54 | ||
|
|
8bda79bcb5 | ||
|
|
4d9897ff26 | ||
|
|
e1647d22ad | ||
|
|
3a819e202d | ||
|
|
2c94d72afe | ||
|
|
0a2f2865fb | ||
|
|
e9d43a7bdd | ||
|
|
a5aa5b6b90 | ||
|
|
50f8c6a3c9 | ||
|
|
9d8c5f309f | ||
|
|
667ad7a3aa | ||
|
|
11560d3d42 | ||
|
|
6fff108ccd | ||
|
|
7cb835e4ea | ||
|
|
43c0346436 | ||
|
|
9653a3173d | ||
|
|
c9aea1b731 | ||
|
|
f7f953b8d1 | ||
|
|
c756ea9456 | ||
|
|
b1e6536a95 | ||
|
|
f71730cff6 | ||
|
|
2ad9643bee | ||
|
|
6e61287f59 | ||
|
|
86322c30c8 | ||
|
|
d9d55c34ad | ||
|
|
2e3a26c32f | ||
|
|
40e264a79d | ||
|
|
e0de3f2a82 | ||
|
|
939f79b2b0 | ||
|
|
ab71dc352f | ||
|
|
a29cabcd91 | ||
|
|
4dcc26bf50 | ||
|
|
431199a388 | ||
|
|
a268583803 | ||
|
|
1099cb50e7 | ||
|
|
f050e51518 | ||
|
|
b5f778051e | ||
|
|
ed6d4baba2 | ||
|
|
76ad483a5f | ||
|
|
0fb6fc736e | ||
|
|
d4c544e793 | ||
|
|
18cc6cbdc0 | ||
|
|
e8769d688a | ||
|
|
275a88d358 | ||
|
|
443f77196a | ||
|
|
2e689eb264 | ||
|
|
da79d67106 | ||
|
|
385b615b81 | ||
|
|
f7a2cd5f0c | ||
|
|
8b81001647 | ||
|
|
9325b15437 | ||
|
|
0b457f9b7f | ||
|
|
0605c5ff86 | ||
|
|
7b01e6c1d0 | ||
|
|
34110039f5 | ||
|
|
38f3d9b82e | ||
|
|
511b1873f7 | ||
|
|
e76269fcdc | ||
|
|
c4f9e81828 | ||
|
|
db9a1bd441 | ||
|
|
42ba55e836 | ||
|
|
91602e4f99 | ||
|
|
ca311ae0a8 | ||
|
|
1cd697fb8b | ||
|
|
6cb9008424 | ||
|
|
681cd7d9eb | ||
|
|
4aafdaea9b | ||
|
|
81cc689b7e | ||
|
|
a47c8dd4fd | ||
|
|
02c90cfc51 | ||
|
|
03293409dd | ||
|
|
26e833013c | ||
|
|
f3b472b2d0 | ||
|
|
54a24b4fdd | ||
|
|
16829a9e4c | ||
|
|
7b7b28571d |
10
.gitignore
vendored
10
.gitignore
vendored
@@ -7,10 +7,6 @@ node_modules/
|
||||
npm-debug.log
|
||||
.node-version
|
||||
|
||||
# Intellij
|
||||
.idea/
|
||||
*.iml
|
||||
|
||||
# HomeBridge
|
||||
config.json
|
||||
persist/
|
||||
# Ignore any extra plugins in the example directory that aren't in Git already
|
||||
# (this is a sandbox for the user)
|
||||
example-plugins
|
||||
|
||||
0
.gitmodules
vendored
0
.gitmodules
vendored
191
README.md
191
README.md
@@ -1,114 +1,161 @@
|
||||
|
||||
# HomeBridge
|
||||
[](https://homebridge-slackin.herokuapp.com)
|
||||
|
||||
HomeBridge is a lightweight NodeJS server you can run on your home network that emulates the iOS HomeKit API. It includes a set of "shims" (found in the [accessories](accessories/) and [platforms](platforms/) folders) that provide a basic bridge from HomeKit to various 3rd-party APIs provided by manufacturers of "smart home" devices.
|
||||
# Homebridge
|
||||
|
||||
Since Siri supports devices added through HomeKit, this means that with HomeBridge you can ask Siri to control devices that don't have any support for HomeKit at all. For instance, using the included shims, you can say things like:
|
||||

|
||||
|
||||
* _Siri, unlock the front door._ ([Lockitron](https://lockitron.com))
|
||||
* _Siri, open the garage door._ ([LiftMaster MyQ](https://www.myliftmaster.com))
|
||||
* _Siri, turn on the Leaf._ ([Carwings](http://www.nissanusa.com/innovations/carwings.article.html))
|
||||
* _Siri, turn off the Speakers._ ([Sonos](http://www.sonos.com))
|
||||
* _Siri, turn on the Dehumidifier._ ([WeMo](http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/))
|
||||
* _Siri, turn on Away Mode._ ([Xfinity Home](http://www.comcast.com/home-security.html))
|
||||
* _Siri, turn on the living room lights._ ([Wink](http://www.wink.com), [SmartThings](http://www.smartthings.com), [X10](http://github.com/edc1591/rest-mochad), [Philips Hue](http://meethue.com))
|
||||
Homebridge is a lightweight NodeJS server you can run on your home network that emulates the iOS HomeKit API. It supports Plugins, which are community-contributed modules that provide a basic bridge from HomeKit to various 3rd-party APIs provided by manufacturers of "smart home" devices.
|
||||
|
||||
If you would like to support any other devices, please write a shim and create a pull request and I'd be happy to add it to this official list.
|
||||
Since Siri supports devices added through HomeKit, this means that with Homebridge you can ask Siri to control devices that don't have any support for HomeKit at all. For instance, using just some of the available plugins, you can say:
|
||||
|
||||
# Shim types
|
||||
There are 2 types of shims supported in HomeBridge.
|
||||
* _Siri, unlock the back door._ [pictured above]
|
||||
* _Siri, open the garage door._
|
||||
* _Siri, turn on the coffee maker._
|
||||
* _Siri, turn on the living room lights._
|
||||
* _Siri, good morning!_
|
||||
|
||||
* Accessory - Individual device
|
||||
* Platform - A full bridge to another system
|
||||
You can explore all available plugins at the NPM website by [searching for the keyword `homebridge-plugin`](https://www.npmjs.com/search?q=homebridge-plugin).
|
||||
|
||||
## Accessories
|
||||
# Community
|
||||
|
||||
Accessories are individual devices you would like to bridge to HomeKit. You set them up by declaring them individually in your `config.json` file. Generally, you specify them by `name` or `id` and which system they use.
|
||||
If you're having an issue with a particular plugin, open an issue in that plugin's Github repository. If you're having an issue with Homebridge itself, feel free to open issues and PRs here.
|
||||
|
||||
## Platforms
|
||||
You can also chat with us in our nascent [Slack instance](http://homebridge-slackin.herokuapp.com).
|
||||
|
||||
Platforms bridge entire systems to HomeKit. Platforms can be things like Wink or SmartThings or Vera. By adding a platform to your `config.json`, HomeBridge will automatically detect all of your devices for you.
|
||||
# Installation
|
||||
|
||||
All you have to do is add the right config options so HomeBridge can authenticate and communicate with your other system, and voila, your devices will be available to HomeKit via HomeBridge.
|
||||
**Note:** If you're running on Linux, you'll need to make sure you have the `libavahi-compat-libdnssd-dev` package installed. If you're running on a Raspberry Pi, you should have a look at the [Wiki](https://github.com/nfarina/homebridge/wiki/Running-HomeBridge-on-a-Raspberry-Pi).
|
||||
|
||||
# Why?
|
||||
Homebridge is published through [NPM](https://www.npmjs.com/package/homebridge) and should be installed "globally" by typing:
|
||||
|
||||
Technically, the device manufacturers should be the ones implementing the HomeKit API. And I'm sure they will - eventually. When they do, these shims will be obsolete, and I hope that happens soon. In the meantime, this server is a fun way to get a taste of the future, for those who just can't bear to wait until "real" HomeKit devices are on the market.
|
||||
sudo npm install -g --unsafe-perm homebridge
|
||||
|
||||
# Credit
|
||||
You may need to use the `--unsafe-perm` flag if you receive an error similar to this:
|
||||
|
||||
HomeBridge itself is basically just a set of shims and a README. The actual HomeKit API work was done by [KhaosT](http://twitter.com/khaost) in his [HAP-NodeJS](https://github.com/KhaosT/HAP-NodeJS) project. Additionally, many of the shims benefit from amazing NodeJS projects out there like `sonos` and `wemo` that implement all the interesting functionality.
|
||||
gyp WARN EACCES user "root" does not have permission to access the dev dir "/root/.node-gyp/5.5.0"
|
||||
|
||||
# Before you Begin
|
||||
Now you should be able to run Homebridge:
|
||||
|
||||
I would call this project a "novelty" in its current form, and is for **intrepid hackers only**. To make any of this work, you'll need:
|
||||
$ homebridge
|
||||
No plugins found. See the README for information on installing plugins.
|
||||
|
||||
* An app on your iOS device that can manage your HomeKit database.
|
||||
* An always-running server (like a Raspberry Pi) on which you can install NodeJS.
|
||||
* Knowledge of Git submodules and npm.
|
||||
Homebridge will complain if you don't have any Plugins installed, since it will essentially be useless, although you can still "pair" with it. See the next section "Installing Plugins" for more info.
|
||||
|
||||
You'll also need some patience, as Siri can be very strict about sentence structure, and occasionally she will forget about HomeKit altogether. But it's not surprising that HomeKit isn't rock solid, since almost no one can actually use it today besides developers who are creating hardware accessories for it. There are, to my knowledge, exactly zero licensed HomeKit devices on the market right now, so Apple can easily get away with this all being a work in progress.
|
||||
Once you've installed a Plugin or two, you can run Homebridge again:
|
||||
|
||||
Additionally, the shims I've created implement the bare minimum of HomeKit needed to provide basic functionality like turning things off and on. I haven't written any kind of good feedback or error handling, and although they support changing state, they don't support reading the current state, so if you ask questions like "Is my door unlocked?" Siri will respond with the default of "Nope!" no matter what.
|
||||
|
||||
# Getting Started
|
||||
|
||||
OK, if you're still excited enough about ordering Siri to make your coffee (which, who wouldn't be!) then here's how to set things up. First, clone this repo:
|
||||
|
||||
$ git clone https://github.com/nfarina/homebridge.git
|
||||
$ cd homebridge
|
||||
$ npm install
|
||||
|
||||
**Node**: You'll need to have NodeJS version 0.12.x or better installed for required submodule `HAP-NodeJS` to load.
|
||||
|
||||
Now you should be able to run the homebridge server:
|
||||
|
||||
$ cd homebridge
|
||||
$ npm run start
|
||||
Starting HomeBridge server...
|
||||
$ homebridge
|
||||
Couldn't find a config.json file [snip]
|
||||
|
||||
The server won't do anything until you've created a `config.json` file containing your home devices (or _accessories_ in HomeKit parlance) or platforms you wish to make available to iOS. You can start by copying and modifying the included `config-sample.json` file which includes declarations for all supported accessories and platforms.
|
||||
However, Homebridge won't do anything until you've created a `config.json` file containing your accessories and/or platforms. You can start by copying and modifying the included `config-sample.json` file which includes declarations for some example accessories and platforms. Each Plugin will have its own expected configuration; the documentation for Plugins should give you some real-world examples for that plugin.
|
||||
|
||||
Once you've added your devices and/or platforms, you should be able to run the server again and see them initialize:
|
||||
**NOTE**: Your `config.json` file MUST be inside of `.homebridge`, which is inside of your home folder. On MacOS and Linux, the full path for your `config.json` would be `~/.homebridge/config.json`. Any error messages will contain the exact path where your config is expected to be found.
|
||||
|
||||
$ npm run start
|
||||
Starting HomeBridge server...
|
||||
Loading 6 accessories...
|
||||
[Speakers] Initializing 'Sonos' accessory...
|
||||
[Coffee Maker] Initializing 'WeMo' accessory...
|
||||
[Speakers] Initializing 'Sonos' accessory...
|
||||
[Coffee Maker] Initializing 'WeMo' accessory...
|
||||
[Wink] Initializing Wink platform...
|
||||
[Wink] Fetching Wink devices.
|
||||
[Wink] Initializing device with name Living Room Lamp...
|
||||
**REALLY IMPORTANT**: You must use a "plain text" editor to create or modify `config.json`. Do NOT use apps like TextEdit on Mac or Wordpad on Windows. Apps like these will corrupt the formatting of the file in hard-to-debug ways, making improper `"` signs is an example. I suggest using the free [Atom text editor](http://atom.io).
|
||||
|
||||
Your server is now ready to receive commands from iOS.
|
||||
Once you've added your config file, you should be able to run Homebridge again:
|
||||
|
||||
# Adding your devices to iOS
|
||||
$ homebridge
|
||||
Loaded plugin: homebridge-lockitron
|
||||
Registering accessory 'Lockitron'
|
||||
---
|
||||
Loaded config.json with 1 accessories and 0 platforms.
|
||||
---
|
||||
Loading 0 platforms...
|
||||
Loading 1 accessories...
|
||||
[Back Door] Initializing Lockitron accessory...
|
||||
|
||||
HomeKit is actually not an app; it's a "database" similar to HealthKit and PassKit. But where HealthKit has the companion _Health_ app and PassKit has _Passbook_, Apple has supplied no app for managing your HomeKit database (at least [not yet](http://9to5mac.com/2015/05/20/apples-planned-ios-9-home-app-uses-virtual-rooms-to-manage-homekit-accessories/)). However, the HomeKit API is open for developers to write their own apps for adding devices to HomeKit.
|
||||
Homebridge is now ready to receive commands from iOS.
|
||||
|
||||
Fortunately, there are now a few apps in the App Store that can manage your HomeKit devices. Try [Insteon+](https://itunes.apple.com/US/app/id919270334?mt=8) or [Lutron](https://itunes.apple.com/us/app/lutron-app-for-caseta-wireless/id886753021?mt=8) or a number of others.
|
||||
# Installing Plugins
|
||||
|
||||
## Adding HomeKit Accessories
|
||||
Plugins are NodeJS modules published through NPM and tagged with the keyword `homebridge-plugin`. They must have a name with the prefix `homebridge-`, like **homebridge-mysmartlock**.
|
||||
|
||||
Once you've gotten a HomeKit app running on your iOS device, you can begin adding accessories. The app should "discover" the accessories defined in your `config.json` file, assuming that you're still running the HomeBridge server and you're on the same Wifi network.
|
||||
Plugins can publish Accessories and/or Platforms. Accessories are individual devices, like a smart switch or a garage door. Platforms act like a single device but can expose a set of devices, like a house full of smart lightbulbs.
|
||||
|
||||
When you attempt to add a device, it will ask for a "PIN code". The default code for _all_ HomeBridge accessories is `031-45-154`. Adding the device should create some files in the `persist` directory of the HomeBridge server, which stores the pairing relationship.
|
||||
You install Plugins the same way you installed Homebridge - as a global NPM module. For example:
|
||||
|
||||
sudo npm install -g homebridge-lockitron
|
||||
|
||||
You can explore all available plugins at the NPM website by [searching for the keyword `homebridge-plugin`](https://www.npmjs.com/search?q=homebridge-plugin).
|
||||
|
||||
**IMPORTANT**: Many of the plugins that Homebridge used to include with its default installation have been moved to the single plugin [homebridge-legacy-plugins](https://www.npmjs.com/package/homebridge-legacy-plugins).
|
||||
|
||||
# Adding Homebridge to iOS
|
||||
|
||||
HomeKit itself is actually not an app; it's a "database" similar to HealthKit and PassKit. Where HealthKit has the companion _Health_ app and PassKit has _Passbook_, HomeKit has the _Home_ app, introduced with iOS 10.
|
||||
|
||||
If you are a member of the iOS developer program, you might also find Apple's [HomeKit Catalog](https://developer.apple.com/library/ios/samplecode/HomeKitCatalog/Introduction/Intro.html) app to be useful, as it provides straightforward and comprehensive management of all HomeKit database "objects".
|
||||
|
||||
Using the Home app (or most other HomeKit apps), you should be able to add the single accessory "Homebridge", assuming that you're still running Homebridge and you're on the same Wifi network. Adding this accessory will automatically add all accessories and platforms defined in `config.json`.
|
||||
|
||||
When you attempt to add Homebridge, it will ask for a "PIN code". The default code is `031-45-154` (but this can be changed, see `config-sample.json`).
|
||||
|
||||
# Interacting with your Devices
|
||||
|
||||
Once your device has been added to HomeKit, you should be able to tell Siri to control your devices. However, realize that Siri is a cloud service, and iOS may need some time to synchronize your device information with iCloud.
|
||||
|
||||
Also, keep in mind HomeKit is not very robust yet, and it is common for it to fail intermittently ("Sorry, I wasn't able to control your devices" etc.) then start working again for no reason. Also I've noticed that it will get cranky and stop working altogether sometimes. The usual voodoo applies here: reboot your device, restart the homebridge server, run your HomeKit iOS app and poke around, etc.
|
||||
One final thing to remember is that Siri will almost always prefer its default phrase handling over HomeKit devices. For instance, if you name your Sonos device "Radio" and try saying "Siri, turn on the Radio" then Siri will probably start playing an iTunes Radio station on your phone. Even if you name it "Sonos" and say "Siri, turn on the Sonos", Siri will probably just launch the Sonos app instead. This is why, for instance, the suggested `name` for the Sonos accessory is "Speakers".
|
||||
|
||||
One final thing to remember is that Siri will almost always prefer its default phrase handling over HomeKit devices. For instance, if you name your Sonos device "Radio" and try saying "Siri, turn on the Radio" then Siri will probably start playing an iTunes Radio station on your phone. Even if you name it "Sonos" and say "Siri, turn on the Sonos", Siri will probably just launch the Sonos app instead. This is why, for instance, the suggested `name` for the Sonos shim in `config-samples.json` is "Speakers".
|
||||
# Writing Plugins
|
||||
|
||||
# Final Notes
|
||||
We don't have a lot of documentation right now for creating plugins, but there are many existing plugins you can study.
|
||||
|
||||
HomeKit is definitely amazing when it works. Speaking to Siri is often much quicker and easier than launching whatever app your device manufacturer provides.
|
||||
The best place to start is the included [Example Plugins](https://github.com/nfarina/homebridge/tree/master/example-plugins). Right now this contains a single plugin that registers a platform that offers fake light accessories. This will show you how to use the Homebridge Plugin API.
|
||||
|
||||
For more example on how to construct HomeKit Services and Characteristics, see the many Accessories in the [Legacy Plugins](https://github.com/nfarina/homebridge-legacy-plugins/tree/master/accessories) repository.
|
||||
|
||||
You can also view the [full list of supported HomeKit Services and Characteristics in the HAP-NodeJS protocol repository](https://github.com/KhaosT/HAP-NodeJS/blob/master/lib/gen/HomeKitTypes.js).
|
||||
|
||||
And you can find an example plugin that publishes an individual accessory at [here](https://github.com/nfarina/homebridge/tree/6500912f54a70ff479e63e2b72760ab589fa558a/example-plugins/homebridge-lockitron).
|
||||
|
||||
See more examples on how to create Platform classes in the [Legacy Plugins](https://github.com/nfarina/homebridge-legacy-plugins/tree/master/platforms) repository.
|
||||
|
||||
# Plugin Development
|
||||
|
||||
When writing your plugin, you'll want Homebridge to load it from your development directory instead of publishing it to `npm` each time. You can tell Homebridge to look for your plugin at a specific location using the command-line parameter `-P`. For example, if you are in the Homebridge directory (as checked out from Github), you might type:
|
||||
|
||||
```sh
|
||||
DEBUG=* ./bin/homebridge -D -P ../my-great-plugin/
|
||||
```
|
||||
|
||||
This will start up Homebridge and load your in-development plugin from a nearby directory. Note that you can also direct Homebridge to load your configuration from somewhere besides the default `~/.homebridge`, for example:
|
||||
|
||||
```sh
|
||||
DEBUG=* ./bin/homebridge -D -U ~/.homebridge-dev -P ../my-great-plugin/
|
||||
```
|
||||
|
||||
This is very useful when you are already using your development machine to host a "real" Homebridge instance (with all your accessories) that you don't want to disturb.
|
||||
|
||||
# Common Issues
|
||||
|
||||
### My iOS App Can't Find Homebridge
|
||||
|
||||
Two reasons why Homebridge may not be discoverable:
|
||||
|
||||
1. Homebridge server thinks it's been paired with, but iOS thinks otherwise. Fix: deleted `persist/` directory which is next to your `config.json`.
|
||||
|
||||
2. iOS device has gotten your Homebridge `username` (looks like a MAC address) "stuck" somehow, where it's in the database but inactive. Fix: change your `username` in the "bridge" section of `config.json` to be some new value.
|
||||
|
||||
### Errors on startup
|
||||
|
||||
The following errors are experienced when starting Homebridge and can be safely ignored. The cost of removing the issue at the core of the errors isn't worth the effort.
|
||||
|
||||
```
|
||||
*** WARNING *** The program 'nodejs' uses the Apple Bonjour compatibility layer of Avahi
|
||||
*** WARNING *** Please fix your application to use the native API of Avahi!
|
||||
*** WARNING *** For more information see http://0pointerde/avahi-compat?s=libdns_sd&e=nodejs
|
||||
*** WARNING *** The program 'nodejs' called 'DNSServiceRegister()' which is not supported (or only supported partially) in the Apple Bonjour compatibility layer of Avahi
|
||||
*** WARNING *** Please fix your application to use the native API of Avahi!
|
||||
*** WARNING *** For more information see http://0pointerde/avahi-compat?s=libdns_sd&e=nodejs&f=DNSServiceRegister
|
||||
```
|
||||
|
||||
# Why Homebridge?
|
||||
|
||||
Technically, the device manufacturers should be the ones implementing the HomeKit API. And I'm sure they will - eventually. When they do, this project will be obsolete, and I hope that happens soon. In the meantime, Homebridge is a fun way to get a taste of the future, for those who just can't bear to wait until "real" HomeKit devices are on the market.
|
||||
|
||||
# Credit
|
||||
|
||||
The original HomeKit API work was done by [KhaosT](http://twitter.com/khaost) in his [HAP-NodeJS](https://github.com/KhaosT/HAP-NodeJS) project.
|
||||
|
||||
I welcome any suggestions or pull requests, but keep in mind that it's likely not possible to support all the things you might want to do with a device through HomeKit. For instance, you might want to hack the Sonos shim to play the specific kind of music you want and that's great, but it might not be appropriate to merge those specific changes into this repository. The shims here should be mostly simple "canonical examples" and easily hackable by others.
|
||||
|
||||
Good luck!
|
||||
|
||||
@@ -1,245 +0,0 @@
|
||||
var types = require("HAP-NodeJS/accessories/types.js");
|
||||
var AD2USB = require('ad2usb');
|
||||
var CUSTOM_PANEL_LCD_TEXT_CTYPE = "A3E7B8F9-216E-42C1-A21C-97D4E3BE52C8";
|
||||
|
||||
function AD2USBAccessory(log, config) {
|
||||
|
||||
this.log = log;
|
||||
this.name = config["name"];
|
||||
this.host = config["host"];
|
||||
this.port = config["port"];
|
||||
this.pin = config["pin"];
|
||||
var that = this;
|
||||
this.currentArmState = 2;
|
||||
this.currentStateCharacteristic = undefined;
|
||||
this.targetStateCharacteristic = undefined;
|
||||
this.lcdCharacteristic = undefined;
|
||||
|
||||
var alarm = AD2USB.connect(this.host, this.port, function() {
|
||||
|
||||
// Send an initial empty character to get status
|
||||
alarm.send('');
|
||||
|
||||
// Armed Away
|
||||
alarm.on('armedAway', function() {
|
||||
|
||||
that.log("Armed to AWAY");
|
||||
if (that.currentStateCharacteristic) {
|
||||
that.currentStateCharacteristic.updateValue(0, null);
|
||||
}
|
||||
if (that.targetStateCharacteristic) {
|
||||
that.targetStateCharacteristic.updateValue(1, null);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// Armed Stay
|
||||
alarm.on('armedStay', function() {
|
||||
|
||||
that.log("Armed to STAY");
|
||||
if (that.currentStateCharacteristic) {
|
||||
that.currentStateCharacteristic.updateValue(0, null);
|
||||
}
|
||||
if (that.targetStateCharacteristic) {
|
||||
that.targetStateCharacteristic.updateValue(0, null);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// Armed Night
|
||||
alarm.on('armedNight', function() {
|
||||
|
||||
that.log("Armed to NIGHT");
|
||||
if (that.currentStateCharacteristic) {
|
||||
that.currentStateCharacteristic.updateValue(0, null);
|
||||
}
|
||||
if (that.targetStateCharacteristic) {
|
||||
that.targetStateCharacteristic.updateValue(2, null);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// Disarmed
|
||||
alarm.on('disarmed', function() {
|
||||
|
||||
that.log("Disarmed");
|
||||
if (that.currentStateCharacteristic) {
|
||||
that.currentStateCharacteristic.updateValue(1, null);
|
||||
}
|
||||
if (that.targetStateCharacteristic) {
|
||||
that.targetStateCharacteristic.updateValue(3, null);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// Text Change
|
||||
alarm.on('lcdtext', function(newText) {
|
||||
|
||||
that.log("LCD: " + newText);
|
||||
if (that.lcdCharacteristic) {
|
||||
that.lcdCharacteristic.updateValue(newText, null);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
this.alarm = alarm;
|
||||
|
||||
}
|
||||
|
||||
AD2USBAccessory.prototype = {
|
||||
|
||||
setArmState: function(targetArmState) {
|
||||
|
||||
var that = this;
|
||||
that.log("Desired target arm state: " + targetArmState);
|
||||
|
||||
// TARGET
|
||||
// 0 - Stay
|
||||
// 1 - Away
|
||||
// 2 - Night
|
||||
// 3 - Disarm
|
||||
if (targetArmState == 0) {
|
||||
that.alarm.armStay(that.pin);
|
||||
}
|
||||
else if (targetArmState == 1) {
|
||||
that.alarm.armAway(that.pin);
|
||||
}
|
||||
else if (targetArmState == 2) {
|
||||
that.alarm.armNight(that.pin);
|
||||
}
|
||||
else if (targetArmState == 3) {
|
||||
that.alarm.disarm(that.pin);
|
||||
}
|
||||
|
||||
|
||||
// CURRENT
|
||||
// 0 - Armed
|
||||
// 1 - Disarmed
|
||||
// 2 - Hold
|
||||
|
||||
},
|
||||
|
||||
getServices: function() {
|
||||
var that = this;
|
||||
return [{
|
||||
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||
characteristics: [{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: this.name,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of the accessory",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MANUFACTURER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "Nutech",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Manufacturer",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MODEL_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "AD2USB",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Model",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.SERIAL_NUMBER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "AD2USBIF",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "SN",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.IDENTIFY_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pw"],
|
||||
format: "bool",
|
||||
initialValue: false,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Identify Accessory",
|
||||
designedMaxLength: 1
|
||||
}]
|
||||
},{
|
||||
sType: types.ALARM_STYPE,
|
||||
characteristics: [{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: this.name,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of service",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.ALARM_CURRENT_STATE_CTYPE,
|
||||
onUpdate: null,
|
||||
onRegister: function(characteristic) {
|
||||
|
||||
that.currentStateCharacteristic = characteristic;
|
||||
characteristic.eventEnabled = true;
|
||||
|
||||
},
|
||||
perms: ["pr","ev"],
|
||||
format: "int",
|
||||
initialValue: 2,
|
||||
supportEvents: true,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Alarm current arm state",
|
||||
designedMaxLength: 1
|
||||
},{
|
||||
cType: types.ALARM_TARGET_STATE_CTYPE,
|
||||
onUpdate: function(value) { that.setArmState(value); },
|
||||
onRegister: function(characteristic) {
|
||||
|
||||
that.targetStateCharacteristic = characteristic;
|
||||
characteristic.eventEnabled = true;
|
||||
|
||||
},
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "int",
|
||||
initialValue: 1,
|
||||
supportEvents: true,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Alarm target arm state",
|
||||
designedMaxLength: 1
|
||||
},
|
||||
{
|
||||
cType: CUSTOM_PANEL_LCD_TEXT_CTYPE,
|
||||
onUpdate: null,
|
||||
onRegister: function(characteristic) {
|
||||
|
||||
that.lcdCharacteristic = characteristic;
|
||||
characteristic.eventEnabled = true;
|
||||
|
||||
},
|
||||
perms: ["pr","ev"],
|
||||
format: "string",
|
||||
initialValue: "Unknown",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Keypad Text",
|
||||
designedMaxLength: 64
|
||||
}]
|
||||
}];
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.accessory = AD2USBAccessory;
|
||||
@@ -1,126 +0,0 @@
|
||||
var types = require("HAP-NodeJS/accessories/types.js");
|
||||
var carwings = require("carwingsjs");
|
||||
|
||||
function CarwingsAccessory(log, config) {
|
||||
this.log = log;
|
||||
this.name = config["name"];
|
||||
this.username = config["username"];
|
||||
this.password = config["password"];
|
||||
}
|
||||
|
||||
CarwingsAccessory.prototype = {
|
||||
|
||||
setPowerState: function(powerOn) {
|
||||
var that = this;
|
||||
|
||||
carwings.login(this.username, this.password, function(err, result) {
|
||||
if (!err) {
|
||||
that.vin = result.vin;
|
||||
that.log("Got VIN: " + that.vin);
|
||||
|
||||
if (powerOn) {
|
||||
carwings.startClimateControl(that.vin, null, function(err, result) {
|
||||
if (!err)
|
||||
that.log("Started climate control.");
|
||||
else
|
||||
that.log("Error starting climate control: " + err);
|
||||
});
|
||||
}
|
||||
else {
|
||||
carwings.stopClimateControl(that.vin, function(err, result) {
|
||||
if (!err)
|
||||
that.log("Stopped climate control.");
|
||||
else
|
||||
that.log("Error stopping climate control: " + err);
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
that.log("Error logging in: " + err);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getServices: function() {
|
||||
var that = this;
|
||||
return [{
|
||||
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||
characteristics: [{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: this.name,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of the accessory",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MANUFACTURER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "Nissan",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Manufacturer",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MODEL_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "Rev-1",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Model",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.SERIAL_NUMBER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "A1S2NASF88EW",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "SN",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.IDENTIFY_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pw"],
|
||||
format: "bool",
|
||||
initialValue: false,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Identify Accessory",
|
||||
designedMaxLength: 1
|
||||
}]
|
||||
},{
|
||||
sType: types.SWITCH_STYPE,
|
||||
characteristics: [{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: this.name,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of service",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.POWER_STATE_CTYPE,
|
||||
onUpdate: function(value) { that.setPowerState(value); },
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "bool",
|
||||
initialValue: false,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Change the power state of the car",
|
||||
designedMaxLength: 1
|
||||
}]
|
||||
}];
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.accessory = CarwingsAccessory;
|
||||
@@ -1,126 +0,0 @@
|
||||
var types = require("HAP-NodeJS/accessories/types.js");
|
||||
var elkington = require("elkington");
|
||||
|
||||
function ElkM1Accessory(log, config) {
|
||||
this.log = log;
|
||||
this.name = config["name"];
|
||||
this.zone = config["zone"];
|
||||
this.host = config["host"];
|
||||
this.port = config["port"];
|
||||
this.pin = config["pin"];
|
||||
this.arm = config["arm"];
|
||||
}
|
||||
|
||||
ElkM1Accessory.prototype = {
|
||||
setPowerState: function(alarmOn) {
|
||||
var that = this;
|
||||
|
||||
if (!alarmOn)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var elk = elkington.createConnection({
|
||||
port: that.port,
|
||||
host: that.host,
|
||||
});
|
||||
|
||||
switch (that.arm)
|
||||
{
|
||||
case 'Away':
|
||||
elk.armAway({area: that.zone, code: that.pin});
|
||||
break;
|
||||
case 'Stay':
|
||||
elk.armStay({area: that.zone, code: that.pin});
|
||||
break;
|
||||
case 'Night':
|
||||
elk.armNightInstant({area: that.zone, code: that.pin});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
getServices: function() {
|
||||
var that = this;
|
||||
return [{
|
||||
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||
characteristics: [{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: this.name,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of the accessory",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MANUFACTURER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "Elk",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Manufacturer",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MODEL_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "M1",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Model",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.SERIAL_NUMBER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "A1S2NASF88EW",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "SN",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.IDENTIFY_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pw"],
|
||||
format: "bool",
|
||||
initialValue: false,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Identify Accessory",
|
||||
designedMaxLength: 1
|
||||
}]
|
||||
},{
|
||||
sType: types.SWITCH_STYPE,
|
||||
characteristics: [{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: this.name,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of service",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.POWER_STATE_CTYPE,
|
||||
onUpdate: function(value) { that.setPowerState(value); },
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "bool",
|
||||
initialValue: false,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Alarm the Zone",
|
||||
designedMaxLength: 1
|
||||
}]
|
||||
}];
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.accessory = ElkM1Accessory;
|
||||
@@ -1,116 +0,0 @@
|
||||
var types = require("HAP-NodeJS/accessories/types.js");
|
||||
var request = require("request");
|
||||
|
||||
function HomeMatic(log, config) {
|
||||
this.log = log;
|
||||
this.name = config["name"];
|
||||
this.ccuID = config["ccu_id"];
|
||||
this.ccuIP = config["ccu_ip"];
|
||||
}
|
||||
|
||||
HomeMatic.prototype = {
|
||||
|
||||
setPowerState: function(powerOn) {
|
||||
|
||||
var binaryState = powerOn ? 1 : 0;
|
||||
var that = this;
|
||||
|
||||
this.log("Setting power state of CCU to " + powerOn);
|
||||
this.log(this.ccuID+ powerOn);
|
||||
|
||||
request.put({
|
||||
url: "http://"+this.ccuIP+"/config/xmlapi/statechange.cgi?ise_id="+this.ccuID+"&new_value="+ powerOn,
|
||||
}, function(err, response, body) {
|
||||
|
||||
if (!err && response.statusCode == 200) {
|
||||
that.log("State change complete.");
|
||||
}
|
||||
else {
|
||||
that.log("Error '"+err+"' setting lock state: " + body);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getServices: function() {
|
||||
var that = this;
|
||||
return [{
|
||||
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||
characteristics: [{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: this.name,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of the accessory",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MANUFACTURER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "WeMo",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Manufacturer",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MODEL_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "Rev-1",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Model",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.SERIAL_NUMBER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "A1S2NASF88EW",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "SN",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.IDENTIFY_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pw"],
|
||||
format: "bool",
|
||||
initialValue: false,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Identify Accessory",
|
||||
designedMaxLength: 1
|
||||
}]
|
||||
},{
|
||||
sType: types.SWITCH_STYPE,
|
||||
characteristics: [{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: this.name,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of service",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.POWER_STATE_CTYPE,
|
||||
onUpdate: function(value) { that.setPowerState(value); },
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "bool",
|
||||
initialValue: false,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Change the power state of a Variable",
|
||||
designedMaxLength: 1
|
||||
}]
|
||||
}];
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.accessory = HomeMatic;
|
||||
@@ -1,160 +0,0 @@
|
||||
var types = require("HAP-NodeJS/accessories/types.js");
|
||||
var request = require("request");
|
||||
|
||||
function HttpAccessory(log, config) {
|
||||
this.log = log;
|
||||
|
||||
// url info
|
||||
this.on_url = config["on_url"];
|
||||
this.off_url = config["off_url"];
|
||||
this.brightness_url = config["brightness_url"];
|
||||
this.http_method = config["http_method"];
|
||||
|
||||
// device info
|
||||
this.name = config["name"];
|
||||
}
|
||||
|
||||
HttpAccessory.prototype = {
|
||||
|
||||
httpRequest: function(url, method, callback) {
|
||||
request({
|
||||
url: url,
|
||||
method: method
|
||||
},
|
||||
function (error, response, body) {
|
||||
callback(error, response, body)
|
||||
})
|
||||
},
|
||||
|
||||
setPowerState: function(powerOn) {
|
||||
var url;
|
||||
|
||||
if (powerOn) {
|
||||
url = this.on_url
|
||||
this.log("Setting power state on the '"+this.name+"' to on");
|
||||
}else{
|
||||
url = this.off_url
|
||||
this.log("Setting power state on the '"+this.name+"' to off");
|
||||
}
|
||||
|
||||
this.httpRequest(url, this.http_method, function(error, response, body){
|
||||
if (error) {
|
||||
return console.error('http power function failed:', error);
|
||||
}else{
|
||||
return console.log('http power function succeeded!');
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
setBrightness: function(level) {
|
||||
var url = this.brightness_url.replace("%b", level)
|
||||
|
||||
this.log("Setting brightness on the '"+this.name+"' to " + level);
|
||||
|
||||
this.httpRequest(url, this.http_method, function(error, response, body){
|
||||
if (error) {
|
||||
return console.error('http brightness function failed:', error);
|
||||
}else{
|
||||
return console.log('http brightness function succeeded!');
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
getServices: function() {
|
||||
var that = this;
|
||||
return [{
|
||||
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||
characteristics: [{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: this.name,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of the accessory",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MANUFACTURER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "Http",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Manufacturer",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MODEL_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "Rev-1",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Model",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.SERIAL_NUMBER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "A1S2NASF88EW",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "SN",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.IDENTIFY_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pw"],
|
||||
format: "bool",
|
||||
initialValue: false,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Identify Accessory",
|
||||
designedMaxLength: 1
|
||||
}]
|
||||
},{
|
||||
sType: types.LIGHTBULB_STYPE,
|
||||
characteristics: [{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: this.name,
|
||||
supportEvents: true,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of service",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.POWER_STATE_CTYPE,
|
||||
onUpdate: function(value) { that.setPowerState(value); },
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "bool",
|
||||
initialValue: 0,
|
||||
supportEvents: true,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Change the power state",
|
||||
designedMaxLength: 1
|
||||
},{
|
||||
cType: types.BRIGHTNESS_CTYPE,
|
||||
onUpdate: function(value) { that.setBrightness(value); },
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "int",
|
||||
initialValue: 0,
|
||||
supportEvents: true,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Adjust Brightness",
|
||||
designedMinValue: 0,
|
||||
designedMaxValue: 100,
|
||||
designedMinStep: 1,
|
||||
unit: "%"
|
||||
}]
|
||||
}];
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.accessory = HttpAccessory;
|
||||
@@ -1,305 +0,0 @@
|
||||
var types = require("HAP-NodeJS/accessories/types.js");
|
||||
var request = require("request");
|
||||
|
||||
// This seems to be the "id" of the official LiftMaster iOS app
|
||||
var APP_ID = "JVM/G9Nwih5BwKgNCjLxiFUQxQijAebyyg8QUHr7JOrP+tuPb8iHfRHKwTmDzHOu"
|
||||
|
||||
function LiftMasterAccessory(log, config) {
|
||||
this.log = log;
|
||||
this.name = config["name"];
|
||||
this.username = config["username"];
|
||||
this.password = config["password"];
|
||||
this.requiredDeviceId = config["requiredDeviceId"];
|
||||
}
|
||||
|
||||
LiftMasterAccessory.prototype = {
|
||||
|
||||
setState: function(state) {
|
||||
this.targetState = state;
|
||||
this.login();
|
||||
},
|
||||
|
||||
login: function() {
|
||||
var that = this;
|
||||
|
||||
// reset our logged-in state hint until we're logged in
|
||||
this.deviceId = null;
|
||||
|
||||
// querystring params
|
||||
var query = {
|
||||
appId: APP_ID,
|
||||
username: this.username,
|
||||
password: this.password,
|
||||
culture: "en"
|
||||
};
|
||||
|
||||
// login to liftmaster
|
||||
request.get({
|
||||
url: "https://myqexternal.myqdevice.com/api/user/validatewithculture",
|
||||
qs: query
|
||||
}, function(err, response, body) {
|
||||
|
||||
if (!err && response.statusCode == 200) {
|
||||
|
||||
// parse and interpret the response
|
||||
var json = JSON.parse(body);
|
||||
that.userId = json["UserId"];
|
||||
that.securityToken = json["SecurityToken"];
|
||||
that.log("Logged in with user ID " + that.userId);
|
||||
that.getDevice();
|
||||
}
|
||||
else {
|
||||
that.log("Error '"+err+"' logging in: " + body);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// find your garage door ID
|
||||
getDevice: function() {
|
||||
var that = this;
|
||||
|
||||
// querystring params
|
||||
var query = {
|
||||
appId: APP_ID,
|
||||
SecurityToken: this.securityToken,
|
||||
filterOn: "true"
|
||||
};
|
||||
|
||||
// some necessary duplicated info in the headers
|
||||
var headers = {
|
||||
MyQApplicationId: APP_ID,
|
||||
SecurityToken: this.securityToken
|
||||
};
|
||||
|
||||
// request details of all your devices
|
||||
request.get({
|
||||
url: "https://myqexternal.myqdevice.com/api/v4/userdevicedetails/get",
|
||||
qs: query,
|
||||
headers: headers
|
||||
}, function(err, response, body) {
|
||||
|
||||
if (!err && response.statusCode == 200) {
|
||||
|
||||
// parse and interpret the response
|
||||
var json = JSON.parse(body);
|
||||
var devices = json["Devices"];
|
||||
var foundDoors = [];
|
||||
|
||||
// look through the array of devices for an opener
|
||||
for (var i=0; i<devices.length; i++) {
|
||||
var device = devices[i];
|
||||
|
||||
if (device["MyQDeviceTypeName"] == "GarageDoorOpener") {
|
||||
|
||||
// If we haven't explicity specified a door ID, we'll loop to make sure we don't have multiple openers, which is confusing
|
||||
if (that.requiredDeviceId == undefined) {
|
||||
var thisDeviceId = device.MyQDeviceId;
|
||||
var thisDoorName = "Unknown";
|
||||
for (var j = 0; j < device.Attributes.length; j ++) {
|
||||
var thisAttributeSet = device.Attributes[j];
|
||||
if (thisAttributeSet.AttributeDisplayName == "desc") {
|
||||
thisDoorName = thisAttributeSet.Value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
foundDoors.push(thisDeviceId + " - " + thisDoorName);
|
||||
}
|
||||
|
||||
// We specified a door ID, sanity check to make sure it's the one we expected
|
||||
else if (that.requiredDeviceId == device.MyQDeviceId) {
|
||||
that.deviceId = device.MyQDeviceId;
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// If we have multiple found doors, refuse to proceed
|
||||
if (foundDoors.length > 1) {
|
||||
that.log("WARNING: You have multiple doors on your MyQ account.");
|
||||
that.log("WARNING: Specify the ID of the door you want to control using the 'requiredDeviceId' property in your config.json file.");
|
||||
that.log("WARNING: You can have multiple liftmaster accessories to cover your multiple doors");
|
||||
|
||||
for (var j = 0; j < foundDoors.length; j++) {
|
||||
that.log("Found Door: " + foundDoors[j]);
|
||||
}
|
||||
|
||||
throw "FATAL: Please specify which specific door this Liftmaster accessory should control - you have multiples on your account";
|
||||
|
||||
}
|
||||
|
||||
// Did we get a device ID?
|
||||
if (that.deviceId) {
|
||||
that.log("Found an opener with ID " + that.deviceId +". Ready to send command...");
|
||||
that.setTargetState();
|
||||
}
|
||||
else
|
||||
{
|
||||
that.log("Error: Couldn't find a door device, or the ID you specified isn't associated with your account");
|
||||
}
|
||||
}
|
||||
else {
|
||||
that.log("Error '"+err+"' getting devices: " + body);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
setTargetState: function() {
|
||||
|
||||
var that = this;
|
||||
var liftmasterState = (this.targetState + "") == "1" ? "0" : "1";
|
||||
|
||||
// querystring params
|
||||
var query = {
|
||||
appId: APP_ID,
|
||||
SecurityToken: this.securityToken,
|
||||
filterOn: "true"
|
||||
};
|
||||
|
||||
// some necessary duplicated info in the headers
|
||||
var headers = {
|
||||
MyQApplicationId: APP_ID,
|
||||
SecurityToken: this.securityToken
|
||||
};
|
||||
|
||||
// PUT request body
|
||||
var body = {
|
||||
AttributeName: "desireddoorstate",
|
||||
AttributeValue: liftmasterState,
|
||||
ApplicationId: APP_ID,
|
||||
SecurityToken: this.securityToken,
|
||||
MyQDeviceId: this.deviceId
|
||||
};
|
||||
|
||||
// send the state request to liftmaster
|
||||
request.put({
|
||||
url: "https://myqexternal.myqdevice.com/api/v4/DeviceAttribute/PutDeviceAttribute",
|
||||
qs: query,
|
||||
headers: headers,
|
||||
body: body,
|
||||
json: true
|
||||
}, function(err, response, json) {
|
||||
|
||||
if (!err && response.statusCode == 200) {
|
||||
|
||||
if (json["ReturnCode"] == "0")
|
||||
that.log("State was successfully set.");
|
||||
else
|
||||
that.log("Bad return code: " + json["ReturnCode"]);
|
||||
that.log("Raw response " + JSON.stringify(json));
|
||||
}
|
||||
else {
|
||||
that.log("Error '"+err+"' setting door state: " + JSON.stringify(json));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getServices: function() {
|
||||
var that = this;
|
||||
return [{
|
||||
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||
characteristics: [{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: this.name,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of the accessory",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MANUFACTURER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "LiftMaster",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Manufacturer",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MODEL_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "Rev-1",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Model",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.SERIAL_NUMBER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "A1S2NASF88EW",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "SN",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.IDENTIFY_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pw"],
|
||||
format: "bool",
|
||||
initialValue: false,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Identify Accessory",
|
||||
designedMaxLength: 1
|
||||
}]
|
||||
},{
|
||||
sType: types.GARAGE_DOOR_OPENER_STYPE,
|
||||
characteristics: [{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "Garage Door Opener Control",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of service",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.CURRENT_DOOR_STATE_CTYPE,
|
||||
onUpdate: function(value) { that.log("Update current state to " + value); },
|
||||
perms: ["pr","ev"],
|
||||
format: "int",
|
||||
initialValue: 0,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "BlaBla",
|
||||
designedMinValue: 0,
|
||||
designedMaxValue: 4,
|
||||
designedMinStep: 1,
|
||||
designedMaxLength: 1
|
||||
},{
|
||||
cType: types.TARGET_DOORSTATE_CTYPE,
|
||||
onUpdate: function(value) { that.setState(value); },
|
||||
perms: ["pr","pw","ev"],
|
||||
format: "int",
|
||||
initialValue: 0,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "BlaBla",
|
||||
designedMinValue: 0,
|
||||
designedMaxValue: 1,
|
||||
designedMinStep: 1,
|
||||
designedMaxLength: 1
|
||||
},{
|
||||
cType: types.OBSTRUCTION_DETECTED_CTYPE,
|
||||
onUpdate: function(value) { that.log("Obstruction detected: " + value); },
|
||||
perms: ["pr","ev"],
|
||||
format: "bool",
|
||||
initialValue: false,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "BlaBla"
|
||||
}]
|
||||
}];
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.accessory = LiftMasterAccessory;
|
||||
@@ -1,196 +0,0 @@
|
||||
var types = require("HAP-NodeJS/accessories/types.js");
|
||||
var request = require("request");
|
||||
|
||||
function LockitronAccessory(log, config) {
|
||||
this.log = log;
|
||||
this.name = config["name"];
|
||||
this.lockID = config["lock_id"];
|
||||
this.accessToken = config["api_token"];
|
||||
}
|
||||
|
||||
LockitronAccessory.prototype = {
|
||||
getState: function(callback) {
|
||||
this.log("Getting current state...");
|
||||
|
||||
var that = this;
|
||||
|
||||
var query = {
|
||||
access_token: this.accessToken
|
||||
};
|
||||
|
||||
request.get({
|
||||
url: "https://api.lockitron.com/v2/locks/"+this.lockID,
|
||||
qs: query
|
||||
}, function(err, response, body) {
|
||||
|
||||
if (!err && response.statusCode == 200) {
|
||||
var json = JSON.parse(body);
|
||||
var state = json.state; // "lock" or "unlock"
|
||||
var locked = state == "lock"
|
||||
callback(locked);
|
||||
}
|
||||
else {
|
||||
that.log("Error getting state (status code "+response.statusCode+"): " + err)
|
||||
callback(undefined);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
setState: function(state) {
|
||||
this.log("Set state to " + state);
|
||||
|
||||
var lockitronState = (state == 1) ? "lock" : "unlock";
|
||||
var that = this;
|
||||
|
||||
var query = {
|
||||
access_token: this.accessToken,
|
||||
state: lockitronState
|
||||
};
|
||||
|
||||
request.put({
|
||||
url: "https://api.lockitron.com/v2/locks/"+this.lockID,
|
||||
qs: query
|
||||
}, function(err, response, body) {
|
||||
|
||||
if (!err && response.statusCode == 200) {
|
||||
that.log("State change complete.");
|
||||
}
|
||||
else {
|
||||
that.log("Error '"+err+"' setting lock state: " + body);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getServices: function() {
|
||||
var that = this;
|
||||
return [{
|
||||
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||
characteristics: [{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: this.name,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of the accessory",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MANUFACTURER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "Apigee",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Manufacturer",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MODEL_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "Rev-2",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Model",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.SERIAL_NUMBER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "A1S2NASF88EW",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "SN",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.IDENTIFY_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pw"],
|
||||
format: "bool",
|
||||
initialValue: false,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Identify Accessory",
|
||||
designedMaxLength: 1
|
||||
}]
|
||||
},{
|
||||
sType: types.LOCK_MECHANISM_STYPE,
|
||||
characteristics: [{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "Lock Mechanism",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of service",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.CURRENT_LOCK_MECHANISM_STATE_CTYPE,
|
||||
onRead: function(callback) { that.getState(callback); },
|
||||
onUpdate: function(value) { that.log("Update current state to " + value); },
|
||||
perms: ["pr","ev"],
|
||||
format: "int",
|
||||
initialValue: 0,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "BlaBla",
|
||||
designedMinValue: 0,
|
||||
designedMaxValue: 3,
|
||||
designedMinStep: 1,
|
||||
designedMaxLength: 1
|
||||
},{
|
||||
cType: types.TARGET_LOCK_MECHANISM_STATE_CTYPE,
|
||||
onUpdate: function(value) { that.setState(value); },
|
||||
perms: ["pr","pw","ev"],
|
||||
format: "int",
|
||||
initialValue: 0,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "BlaBla",
|
||||
designedMinValue: 0,
|
||||
designedMaxValue: 1,
|
||||
designedMinStep: 1,
|
||||
designedMaxLength: 1
|
||||
}]
|
||||
},{
|
||||
sType: types.LOCK_MANAGEMENT_STYPE,
|
||||
characteristics: [{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "Lock Management",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of service",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.LOCK_MANAGEMENT_CONTROL_POINT_CTYPE,
|
||||
onUpdate: function(value) { that.log("Update control point to " + value); },
|
||||
perms: ["pw"],
|
||||
format: "data",
|
||||
initialValue: 0,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "BlaBla",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.VERSION_CTYPE,
|
||||
onUpdate: function(value) { that.log("Update version to " + value); },
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "1.0",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "BlaBla",
|
||||
designedMaxLength: 255
|
||||
}]
|
||||
}];
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.accessory = LockitronAccessory;
|
||||
@@ -1,146 +0,0 @@
|
||||
var types = require("HAP-NodeJS/accessories/types.js");
|
||||
var sonos = require('sonos');
|
||||
|
||||
function SonosAccessory(log, config) {
|
||||
this.log = log;
|
||||
this.name = config["name"];
|
||||
this.playVolume = config["play_volume"];
|
||||
this.device = null;
|
||||
this.search();
|
||||
}
|
||||
|
||||
SonosAccessory.prototype = {
|
||||
|
||||
search: function() {
|
||||
var that = this;
|
||||
|
||||
sonos.search(function(device) {
|
||||
that.log("Found device at " + device.host);
|
||||
|
||||
device.deviceDescription(function (err, description) {
|
||||
|
||||
if (description["zoneType"] == '3') {
|
||||
that.log("Found playable device");
|
||||
// device is an instance of sonos.Sonos
|
||||
that.device = device;
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
setPlaying: function(playing) {
|
||||
|
||||
if (!this.device) {
|
||||
this.log("No device found (yet?)");
|
||||
return;
|
||||
}
|
||||
|
||||
var that = this;
|
||||
|
||||
if (playing) {
|
||||
this.device.play(function(err, success) {
|
||||
that.log("Playback attempt with success: " + success);
|
||||
});
|
||||
|
||||
if (this.playVolume) {
|
||||
this.device.setVolume(this.playVolume, function(err, success) {
|
||||
if (!err) {
|
||||
that.log("Set volume to " + that.playVolume);
|
||||
}
|
||||
else {
|
||||
that.log("Problem setting volume: " + err);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.device.stop(function(err, success) {
|
||||
that.log("Stop attempt with success: " + success);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
getServices: function() {
|
||||
var that = this;
|
||||
return [{
|
||||
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||
characteristics: [{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: this.name,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of the accessory",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MANUFACTURER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "Sonos",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Manufacturer",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MODEL_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "Rev-1",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Model",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.SERIAL_NUMBER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "A1S2NASF88EW",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "SN",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.IDENTIFY_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pw"],
|
||||
format: "bool",
|
||||
initialValue: false,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Identify Accessory",
|
||||
designedMaxLength: 1
|
||||
}]
|
||||
},{
|
||||
sType: types.SWITCH_STYPE,
|
||||
characteristics: [{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "Speakers",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of service",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.POWER_STATE_CTYPE,
|
||||
onUpdate: function(value) { that.setPlaying(value); },
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "bool",
|
||||
initialValue: false,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Change the playback state of the sonos",
|
||||
designedMaxLength: 1
|
||||
}]
|
||||
}];
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.accessory = SonosAccessory;
|
||||
@@ -1,139 +0,0 @@
|
||||
var types = require("HAP-NodeJS/accessories/types.js");
|
||||
var wemo = require('wemo');
|
||||
|
||||
// extend our search timeout from 5 seconds to 60
|
||||
wemo.SearchTimeout = 60000;
|
||||
wemo.timeout = wemo.SearchTimeout // workaround for a bug in wemo.js v0.0.4
|
||||
|
||||
function WeMoAccessory(log, config) {
|
||||
this.log = log;
|
||||
this.name = config["name"];
|
||||
this.wemoName = config["wemo_name"];
|
||||
this.device = null;
|
||||
this.log("Searching for WeMo device with exact name '" + this.wemoName + "'...");
|
||||
this.search();
|
||||
}
|
||||
|
||||
WeMoAccessory.prototype = {
|
||||
|
||||
search: function() {
|
||||
var that = this;
|
||||
|
||||
wemo.Search(this.wemoName, function(err, device) {
|
||||
if (!err && device) {
|
||||
that.log("Found '"+that.wemoName+"' device at " + device.ip);
|
||||
that.device = new wemo(device.ip, device.port);
|
||||
}
|
||||
else {
|
||||
that.log("Error finding device '" + that.wemoName + "': " + err);
|
||||
that.log("Continuing search for WeMo device with exact name '" + that.wemoName + "'...");
|
||||
that.search();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
setPowerState: function(powerOn) {
|
||||
|
||||
if (!this.device) {
|
||||
this.log("No '"+this.wemoName+"' device found (yet?)");
|
||||
return;
|
||||
}
|
||||
|
||||
var binaryState = powerOn ? 1 : 0;
|
||||
var that = this;
|
||||
|
||||
this.log("Setting power state on the '"+this.wemoName+"' to " + binaryState);
|
||||
|
||||
this.device.setBinaryState(binaryState, function(err, result) {
|
||||
if (!err) {
|
||||
that.log("Successfully set power state on the '"+that.wemoName+"' to " + binaryState);
|
||||
}
|
||||
else {
|
||||
that.log("Error setting power state on the '"+that.wemoName+"'")
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getServices: function() {
|
||||
var that = this;
|
||||
return [{
|
||||
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||
characteristics: [{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: this.name,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of the accessory",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MANUFACTURER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "WeMo",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Manufacturer",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MODEL_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "Rev-1",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Model",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.SERIAL_NUMBER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "A1S2NASF88EW",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "SN",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.IDENTIFY_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pw"],
|
||||
format: "bool",
|
||||
initialValue: false,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Identify Accessory",
|
||||
designedMaxLength: 1
|
||||
}]
|
||||
},{
|
||||
sType: types.SWITCH_STYPE,
|
||||
characteristics: [{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: this.name,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of service",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.POWER_STATE_CTYPE,
|
||||
onUpdate: function(value) { that.setPowerState(value); },
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "bool",
|
||||
initialValue: false,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Change the power state of the WeMo",
|
||||
designedMaxLength: 1
|
||||
}]
|
||||
}];
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.accessory = WeMoAccessory;
|
||||
@@ -1,151 +0,0 @@
|
||||
var types = require("HAP-NodeJS/accessories/types.js");
|
||||
var request = require("request");
|
||||
|
||||
function X10(log, config) {
|
||||
this.log = log;
|
||||
this.ip_address = config["ip_address"];
|
||||
this.name = config["name"];
|
||||
this.deviceID = config["device_id"];
|
||||
this.protocol = config["protocol"];
|
||||
this.canDim = config["can_dim"];
|
||||
}
|
||||
|
||||
X10.prototype = {
|
||||
|
||||
setPowerState: function(powerOn) {
|
||||
|
||||
var binaryState = powerOn ? "on" : "off";
|
||||
var that = this;
|
||||
|
||||
this.log("Setting power state of " + this.deviceID + " to " + powerOn);
|
||||
request.put({
|
||||
url: "http://"+this.ip_address+"/x10/"+this.deviceID+"/power/"+binaryState+"?protocol="+this.protocol,
|
||||
}, function(err, response, body) {
|
||||
|
||||
if (!err && response.statusCode == 200) {
|
||||
that.log("State change complete.");
|
||||
}
|
||||
else {
|
||||
that.log("Error '"+err+"' setting power state: " + body);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
setBrightnessLevel: function(value) {
|
||||
|
||||
var that = this;
|
||||
|
||||
this.log("Setting brightness level of " + this.deviceID + " to " + value);
|
||||
request.put({
|
||||
url: "http://"+this.ip_address+"/x10/"+this.deviceID+"/brightness/?protocol="+this.protocol+"&value="+value,
|
||||
}, function(err, response, body) {
|
||||
|
||||
if (!err && response.statusCode == 200) {
|
||||
that.log("State change complete.");
|
||||
}
|
||||
else {
|
||||
that.log("Error '"+err+"' setting brightness level: " + body);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getServices: function() {
|
||||
var that = this;
|
||||
var services = [{
|
||||
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||
characteristics: [{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: this.name,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of the accessory",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MANUFACTURER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "X10",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Manufacturer",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MODEL_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "Rev-1",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Model",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.SERIAL_NUMBER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "A1S2NASF88EW",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "SN",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.IDENTIFY_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pw"],
|
||||
format: "bool",
|
||||
initialValue: false,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Identify Accessory",
|
||||
designedMaxLength: 1
|
||||
}]
|
||||
},{
|
||||
sType: types.LIGHTBULB_STYPE,
|
||||
characteristics: [{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: this.name,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of service",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.POWER_STATE_CTYPE,
|
||||
onUpdate: function(value) { that.setPowerState(value); },
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "bool",
|
||||
initialValue: false,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Change the power state of a Variable",
|
||||
designedMaxLength: 1
|
||||
}]
|
||||
}];
|
||||
if (that.canDim) {
|
||||
services[1].characteristics.push({
|
||||
cType: types.BRIGHTNESS_CTYPE,
|
||||
onUpdate: function(value) { that.setBrightnessLevel(value); },
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "int",
|
||||
initialValue: 0,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Adjust Brightness of Light",
|
||||
designedMinValue: 0,
|
||||
designedMaxValue: 100,
|
||||
designedMinStep: 1,
|
||||
unit: "%"
|
||||
});
|
||||
}
|
||||
return services;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.accessory = X10;
|
||||
@@ -1,284 +0,0 @@
|
||||
var types = require("HAP-NodeJS/accessories/types.js");
|
||||
var request = require("request");
|
||||
var xmldoc = require("xmldoc");
|
||||
|
||||
function XfinityHomeAccessory(log, config) {
|
||||
this.log = log;
|
||||
this.name = config["name"];
|
||||
this.email = config["email"];
|
||||
this.password = config["password"];
|
||||
this.dsig = config["dsig"];
|
||||
this.pinCode = config["pin"];
|
||||
}
|
||||
|
||||
XfinityHomeAccessory.prototype = {
|
||||
|
||||
armWithType: function(armed, type) {
|
||||
this.log("Arming with type " + type + " = " + armed + "...");
|
||||
this.targetArmed = armed;
|
||||
this.targetArmType = type;
|
||||
this.getLoginToken();
|
||||
},
|
||||
|
||||
getLoginToken: function() {
|
||||
this.log("Retrieving login token...");
|
||||
|
||||
var that = this;
|
||||
|
||||
request.post({
|
||||
url: "https://login.comcast.net/api/login",
|
||||
form: {
|
||||
appkey:"iControl",
|
||||
dsig: this.dsig,
|
||||
u: this.email,
|
||||
p: this.password
|
||||
}
|
||||
}, function(err, response, body) {
|
||||
|
||||
if (!err && response.statusCode == 200) {
|
||||
|
||||
var doc = new xmldoc.XmlDocument(body);
|
||||
that.loginToken = doc.valueWithPath("LoginToken");
|
||||
that.refreshLoginCookie();
|
||||
}
|
||||
else {
|
||||
that.log("Error '"+err+"' getting login token: " + body);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
refreshLoginCookie: function() {
|
||||
this.log("Refreshing login cookie...");
|
||||
|
||||
var that = this;
|
||||
|
||||
request.post({
|
||||
url: "https://www.xfinityhomesecurity.com/rest/icontrol/login",
|
||||
form: {
|
||||
token: this.loginToken
|
||||
}
|
||||
}, function(err, response, body) {
|
||||
|
||||
if (!err && response.statusCode == 200) {
|
||||
|
||||
// extract our "site" from the login response
|
||||
var json = JSON.parse(body);
|
||||
that.siteHref = json["login"]["site"]["href"];
|
||||
|
||||
// manual cookie handling
|
||||
that.loginCookie = response.headers["set-cookie"];
|
||||
|
||||
that.getInstances();
|
||||
}
|
||||
else {
|
||||
that.log("Error '"+err+"' refreshing login cookie: " + body);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getInstances: function() {
|
||||
this.log("Getting instances for site " + this.siteHref + "...");
|
||||
|
||||
this.panelHref = null;
|
||||
var that = this;
|
||||
|
||||
request.get({
|
||||
url: "https://www.xfinityhomesecurity.com/"+that.siteHref+"/network/instances",
|
||||
headers: { Cookie: this.loginCookie },
|
||||
json: true
|
||||
}, function(err, response, json) {
|
||||
|
||||
if (!err && response.statusCode == 200) {
|
||||
|
||||
// extract our "instance" from the response. look for the first "panel"
|
||||
var instances = json["instances"]["instance"];
|
||||
for (var i=0; i<instances.length; i++) {
|
||||
var instance = instances[i];
|
||||
|
||||
if (instance["mediaType"] == "instance/panel") {
|
||||
that.panelHref = instance.href;
|
||||
}
|
||||
}
|
||||
|
||||
if (that.panelHref) {
|
||||
that.log("Found panel " + that.panelHref + ". Ready to arm.");
|
||||
that.finishArm();
|
||||
}
|
||||
else {
|
||||
that.log("Couldn't find a panel.");
|
||||
}
|
||||
}
|
||||
else {
|
||||
that.log("Error '"+err+"' getting instances: " + JSON.stringify(json));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
finishArm: function() {
|
||||
this.log("Finish arming with type " + this.targetArmType + " = " + this.targetArmed + "...");
|
||||
|
||||
var path, form;
|
||||
var that = this;
|
||||
|
||||
if (!this.targetArmed) {
|
||||
path = this.panelHref + "/functions/disarm";
|
||||
form = {code: this.pinCode};
|
||||
}
|
||||
else {
|
||||
path = this.panelHref + "/functions/arm";
|
||||
form = {code: this.pinCode, armType: this.targetArmType };
|
||||
}
|
||||
|
||||
request.post({
|
||||
url: "https://www.xfinityhomesecurity.com"+path,
|
||||
headers: { Cookie: this.loginCookie },
|
||||
form: form
|
||||
}, function(err, response, body) {
|
||||
|
||||
if (!err && response.statusCode >= 200 && response.statusCode < 300) {
|
||||
that.log("Arm response: " + response);
|
||||
}
|
||||
else {
|
||||
that.log("Error '"+err+"' performing arm request: " + body);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getServices: function() {
|
||||
var that = this;
|
||||
return [{
|
||||
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||
characteristics: [{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: this.name,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of the accessory",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MANUFACTURER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "Comcast",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Manufacturer",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MODEL_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "Rev-1",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Model",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.SERIAL_NUMBER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "A1S2NASF88EW",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "SN",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.IDENTIFY_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pw"],
|
||||
format: "bool",
|
||||
initialValue: false,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Identify Accessory",
|
||||
designedMaxLength: 1
|
||||
}]
|
||||
},{
|
||||
sType: types.SWITCH_STYPE,
|
||||
characteristics: [{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "Away Mode",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Away Mode service",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.POWER_STATE_CTYPE,
|
||||
onUpdate: function(value) { that.armWithType(value, "away"); },
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "bool",
|
||||
initialValue: false,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Turn on the Away alarm",
|
||||
designedMaxLength: 1
|
||||
}]
|
||||
},{
|
||||
sType: types.SWITCH_STYPE,
|
||||
characteristics: [{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "Night Mode",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Night Mode service",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.POWER_STATE_CTYPE,
|
||||
onUpdate: function(value) { that.armWithType(value, "night"); },
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "bool",
|
||||
initialValue: false,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Turn on the Night alarm",
|
||||
designedMaxLength: 1
|
||||
}]
|
||||
},{
|
||||
sType: types.SWITCH_STYPE,
|
||||
characteristics: [{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "Stay Mode",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Stay Mode service",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.POWER_STATE_CTYPE,
|
||||
onUpdate: function(value) { that.armWithType(value, "stay"); },
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "bool",
|
||||
initialValue: false,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Turn on the Stay alarm",
|
||||
designedMaxLength: 1
|
||||
}]
|
||||
}];
|
||||
}
|
||||
};
|
||||
|
||||
// Enable cookie handling and append our expected headers
|
||||
request = request.defaults({
|
||||
headers: {
|
||||
"X-appkey": "comcastTokenKey",
|
||||
"X-ClientInfo": "5.2.51",
|
||||
"X-format": "json"
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.accessory = XfinityHomeAccessory;
|
||||
189
app.js
189
app.js
@@ -1,189 +0,0 @@
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var storage = require('node-persist');
|
||||
var crypto = require('crypto');
|
||||
|
||||
console.log("Starting HomeBridge server...");
|
||||
|
||||
// Look for the configuration file
|
||||
var configPath = path.join(__dirname, "config.json");
|
||||
|
||||
// Complain and exit if it doesn't exist yet
|
||||
if (!fs.existsSync(configPath)) {
|
||||
console.log("Couldn't find a config.json file in the same directory as app.js. Look at config-sample.json for examples of how to format your config.js and add your home accessories.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Initialize persistent storage
|
||||
storage.initSync();
|
||||
|
||||
// Load up the configuration file
|
||||
var config = JSON.parse(fs.readFileSync(configPath));
|
||||
|
||||
// Just to prevent them getting garbage collected
|
||||
var accessories = [];
|
||||
|
||||
function startup() {
|
||||
if (config.platforms) loadPlatforms();
|
||||
if (config.accessories) loadAccessories();
|
||||
}
|
||||
|
||||
function loadAccessories() {
|
||||
|
||||
// Instantiate all accessories in the config
|
||||
console.log("Loading " + config.accessories.length + " accessories...");
|
||||
for (var i=0; i<config.accessories.length; i++) {
|
||||
|
||||
var accessoryConfig = config.accessories[i];
|
||||
|
||||
// Load up the class for this accessory
|
||||
var accessoryName = accessoryConfig["accessory"]; // like "WeMo"
|
||||
var accessoryModule = require('./accessories/' + accessoryName + ".js"); // like "./accessories/WeMo.js"
|
||||
var accessoryConstructor = accessoryModule.accessory; // like "WeMoAccessory", a JavaScript constructor
|
||||
|
||||
// Create a custom logging function that prepends the device display name for debugging
|
||||
var name = accessoryConfig["name"];
|
||||
var log = function(name) { return function(s) { console.log("[" + name + "] " + s); }; }(name);
|
||||
|
||||
log("Initializing " + accessoryName + " accessory...");
|
||||
var accessory = new accessoryConstructor(log, accessoryConfig);
|
||||
accessories.push(accessory);
|
||||
|
||||
// Extract the raw "services" for this accessory which is a big array of objects describing the various
|
||||
// hooks in and out of HomeKit for the HAP-NodeJS server.
|
||||
var services = accessory.getServices();
|
||||
|
||||
// Create the HAP server for this accessory
|
||||
createHAPServer(name, services, accessory.transportCategory);
|
||||
}
|
||||
}
|
||||
|
||||
function loadPlatforms() {
|
||||
|
||||
console.log("Loading " + config.platforms.length + " platforms...");
|
||||
for (var i=0; i<config.platforms.length; i++) {
|
||||
|
||||
var platformConfig = config.platforms[i];
|
||||
|
||||
// Load up the class for this accessory
|
||||
var platformName = platformConfig["platform"]; // like "Wink"
|
||||
var platformModule = require('./platforms/' + platformName + ".js"); // like "./platforms/Wink.js"
|
||||
var platformConstructor = platformModule.platform; // like "WinkPlatform", a JavaScript constructor
|
||||
|
||||
// Create a custom logging function that prepends the platform display name for debugging
|
||||
var name = platformConfig["name"];
|
||||
var log = function(name) { return function(s) { console.log("[" + name + "] " + s); }; }(name);
|
||||
|
||||
log("Initializing " + platformName + " platform...");
|
||||
|
||||
var platform = new platformConstructor(log, platformConfig);
|
||||
|
||||
// query for devices
|
||||
platform.accessories(function(foundAccessories){
|
||||
// loop through accessories adding them to the list and registering them
|
||||
for (var i = 0; i < foundAccessories.length; i++) {
|
||||
accessory = foundAccessories[i]
|
||||
accessories.push(accessory);
|
||||
log("Initializing device with name " + accessory.name + "...")
|
||||
// Extract the raw "services" for this accessory which is a big array of objects describing the various
|
||||
// hooks in and out of HomeKit for the HAP-NodeJS server.
|
||||
var services = accessory.getServices();
|
||||
// Create the HAP server for this accessory
|
||||
createHAPServer(accessory.name, services, accessory.transportCategory);
|
||||
}
|
||||
accessories.push.apply(accessories, foundAccessories);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Creates the actual HAP servers which listen on different sockets
|
||||
//
|
||||
|
||||
// Pull in required HAP-NodeJS stuff
|
||||
var accessory_Factor = new require("HAP-NodeJS/Accessory.js");
|
||||
var accessoryController_Factor = new require("HAP-NodeJS/AccessoryController.js");
|
||||
var service_Factor = new require("HAP-NodeJS/Service.js");
|
||||
var characteristic_Factor = new require("HAP-NodeJS/Characteristic.js");
|
||||
|
||||
// Each accessory has its own little server. We'll need to allocate some ports for these servers
|
||||
var nextPort = 51826;
|
||||
var nextServer = 0;
|
||||
var accessoryServers = [];
|
||||
var accessoryControllers = [];
|
||||
var usernames = {};
|
||||
|
||||
function createHAPServer(name, services, transportCategory) {
|
||||
var accessoryController = new accessoryController_Factor.AccessoryController();
|
||||
|
||||
//loop through services
|
||||
for (var j = 0; j < services.length; j++) {
|
||||
var service = new service_Factor.Service(services[j].sType);
|
||||
|
||||
//loop through characteristics
|
||||
for (var k = 0; k < services[j].characteristics.length; k++) {
|
||||
var options = {
|
||||
onRead: services[j].characteristics[k].onRead,
|
||||
onRegister: services[j].characteristics[k].onRegister,
|
||||
type: services[j].characteristics[k].cType,
|
||||
perms: services[j].characteristics[k].perms,
|
||||
format: services[j].characteristics[k].format,
|
||||
initialValue: services[j].characteristics[k].initialValue,
|
||||
supportEvents: services[j].characteristics[k].supportEvents,
|
||||
supportBonjour: services[j].characteristics[k].supportBonjour,
|
||||
manfDescription: services[j].characteristics[k].manfDescription,
|
||||
designedMaxLength: services[j].characteristics[k].designedMaxLength,
|
||||
designedMinValue: services[j].characteristics[k].designedMinValue,
|
||||
designedMaxValue: services[j].characteristics[k].designedMaxValue,
|
||||
designedMinStep: services[j].characteristics[k].designedMinStep,
|
||||
unit: services[j].characteristics[k].unit
|
||||
};
|
||||
|
||||
var characteristic = new characteristic_Factor.Characteristic(options, services[j].characteristics[k].onUpdate);
|
||||
|
||||
service.addCharacteristic(characteristic);
|
||||
}
|
||||
accessoryController.addService(service);
|
||||
}
|
||||
|
||||
// create a unique "username" for this accessory based on the default display name
|
||||
var username = createUsername(name);
|
||||
|
||||
if (usernames[username]) {
|
||||
console.log("Cannot create another accessory with the same name '" + name + "'. The 'name' property must be unique for each accessory.");
|
||||
return;
|
||||
}
|
||||
|
||||
// remember that we used this name already
|
||||
usernames[username] = name;
|
||||
|
||||
// increment ports for each accessory
|
||||
nextPort = nextPort + (nextServer*2);
|
||||
|
||||
// hardcode the PIN to something random (same PIN as HAP-NodeJS sample accessories)
|
||||
var pincode = "031-45-154";
|
||||
|
||||
var accessory = new accessory_Factor.Accessory(name, username, storage, parseInt(nextPort), pincode, accessoryController, transportCategory);
|
||||
accessoryServers[nextServer] = accessory;
|
||||
accessoryControllers[nextServer] = accessoryController;
|
||||
accessory.publishAccessory();
|
||||
|
||||
nextServer++;
|
||||
}
|
||||
|
||||
// Creates a unique "username" for HomeKit from a hash of the given string
|
||||
function createUsername(str) {
|
||||
|
||||
// Hash str into something like "098F6BCD4621D373CADE4E832627B4F6"
|
||||
var hash = crypto.createHash('md5').update(str).digest("hex").toUpperCase();
|
||||
|
||||
// Turn it into a MAC-address-looking "username" for HomeKit
|
||||
return hash[0] + hash[1] + ":" +
|
||||
hash[2] + hash[3] + ":" +
|
||||
hash[4] + hash[5] + ":" +
|
||||
hash[6] + hash[7] + ":" +
|
||||
hash[8] + hash[9] + ":" +
|
||||
hash[10] + hash[11];
|
||||
}
|
||||
|
||||
startup();
|
||||
17
bin/homebridge
Executable file
17
bin/homebridge
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
//
|
||||
// This executable sets up the environment and runs the HomeBridge CLI.
|
||||
//
|
||||
|
||||
'use strict';
|
||||
|
||||
process.title = 'homebridge';
|
||||
|
||||
// Find the HomeBridge lib
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
var lib = path.join(path.dirname(fs.realpathSync(__filename)), '../lib');
|
||||
|
||||
// Run HomeBridge
|
||||
require(lib + '/cli')();
|
||||
@@ -1,125 +1,24 @@
|
||||
{
|
||||
"description": "This is an example configuration file with all supported devices. You can use this as a template for creating your own configuration file containing devices you actually own.",
|
||||
|
||||
"platforms": [
|
||||
{
|
||||
"platform": "Wink",
|
||||
"name": "Wink",
|
||||
"client_id": "YOUR_WINK_API_CLIENT_ID",
|
||||
"client_secret": "YOUR_WINK_API_CLIENT_SECRET",
|
||||
"username": "your@email.com",
|
||||
"password": "WINK_PASSWORD"
|
||||
},
|
||||
{
|
||||
"platform": "SmartThings",
|
||||
"name": "SmartThings",
|
||||
"app_id": "JSON SmartApp Id",
|
||||
"access_token": "JSON SmartApp AccessToken"
|
||||
},
|
||||
{
|
||||
"platform": "Domoticz",
|
||||
"name": "Domoticz",
|
||||
"server": "127.0.0.1",
|
||||
"port": "8005"
|
||||
},
|
||||
{
|
||||
"platform": "PhilipsHue",
|
||||
"name": "Phillips Hue",
|
||||
"username": ""
|
||||
},
|
||||
{
|
||||
"platform": "ISY",
|
||||
"name": "ISY",
|
||||
"host": "192.168.1.20",
|
||||
"port": "8000",
|
||||
"username": "username",
|
||||
"password": "password"
|
||||
}
|
||||
],
|
||||
"bridge": {
|
||||
"name": "Homebridge",
|
||||
"username": "CC:22:3D:E3:CE:30",
|
||||
"port": 51826,
|
||||
"pin": "031-45-154"
|
||||
},
|
||||
|
||||
"description": "This is an example configuration file with one fake accessory and one fake platform. You can use this as a template for creating your own configuration file containing devices you actually own.",
|
||||
|
||||
"accessories": [
|
||||
{
|
||||
"accessory": "WeMo",
|
||||
"name": "Coffee Maker",
|
||||
"description": "This shim supports Belkin WeMo devices on the same network as this server. You can create duplicate entries for this device and change the 'name' attribute to reflect what device is plugged into the WeMo, for instance 'Air Conditioner' or 'Coffee Maker'. This name will be used by Siri. Make sure to update the 'wemo_name' attribute with the EXACT name of the device in the WeMo app itself. This can be the same value as 'name' but it doesn't have to be.",
|
||||
"wemo_name": "CoffeeMaker"
|
||||
},
|
||||
"name": "Coffee Maker"
|
||||
}
|
||||
],
|
||||
|
||||
"platforms": [
|
||||
{
|
||||
"accessory": "LiftMaster",
|
||||
"name": "Garage Door",
|
||||
"description": "This shim supports LiftMaster garage door openers that are already internet-connected to the 'MyQ' service.",
|
||||
// "requiredDeviceId", "<ID of door if you have multiple doors, prompted by shim during startup if needed>",
|
||||
"username": "your-liftmaster-username",
|
||||
"password" : "your-liftmaster-password"
|
||||
},
|
||||
{
|
||||
"accessory": "Sonos",
|
||||
"name": "Speakers",
|
||||
"description": "This shim supports Sonos devices on the same network as this server. It acts as a simple switch that calls play() or pause() on the Sonos, so it's only useful for pausing and resuming tracks or radio stations that are already in the queue. When 'play_volume' is nonzero, the volume will be reset to that value when it turns the Sonos on.",
|
||||
"play_volume": 25
|
||||
},
|
||||
{
|
||||
"accessory": "Lockitron",
|
||||
"name": "Front Door",
|
||||
"description": "This shim supports Lockitron locks. It uses the Lockitron cloud API, so the Lockitron must be 'awake' for locking and unlocking to actually happen. You can wake up Lockitron after issuing an lock/unlock command by knocking on the door.",
|
||||
"lock_id": "your-lock-id",
|
||||
"api_token" : "your-lockitron-api-access-token"
|
||||
},
|
||||
{
|
||||
"accessory": "Carwings",
|
||||
"name": "Leaf",
|
||||
"description": "This shim supports controlling climate control on Nissan cars with Carwings. Note that Carwings is super slow and it may take up to 5 minutes for your command to be processed by the Carwings system.",
|
||||
"username": "your-carwings-username",
|
||||
"password" : "your-carwings-password"
|
||||
},
|
||||
{
|
||||
"accessory": "XfinityHome",
|
||||
"name": "Xfinity Home",
|
||||
"description": "This shim supports the 'Xfinity Home' security system. Unfortunately I don't know how to generate the 'dsig' property, so you'll need to figure yours out by running the Xfinity Home app on your iOS device while connected to a proxy server like Charles. If you didn't understand any of that, sorry! I welcome any suggestions for how to figure out dsig automatically.",
|
||||
"email": "your-comcast-email@example.com",
|
||||
"password": "your-comcast-password",
|
||||
"dsig": "your-digital-signature",
|
||||
"pin": "your-security-system-pin-code"
|
||||
},
|
||||
{
|
||||
"accessory": "HomeMatic",
|
||||
"name": "Light",
|
||||
"description": "Control HomeMatic devices (The XMP-API addon for the CCU is required)",
|
||||
"ccu_id": "The XMP-API id of your HomeMatic device",
|
||||
"ccu_ip": "The IP-Adress of your HomeMatic CCU device"
|
||||
},
|
||||
{
|
||||
"accessory": "X10",
|
||||
"name": "Lamp",
|
||||
"ip_address": "localhost:3000",
|
||||
"device_id": "E1",
|
||||
"protocol": "pl",
|
||||
"can_dim": true
|
||||
},
|
||||
{
|
||||
"accessory": "Http",
|
||||
"name": "Kitchen Lamp",
|
||||
"on_url": "https://192.168.1.22:3030/devices/23222/on",
|
||||
"off_url": "https://192.168.1.22:3030/devices/23222/off",
|
||||
"brightness_url": "https://192.168.1.22:3030/devices/23222/brightness/%b",
|
||||
"http_method": "POST"
|
||||
},{
|
||||
"accessory": "ELKM1",
|
||||
"name": "Security System",
|
||||
"description": "Allows basic control of Elk M1 security system. You can use 1 of 3 arm modes: Away, Stay, Night. If you need to access all 3, create 3 accessories with different names.",
|
||||
"zone": "1",
|
||||
"host": "192.168.1.10",
|
||||
"port": "2101",
|
||||
"pin": "1234",
|
||||
"arm": "Away"
|
||||
},
|
||||
{
|
||||
"accessory": "AD2USB",
|
||||
"name": "Alarm",
|
||||
"description": "Arm, disarm, and status monitoring of the default partition for Honeywell/Ademco alarm systems. Requires network configured AD2USB interface",
|
||||
"host": "192.168.1.200", // IP address of the SER2SOCK service
|
||||
"port" : 4999, // Port the SER2SOCK process is running on
|
||||
"pin": "1234" // PIN used for arming / disarming
|
||||
"platform" : "PhilipsHue",
|
||||
"name" : "Hue"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
218
example-plugins/homebridge-samplePlatform/index.js
Normal file
218
example-plugins/homebridge-samplePlatform/index.js
Normal file
@@ -0,0 +1,218 @@
|
||||
var http = require('http');
|
||||
var Accessory, Service, Characteristic, UUIDGen;
|
||||
|
||||
module.exports = function(homebridge) {
|
||||
console.log("homebridge API version: " + homebridge.version);
|
||||
|
||||
// Accessory must be created from PlatformAccessory Constructor
|
||||
Accessory = homebridge.platformAccessory;
|
||||
|
||||
// Service and Characteristic are from hap-nodejs
|
||||
Service = homebridge.hap.Service;
|
||||
Characteristic = homebridge.hap.Characteristic;
|
||||
UUIDGen = homebridge.hap.uuid;
|
||||
|
||||
// For platform plugin to be considered as dynamic platform plugin,
|
||||
// registerPlatform(pluginName, platformName, constructor, dynamic), dynamic must be true
|
||||
homebridge.registerPlatform("homebridge-samplePlatform", "SamplePlatform", SamplePlatform, true);
|
||||
}
|
||||
|
||||
// Platform constructor
|
||||
// config may be null
|
||||
// api may be null if launched from old homebridge version
|
||||
function SamplePlatform(log, config, api) {
|
||||
log("SamplePlatform Init");
|
||||
var platform = this;
|
||||
this.log = log;
|
||||
this.config = config;
|
||||
this.accessories = [];
|
||||
|
||||
this.requestServer = http.createServer(function(request, response) {
|
||||
if (request.url === "/add") {
|
||||
this.addAccessory(new Date().toISOString());
|
||||
response.writeHead(204);
|
||||
response.end();
|
||||
}
|
||||
|
||||
if (request.url == "/reachability") {
|
||||
this.updateAccessoriesReachability();
|
||||
response.writeHead(204);
|
||||
response.end();
|
||||
}
|
||||
|
||||
if (request.url == "/remove") {
|
||||
this.removeAccessory();
|
||||
response.writeHead(204);
|
||||
response.end();
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
this.requestServer.listen(18081, function() {
|
||||
platform.log("Server Listening...");
|
||||
});
|
||||
|
||||
if (api) {
|
||||
// Save the API object as plugin needs to register new accessory via this object.
|
||||
this.api = api;
|
||||
|
||||
// Listen to event "didFinishLaunching", this means homebridge already finished loading cached accessories
|
||||
// Platform Plugin should only register new accessory that doesn't exist in homebridge after this event.
|
||||
// Or start discover new accessories
|
||||
this.api.on('didFinishLaunching', function() {
|
||||
platform.log("DidFinishLaunching");
|
||||
}.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
// Function invoked when homebridge tries to restore cached accessory
|
||||
// Developer can configure accessory at here (like setup event handler)
|
||||
// Update current value
|
||||
SamplePlatform.prototype.configureAccessory = function(accessory) {
|
||||
this.log(accessory.displayName, "Configure Accessory");
|
||||
var platform = this;
|
||||
|
||||
// set the accessory to reachable if plugin can currently process the accessory
|
||||
// otherwise set to false and update the reachability later by invoking
|
||||
// accessory.updateReachability()
|
||||
accessory.reachable = true;
|
||||
|
||||
accessory.on('identify', function(paired, callback) {
|
||||
platform.log(accessory.displayName, "Identify!!!");
|
||||
callback();
|
||||
});
|
||||
|
||||
if (accessory.getService(Service.Lightbulb)) {
|
||||
accessory.getService(Service.Lightbulb)
|
||||
.getCharacteristic(Characteristic.On)
|
||||
.on('set', function(value, callback) {
|
||||
platform.log(accessory.displayName, "Light -> " + value);
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
this.accessories.push(accessory);
|
||||
}
|
||||
|
||||
//Handler will be invoked when user try to config your plugin
|
||||
//Callback can be cached and invoke when nessary
|
||||
SamplePlatform.prototype.configurationRequestHandler = function(context, request, callback) {
|
||||
this.log("Context: ", JSON.stringify(context));
|
||||
this.log("Request: ", JSON.stringify(request));
|
||||
|
||||
// Check the request response
|
||||
if (request && request.response && request.response.inputs && request.response.inputs.name) {
|
||||
this.addAccessory(request.response.inputs.name);
|
||||
|
||||
// Invoke callback with config will let homebridge save the new config into config.json
|
||||
// Callback = function(response, type, replace, config)
|
||||
// set "type" to platform if the plugin is trying to modify platforms section
|
||||
// set "replace" to true will let homebridge replace existing config in config.json
|
||||
// "config" is the data platform trying to save
|
||||
callback(null, "platform", true, {"platform":"SamplePlatform", "otherConfig":"SomeData"});
|
||||
return;
|
||||
}
|
||||
|
||||
// - UI Type: Input
|
||||
// Can be used to request input from user
|
||||
// User response can be retrieved from request.response.inputs next time
|
||||
// when configurationRequestHandler being invoked
|
||||
|
||||
var respDict = {
|
||||
"type": "Interface",
|
||||
"interface": "input",
|
||||
"title": "Add Accessory",
|
||||
"items": [
|
||||
{
|
||||
"id": "name",
|
||||
"title": "Name",
|
||||
"placeholder": "Fancy Light"
|
||||
}//,
|
||||
// {
|
||||
// "id": "pw",
|
||||
// "title": "Password",
|
||||
// "secure": true
|
||||
// }
|
||||
]
|
||||
}
|
||||
|
||||
// - UI Type: List
|
||||
// Can be used to ask user to select something from the list
|
||||
// User response can be retrieved from request.response.selections next time
|
||||
// when configurationRequestHandler being invoked
|
||||
|
||||
// var respDict = {
|
||||
// "type": "Interface",
|
||||
// "interface": "list",
|
||||
// "title": "Select Something",
|
||||
// "allowMultipleSelection": true,
|
||||
// "items": [
|
||||
// "A","B","C"
|
||||
// ]
|
||||
// }
|
||||
|
||||
// - UI Type: Instruction
|
||||
// Can be used to ask user to do something (other than text input)
|
||||
// Hero image is base64 encoded image data. Not really sure the maximum length HomeKit allows.
|
||||
|
||||
// var respDict = {
|
||||
// "type": "Interface",
|
||||
// "interface": "instruction",
|
||||
// "title": "Almost There",
|
||||
// "detail": "Please press the button on the bridge to finish the setup.",
|
||||
// "heroImage": "base64 image data",
|
||||
// "showActivityIndicator": true,
|
||||
// "showNextButton": true,
|
||||
// "buttonText": "Login in browser",
|
||||
// "actionURL": "https://google.com"
|
||||
// }
|
||||
|
||||
// Plugin can set context to allow it track setup process
|
||||
context.ts = "Hello";
|
||||
|
||||
//invoke callback to update setup UI
|
||||
callback(respDict);
|
||||
}
|
||||
|
||||
// Sample function to show how developer can add accessory dynamically from outside event
|
||||
SamplePlatform.prototype.addAccessory = function(accessoryName) {
|
||||
this.log("Add Accessory");
|
||||
var platform = this;
|
||||
var uuid;
|
||||
|
||||
uuid = UUIDGen.generate(accessoryName);
|
||||
|
||||
var newAccessory = new Accessory(accessoryName, uuid);
|
||||
newAccessory.on('identify', function(paired, callback) {
|
||||
platform.log(accessory.displayName, "Identify!!!");
|
||||
callback();
|
||||
});
|
||||
// Plugin can save context on accessory
|
||||
// To help restore accessory in configureAccessory()
|
||||
// newAccessory.context.something = "Something"
|
||||
|
||||
newAccessory.addService(Service.Lightbulb, "Test Light")
|
||||
.getCharacteristic(Characteristic.On)
|
||||
.on('set', function(value, callback) {
|
||||
platform.log(accessory.displayName, "Light -> " + value);
|
||||
callback();
|
||||
});
|
||||
|
||||
this.accessories.push(newAccessory);
|
||||
this.api.registerPlatformAccessories("homebridge-samplePlatform", "SamplePlatform", [newAccessory]);
|
||||
}
|
||||
|
||||
SamplePlatform.prototype.updateAccessoriesReachability = function() {
|
||||
this.log("Update Reachability");
|
||||
for (var index in this.accessories) {
|
||||
var accessory = this.accessories[index];
|
||||
accessory.updateReachability(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Sample function to show how developer can remove accessory dynamically from outside event
|
||||
SamplePlatform.prototype.removeAccessory = function() {
|
||||
this.log("Remove Accessory");
|
||||
this.api.unregisterPlatformAccessories("homebridge-samplePlatform", "SamplePlatform", this.accessories);
|
||||
|
||||
this.accessories = [];
|
||||
}
|
||||
20
example-plugins/homebridge-samplePlatform/package.json
Normal file
20
example-plugins/homebridge-samplePlatform/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "homebridge-samplePlatform",
|
||||
"version": "0.0.1",
|
||||
"description": "Sample Platform plugin for homebridge: https://github.com/nfarina/homebridge",
|
||||
"license": "ISC",
|
||||
"keywords": [
|
||||
"homebridge-plugin"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/example/homebridge.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "http://github.com/example/homebridge/issues"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.12.0",
|
||||
"homebridge": ">=0.2.0"
|
||||
}
|
||||
}
|
||||
170
lib/api.js
Normal file
170
lib/api.js
Normal file
@@ -0,0 +1,170 @@
|
||||
var inherits = require('util').inherits;
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var hap = require("hap-nodejs");
|
||||
var hapLegacyTypes = require("hap-nodejs/accessories/types.js");
|
||||
var log = require("./logger")._system;
|
||||
var User = require("./user").User;
|
||||
var PlatformAccessory = require("./platformAccessory").PlatformAccessory;
|
||||
|
||||
// The official homebridge API is the object we feed the plugin's exported initializer function.
|
||||
|
||||
module.exports = {
|
||||
API: API
|
||||
}
|
||||
|
||||
function API() {
|
||||
this._accessories = {}; // this._accessories[pluginName.accessoryName] = accessory constructor
|
||||
this._platforms = {}; // this._platforms[pluginName.platformName] = platform constructor
|
||||
|
||||
this._configurableAccessories = {};
|
||||
this._dynamicPlatforms = {}; // this._dynamicPlatforms[pluginName.platformName] = platform constructor
|
||||
|
||||
// expose the homebridge API version
|
||||
this.version = 2.1;
|
||||
|
||||
// expose the User class methods to plugins to get paths. Example: homebridge.user.storagePath()
|
||||
this.user = User;
|
||||
|
||||
// expose HAP-NodeJS in its entirely for plugins to use instead of making Plugins
|
||||
// require() it as a dependency - it's a heavy dependency so we don't want it in
|
||||
// every single plugin.
|
||||
this.hap = hap;
|
||||
|
||||
// we also need to "bolt on" the legacy "types" constants for older accessories/platforms
|
||||
// still using the "object literal" style JSON.
|
||||
this.hapLegacyTypes = hapLegacyTypes;
|
||||
|
||||
this.platformAccessory = PlatformAccessory;
|
||||
}
|
||||
|
||||
inherits(API, EventEmitter);
|
||||
|
||||
API.prototype.accessory = function(name) {
|
||||
|
||||
// if you passed the "short form" name like "Lockitron" instead of "homebridge-lockitron.Lockitron",
|
||||
// see if it matches exactly one accessory.
|
||||
if (name.indexOf('.') == -1) {
|
||||
var found = [];
|
||||
for (var fullName in this._accessories) {
|
||||
if (fullName.split(".")[1] == name)
|
||||
found.push(fullName);
|
||||
}
|
||||
|
||||
if (found.length == 1) {
|
||||
return this._accessories[found[0]];
|
||||
}
|
||||
else if (found.length > 1) {
|
||||
throw new Error("The requested accessory '" + name + "' has been registered multiple times. Please be more specific by writing one of: " + found.join(", "));
|
||||
}
|
||||
else {
|
||||
throw new Error("The requested accessory '" + name + "' was not registered by any plugin.");
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
||||
if (!this._accessories[name])
|
||||
throw new Error("The requested accessory '" + name + "' was not registered by any plugin.");
|
||||
|
||||
return this._accessories[name];
|
||||
}
|
||||
}
|
||||
|
||||
API.prototype.registerAccessory = function(pluginName, accessoryName, constructor, configurationRequestHandler) {
|
||||
var fullName = pluginName + "." + accessoryName;
|
||||
|
||||
if (this._accessories[fullName])
|
||||
throw new Error("Attempting to register an accessory '" + fullName + "' which has already been registered!");
|
||||
|
||||
log.info("Registering accessory '%s'", fullName);
|
||||
|
||||
this._accessories[fullName] = constructor;
|
||||
|
||||
// The plugin supports configuration
|
||||
if (configurationRequestHandler) {
|
||||
this._configurableAccessories[fullName] = configurationRequestHandler;
|
||||
}
|
||||
}
|
||||
|
||||
API.prototype.publishCameraAccessories = function(pluginName, accessories) {
|
||||
for (var index in accessories) {
|
||||
var accessory = accessories[index];
|
||||
if (!(accessory instanceof PlatformAccessory)) {
|
||||
throw new Error(pluginName + " attempt to register an accessory that isn\'t PlatformAccessory!");
|
||||
}
|
||||
accessory._associatedPlugin = pluginName;
|
||||
}
|
||||
|
||||
this.emit('publishCameraAccessories', accessories);
|
||||
}
|
||||
|
||||
API.prototype.platform = function(name) {
|
||||
|
||||
// if you passed the "short form" name like "Lockitron" instead of "homebridge-lockitron.Lockitron",
|
||||
// see if it matches exactly one platform.
|
||||
if (name.indexOf('.') == -1) {
|
||||
var found = [];
|
||||
for (var fullName in this._platforms) {
|
||||
if (fullName.split(".")[1] == name)
|
||||
found.push(fullName);
|
||||
}
|
||||
|
||||
if (found.length == 1) {
|
||||
return this._platforms[found[0]];
|
||||
}
|
||||
else if (found.length > 1) {
|
||||
throw new Error("The requested platform '" + name + "' has been registered multiple times. Please be more specific by writing one of: " + found.join(", "));
|
||||
}
|
||||
else {
|
||||
throw new Error("The requested platform '" + name + "' was not registered by any plugin.");
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
||||
if (!this._platforms[name])
|
||||
throw new Error("The requested platform '" + name + "' was not registered by any plugin.");
|
||||
|
||||
return this._platforms[name];
|
||||
}
|
||||
}
|
||||
|
||||
API.prototype.registerPlatform = function(pluginName, platformName, constructor, dynamic) {
|
||||
var fullName = pluginName + "." + platformName;
|
||||
|
||||
if (this._platforms[fullName])
|
||||
throw new Error("Attempting to register a platform '" + fullName + "' which has already been registered!");
|
||||
|
||||
log.info("Registering platform '%s'", fullName);
|
||||
|
||||
this._platforms[fullName] = constructor;
|
||||
|
||||
if (dynamic) {
|
||||
this._dynamicPlatforms[fullName] = constructor;
|
||||
}
|
||||
}
|
||||
|
||||
API.prototype.registerPlatformAccessories = function(pluginName, platformName, accessories) {
|
||||
for (var index in accessories) {
|
||||
var accessory = accessories[index];
|
||||
if (!(accessory instanceof PlatformAccessory)) {
|
||||
throw new Error(pluginName + " - " + platformName + " attempt to register an accessory that isn\'t PlatformAccessory!");
|
||||
}
|
||||
accessory._associatedPlugin = pluginName;
|
||||
accessory._associatedPlatform = platformName;
|
||||
}
|
||||
|
||||
this.emit('registerPlatformAccessories', accessories);
|
||||
}
|
||||
|
||||
API.prototype.updatePlatformAccessories = function(accessories) {
|
||||
this.emit('updatePlatformAccessories', accessories);
|
||||
}
|
||||
|
||||
API.prototype.unregisterPlatformAccessories = function(pluginName, platformName, accessories) {
|
||||
for (var index in accessories) {
|
||||
var accessory = accessories[index];
|
||||
if (!(accessory instanceof PlatformAccessory)) {
|
||||
throw new Error(pluginName + " - " + platformName + " attempt to unregister an accessory that isn\'t PlatformAccessory!");
|
||||
}
|
||||
}
|
||||
this.emit('unregisterPlatformAccessories', accessories);
|
||||
}
|
||||
96
lib/bridgeSetupManager.js
Normal file
96
lib/bridgeSetupManager.js
Normal file
@@ -0,0 +1,96 @@
|
||||
var inherits = require('util').inherits;
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var Service = require("hap-nodejs").Service;
|
||||
var Characteristic = require("hap-nodejs").Characteristic;
|
||||
var SetupSession = require("./bridgeSetupSession").SetupSession;
|
||||
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
BridgeSetupManager: BridgeSetupManager
|
||||
}
|
||||
|
||||
function BridgeSetupManager() {
|
||||
this.session;
|
||||
|
||||
this.service = new Service(null, "49FB9D4D-0FEA-4BF1-8FA6-E7B18AB86DCE");
|
||||
|
||||
this.stateCharacteristic = new Characteristic("State", "77474A2F-FA98-485E-97BE-4762458774D8", {
|
||||
format: Characteristic.Formats.UINT8,
|
||||
minValue: 0,
|
||||
maxValue: 1,
|
||||
minStep: 1,
|
||||
perms: [Characteristic.Perms.READ, Characteristic.Perms.NOTIFY]
|
||||
});
|
||||
this.stateCharacteristic.value = 0;
|
||||
this.service.addCharacteristic(this.stateCharacteristic);
|
||||
|
||||
this.versionCharacteristic = new Characteristic("Version", "FD9FE4CC-D06F-4FFE-96C6-595D464E1026", {
|
||||
format: Characteristic.Formats.STRING,
|
||||
perms: [Characteristic.Perms.READ, Characteristic.Perms.NOTIFY]
|
||||
});
|
||||
this.versionCharacteristic.value = "1.0";
|
||||
this.service.addCharacteristic(this.versionCharacteristic);
|
||||
|
||||
this.controlPointCharacteristic = new Characteristic("Control Point", "5819A4C2-E1B0-4C9D-B761-3EB1AFF43073", {
|
||||
format: Characteristic.Formats.DATA,
|
||||
perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY]
|
||||
})
|
||||
this.controlPointCharacteristic.on('get', function(callback, context) {
|
||||
this.handleReadRequest(callback, context);
|
||||
}.bind(this));
|
||||
this.controlPointCharacteristic.on('set', function(newValue, callback, context) {
|
||||
this.handleWriteRequest(newValue, callback, context);
|
||||
}.bind(this));
|
||||
|
||||
this.controlPointCharacteristic.value = null;
|
||||
this.service.addCharacteristic(this.controlPointCharacteristic);
|
||||
}
|
||||
|
||||
inherits(BridgeSetupManager, EventEmitter);
|
||||
|
||||
BridgeSetupManager.prototype.handleReadRequest = function(callback, context) {
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.session) {
|
||||
callback(null, null);
|
||||
} else {
|
||||
this.session.handleReadRequest(callback);
|
||||
}
|
||||
}
|
||||
|
||||
BridgeSetupManager.prototype.handleWriteRequest = function(value, callback, context) {
|
||||
if (!context) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
var data = new Buffer(value, 'base64');
|
||||
var request = JSON.parse(data.toString());
|
||||
callback();
|
||||
|
||||
if (!this.session || this.session.sessionUUID !== request.sid) {
|
||||
if (this.session) {
|
||||
this.session.removeAllListeners();
|
||||
this.session.validSession = false;
|
||||
}
|
||||
|
||||
this.session = new SetupSession(this.stateCharacteristic, this.controlPointCharacteristic);
|
||||
this.session.configurablePlatformPlugins = this.configurablePlatformPlugins;
|
||||
this.session.on('newConfig', function(type, name, replace, config) {
|
||||
this.emit('newConfig', type, name, replace, config);
|
||||
}.bind(this));
|
||||
|
||||
this.session.on('requestCurrentConfig', function(callback) {
|
||||
this.emit('requestCurrentConfig', callback);
|
||||
}.bind(this));
|
||||
|
||||
this.session.on('end', function() {
|
||||
this.session = null;
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
this.session.handleWriteRequest(request);
|
||||
}
|
||||
191
lib/bridgeSetupSession.js
Normal file
191
lib/bridgeSetupSession.js
Normal file
File diff suppressed because one or more lines are too long
41
lib/cli.js
Normal file
41
lib/cli.js
Normal file
@@ -0,0 +1,41 @@
|
||||
var program = require('commander');
|
||||
var hap = require("hap-nodejs");
|
||||
var version = require('./version');
|
||||
var Server = require('./server').Server;
|
||||
var Plugin = require('./plugin').Plugin;
|
||||
var User = require('./user').User;
|
||||
var log = require("./logger")._system;
|
||||
|
||||
'use strict';
|
||||
|
||||
module.exports = function() {
|
||||
|
||||
var insecureAccess = false;
|
||||
|
||||
program
|
||||
.version(version)
|
||||
.option('-P, --plugin-path [path]', 'look for plugins installed at [path] as well as the default locations ([path] can also point to a single plugin)', function(p) { Plugin.addPluginPath(p); })
|
||||
.option('-U, --user-storage-path [path]', 'look for homebridge user files at [path] instead of the default location (~/.homebridge)', function(p) { User.setStoragePath(p); })
|
||||
.option('-D, --debug', 'turn on debug level logging', function() { require('./logger').setDebugEnabled(true) })
|
||||
.option('-I, --insecure', 'allow unauthenticated requests (for easier hacking)', function() { insecureAccess = true })
|
||||
.parse(process.argv);
|
||||
|
||||
// Initialize HAP-NodeJS with a custom persist directory
|
||||
hap.init(User.persistPath());
|
||||
|
||||
var server = new Server(insecureAccess);
|
||||
|
||||
var signals = { 'SIGINT': 2, 'SIGTERM': 15 };
|
||||
Object.keys(signals).forEach(function (signal) {
|
||||
process.on(signal, function () {
|
||||
log.info("Got %s, shutting down Homebridge...", signal);
|
||||
|
||||
// Save cached accessories to persist storage.
|
||||
server._updateCachedAccessories();
|
||||
|
||||
process.exit(128 + signals[signal]);
|
||||
});
|
||||
});
|
||||
|
||||
server.run();
|
||||
}
|
||||
92
lib/logger.js
Normal file
92
lib/logger.js
Normal file
@@ -0,0 +1,92 @@
|
||||
var chalk = require('chalk');
|
||||
var util = require('util');
|
||||
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
Logger: Logger,
|
||||
setDebugEnabled: setDebugEnabled,
|
||||
_system: new Logger() // system logger, for internal use only
|
||||
}
|
||||
|
||||
var DEBUG_ENABLED = false;
|
||||
|
||||
// Turns on debug level logging
|
||||
function setDebugEnabled(enabled) {
|
||||
DEBUG_ENABLED = enabled;
|
||||
}
|
||||
|
||||
// global cache of logger instances by plugin name
|
||||
var loggerCache = {};
|
||||
|
||||
/**
|
||||
* Logger class
|
||||
*/
|
||||
|
||||
function Logger(prefix) {
|
||||
this.prefix = prefix;
|
||||
}
|
||||
|
||||
Logger.prototype.debug = function(msg) {
|
||||
if (DEBUG_ENABLED)
|
||||
this.log.apply(this, ['debug'].concat(Array.prototype.slice.call(arguments)));
|
||||
}
|
||||
|
||||
Logger.prototype.info = function(msg) {
|
||||
this.log.apply(this, ['info'].concat(Array.prototype.slice.call(arguments)));
|
||||
}
|
||||
|
||||
Logger.prototype.warn = function(msg) {
|
||||
this.log.apply(this, ['warn'].concat(Array.prototype.slice.call(arguments)));
|
||||
}
|
||||
|
||||
Logger.prototype.error = function(msg) {
|
||||
this.log.apply(this, ['error'].concat(Array.prototype.slice.call(arguments)));
|
||||
}
|
||||
|
||||
Logger.prototype.log = function(level, msg) {
|
||||
|
||||
msg = util.format.apply(util, Array.prototype.slice.call(arguments, 1));
|
||||
func = console.log;
|
||||
|
||||
if (level == 'debug') {
|
||||
msg = chalk.gray(msg);
|
||||
}
|
||||
else if (level == 'warn') {
|
||||
msg = chalk.yellow(msg);
|
||||
func = console.error;
|
||||
}
|
||||
else if (level == 'error') {
|
||||
msg = chalk.bold.red(msg);
|
||||
func = console.error;
|
||||
}
|
||||
|
||||
// prepend prefix if applicable
|
||||
if (this.prefix)
|
||||
msg = chalk.cyan("[" + this.prefix + "]") + " " + msg;
|
||||
|
||||
// prepend timestamp
|
||||
var date = new Date();
|
||||
msg = "[" + date.toLocaleString() + "]" + " " + msg;
|
||||
|
||||
func(msg);
|
||||
}
|
||||
|
||||
Logger.withPrefix = function(prefix) {
|
||||
|
||||
if (!loggerCache[prefix]) {
|
||||
// create a class-like logger thing that acts as a function as well
|
||||
// as an instance of Logger.
|
||||
var logger = new Logger(prefix);
|
||||
var log = logger.info.bind(logger);
|
||||
log.debug = logger.debug;
|
||||
log.info = logger.info;
|
||||
log.warn = logger.warn;
|
||||
log.error = logger.error;
|
||||
log.log = logger.log;
|
||||
log.prefix = logger.prefix;
|
||||
loggerCache[prefix] = log;
|
||||
}
|
||||
|
||||
return loggerCache[prefix];
|
||||
}
|
||||
226
lib/platformAccessory.js
Normal file
226
lib/platformAccessory.js
Normal file
@@ -0,0 +1,226 @@
|
||||
var uuid = require("hap-nodejs").uuid;
|
||||
var Accessory = require("hap-nodejs").Accessory;
|
||||
var Service = require("hap-nodejs").Service;
|
||||
var Characteristic = require("hap-nodejs").Characteristic;
|
||||
var inherits = require('util').inherits;
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
PlatformAccessory: PlatformAccessory
|
||||
}
|
||||
|
||||
function PlatformAccessory(displayName, UUID, category) {
|
||||
if (!displayName) throw new Error("Accessories must be created with a non-empty displayName.");
|
||||
if (!UUID) throw new Error("Accessories must be created with a valid UUID.");
|
||||
if (!uuid.isValid(UUID)) throw new Error("UUID '" + UUID + "' is not a valid UUID. Try using the provided 'generateUUID' function to create a valid UUID from any arbitrary string, like a serial number.");
|
||||
|
||||
this.displayName = displayName;
|
||||
this.UUID = UUID;
|
||||
this.category = category || Accessory.Categories.OTHER;
|
||||
this.services = [];
|
||||
this.reachable = false;
|
||||
this.context = {};
|
||||
|
||||
this._associatedPlugin;
|
||||
this._associatedPlatform;
|
||||
this._associatedHAPAccessory;
|
||||
|
||||
this
|
||||
.addService(Service.AccessoryInformation)
|
||||
.setCharacteristic(Characteristic.Name, displayName)
|
||||
.setCharacteristic(Characteristic.Manufacturer, "Default-Manufacturer")
|
||||
.setCharacteristic(Characteristic.Model, "Default-Model")
|
||||
.setCharacteristic(Characteristic.SerialNumber, "Default-SerialNumber");
|
||||
}
|
||||
|
||||
inherits(PlatformAccessory, EventEmitter);
|
||||
|
||||
PlatformAccessory.prototype.addService = function(service) {
|
||||
// service might be a constructor like `Service.AccessoryInformation` instead of an instance
|
||||
// of Service. Coerce if necessary.
|
||||
if (typeof service === 'function')
|
||||
service = new (Function.prototype.bind.apply(service, arguments));
|
||||
|
||||
// check for UUID+subtype conflict
|
||||
for (var index in this.services) {
|
||||
var existing = this.services[index];
|
||||
if (existing.UUID === service.UUID) {
|
||||
// OK we have two Services with the same UUID. Check that each defines a `subtype` property and that each is unique.
|
||||
if (!service.subtype)
|
||||
throw new Error("Cannot add a Service with the same UUID '" + existing.UUID + "' as another Service in this Accessory without also defining a unique 'subtype' property.");
|
||||
|
||||
if (service.subtype.toString() === existing.subtype.toString())
|
||||
throw new Error("Cannot add a Service with the same UUID '" + existing.UUID + "' and subtype '" + existing.subtype + "' as another Service in this Accessory.");
|
||||
}
|
||||
}
|
||||
|
||||
this.services.push(service);
|
||||
|
||||
if (this._associatedHAPAccessory) {
|
||||
this._associatedHAPAccessory.addService(service);
|
||||
}
|
||||
return service;
|
||||
}
|
||||
|
||||
PlatformAccessory.prototype.removeService = function(service) {
|
||||
var targetServiceIndex;
|
||||
|
||||
for (var index in this.services) {
|
||||
var existingService = this.services[index];
|
||||
|
||||
if (existingService === service) {
|
||||
targetServiceIndex = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetServiceIndex) {
|
||||
this.services.splice(targetServiceIndex, 1);
|
||||
service.removeAllListeners();
|
||||
|
||||
if (this._associatedHAPAccessory) {
|
||||
this._associatedHAPAccessory.removeService(service);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* searchs for a Service in the services collection and returns the first Service object that matches.
|
||||
* If multiple services of the same type are present in one accessory, use getServiceByUUIDAndSubType instead.
|
||||
* @param {ServiceConstructor|string} name
|
||||
* @returns Service
|
||||
*/
|
||||
PlatformAccessory.prototype.getService = function(name) {
|
||||
for (var index in this.services) {
|
||||
var service = this.services[index];
|
||||
|
||||
if (typeof name === 'string' && (service.displayName === name || service.name === name))
|
||||
return service;
|
||||
else if (typeof name === 'function' && ((service instanceof name) || (name.UUID === service.UUID)))
|
||||
return service;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* searchs for a Service in the services collection and returns the first Service object that matches.
|
||||
* If multiple services of the same type are present in one accessory, use getServiceByUUIDAndSubType instead.
|
||||
* @param {string} UUID Can be an UUID, a service.displayName, or a constructor of a Service
|
||||
* @param {string} subtype A subtype string to match
|
||||
* @returns Service
|
||||
*/
|
||||
PlatformAccessory.prototype.getServiceByUUIDAndSubType = function(UUID, subtype) {
|
||||
for (var index in this.services) {
|
||||
var service = this.services[index];
|
||||
|
||||
if (typeof UUID === 'string' && (service.displayName === UUID || service.name === UUID) && service.subtype === subtype )
|
||||
return service;
|
||||
else if (typeof UUID === 'function' && ((service instanceof UUID) || (UUID.UUID === service.UUID)) && service.subtype === subtype)
|
||||
return service;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
PlatformAccessory.prototype.updateReachability = function(reachable) {
|
||||
this.reachable = reachable;
|
||||
|
||||
if (this._associatedHAPAccessory) {
|
||||
this._associatedHAPAccessory.updateReachability(reachable);
|
||||
}
|
||||
}
|
||||
|
||||
PlatformAccessory.prototype.configureCameraSource = function(cameraSource) {
|
||||
this.cameraSource = cameraSource;
|
||||
for (var index in cameraSource.services) {
|
||||
var service = cameraSource.services[index];
|
||||
this.addService(service);
|
||||
}
|
||||
}
|
||||
|
||||
PlatformAccessory.prototype._prepareAssociatedHAPAccessory = function () {
|
||||
this._associatedHAPAccessory = new Accessory(this.displayName, this.UUID);
|
||||
|
||||
if (this.cameraSource) {
|
||||
this._associatedHAPAccessory.configureCameraSource(this.cameraSource);
|
||||
}
|
||||
|
||||
this._associatedHAPAccessory._sideloadServices(this.services);
|
||||
this._associatedHAPAccessory.category = this.category;
|
||||
this._associatedHAPAccessory.reachable = this.reachable;
|
||||
this._associatedHAPAccessory.on('identify', function(paired, callback) {
|
||||
if (this.listeners('identify').length > 0) {
|
||||
// allow implementors to identify this Accessory in whatever way is appropriate, and pass along
|
||||
// the standard callback for completion.
|
||||
this.emit('identify', paired, callback);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
PlatformAccessory.prototype._dictionaryPresentation = function() {
|
||||
var accessory = {};
|
||||
|
||||
accessory.plugin = this._associatedPlugin;
|
||||
accessory.platform = this._associatedPlatform;
|
||||
accessory.displayName = this.displayName;
|
||||
accessory.UUID = this.UUID;
|
||||
accessory.category = this.category;
|
||||
accessory.context = this.context;
|
||||
|
||||
var services = [];
|
||||
for (var index in this.services) {
|
||||
var service = this.services[index];
|
||||
var servicePresentation = {};
|
||||
servicePresentation.displayName = service.displayName;
|
||||
servicePresentation.UUID = service.UUID;
|
||||
servicePresentation.subtype = service.subtype;
|
||||
|
||||
var characteristics = [];
|
||||
for (var cIndex in service.characteristics) {
|
||||
var characteristic = service.characteristics[cIndex];
|
||||
var characteristicPresentation = {};
|
||||
characteristicPresentation.displayName = characteristic.displayName;
|
||||
characteristicPresentation.UUID = characteristic.UUID;
|
||||
characteristicPresentation.props = characteristic.props;
|
||||
characteristicPresentation.value = characteristic.value;
|
||||
characteristics.push(characteristicPresentation);
|
||||
}
|
||||
|
||||
servicePresentation.characteristics = characteristics;
|
||||
services.push(servicePresentation);
|
||||
}
|
||||
|
||||
accessory.services = services;
|
||||
return accessory;
|
||||
}
|
||||
|
||||
PlatformAccessory.prototype._configFromData = function(data) {
|
||||
this._associatedPlugin = data.plugin;
|
||||
this._associatedPlatform = data.platform;
|
||||
this.displayName = data.displayName;
|
||||
this.UUID = data.UUID;
|
||||
this.category = data.category;
|
||||
this.context = data.context;
|
||||
this.reachable = false;
|
||||
|
||||
var services = [];
|
||||
for (var index in data.services) {
|
||||
var service = data.services[index];
|
||||
var hapService = new Service(service.displayName, service.UUID, service.subtype);
|
||||
|
||||
var characteristics = [];
|
||||
for (var cIndex in service.characteristics) {
|
||||
var characteristic = service.characteristics[cIndex];
|
||||
var hapCharacteristic = new Characteristic(characteristic.displayName, characteristic.UUID, characteristic.props);
|
||||
hapCharacteristic.value = characteristic.value;
|
||||
characteristics.push(hapCharacteristic);
|
||||
}
|
||||
|
||||
hapService._sideloadCharacteristics(characteristics);
|
||||
services.push(hapService);
|
||||
}
|
||||
|
||||
this.services = services;
|
||||
}
|
||||
203
lib/plugin.js
Normal file
203
lib/plugin.js
Normal file
@@ -0,0 +1,203 @@
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
var semver = require('semver');
|
||||
var User = require('./user').User;
|
||||
var version = require('./version');
|
||||
var log = require("./logger")._system;
|
||||
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
Plugin: Plugin
|
||||
}
|
||||
|
||||
/**
|
||||
* Homebridge Plugin.
|
||||
*
|
||||
* Allows for discovering and loading installed Homebridge plugins.
|
||||
*/
|
||||
|
||||
function Plugin(pluginPath) {
|
||||
this.pluginPath = pluginPath; // like "/usr/local/lib/node_modules/homebridge-lockitron"
|
||||
this.initializer; // exported function from the plugin that initializes it
|
||||
}
|
||||
|
||||
Plugin.prototype.name = function() {
|
||||
return path.basename(this.pluginPath);
|
||||
}
|
||||
|
||||
Plugin.prototype.load = function(options) {
|
||||
options = options || {};
|
||||
|
||||
// does this plugin exist at all?
|
||||
if (!fs.existsSync(this.pluginPath)) {
|
||||
throw new Error("Plugin " + this.pluginPath + " was not found. Make sure the module '" + this.pluginPath + "' is installed.");
|
||||
}
|
||||
|
||||
// attempt to load package.json
|
||||
var pjson = Plugin.loadPackageJSON(this.pluginPath);
|
||||
|
||||
// very temporary fix for first wave of plugins
|
||||
if (pjson.peerDepdendencies && (!pjson.engines || !pjson.engines.homebridge)) {
|
||||
var engines = pjson.engines || {}
|
||||
engines.homebridge = pjson.peerDepdendencies.homebridge;
|
||||
pjson.engines = engines;
|
||||
}
|
||||
|
||||
// pluck out the HomeBridge version requirement
|
||||
if (!pjson.engines || !pjson.engines.homebridge) {
|
||||
throw new Error("Plugin " + this.pluginPath + " does not contain the 'homebridge' package in 'engines'.");
|
||||
}
|
||||
|
||||
var versionRequired = pjson.engines.homebridge;
|
||||
|
||||
// make sure the version is satisfied by the currently running version of HomeBridge
|
||||
if (!semver.satisfies(version, versionRequired)) {
|
||||
throw new Error("Plugin " + this.pluginPath + " requires a HomeBridge version of " + versionRequired + " which does not satisfy the current HomeBridge version of " + version + ". You may need to upgrade your installation of HomeBridge.");
|
||||
}
|
||||
|
||||
// figure out the main module - index.js unless otherwise specified
|
||||
var main = pjson.main || "./index.js";
|
||||
|
||||
var mainPath = path.join(this.pluginPath, main);
|
||||
|
||||
// try to require() it and grab the exported initialization hook
|
||||
this.initializer = require(mainPath);
|
||||
}
|
||||
|
||||
Plugin.loadPackageJSON = function(pluginPath) {
|
||||
// check for a package.json
|
||||
var pjsonPath = path.join(pluginPath, "package.json");
|
||||
var pjson = null;
|
||||
|
||||
if (!fs.existsSync(pjsonPath)) {
|
||||
throw new Error("Plugin " + pluginPath + " does not contain a package.json.");
|
||||
}
|
||||
|
||||
try {
|
||||
// attempt to parse package.json
|
||||
pjson = JSON.parse(fs.readFileSync(pjsonPath));
|
||||
}
|
||||
catch (err) {
|
||||
throw new Error("Plugin " + pluginPath + " contains an invalid package.json. Error: " + err);
|
||||
}
|
||||
|
||||
// make sure the name is prefixed with 'homebridge-'
|
||||
if (!pjson.name || pjson.name.indexOf('homebridge-') != 0) {
|
||||
throw new Error("Plugin " + pluginPath + " does not have a package name that begins with 'homebridge-'.");
|
||||
}
|
||||
|
||||
// verify that it's tagged with the correct keyword
|
||||
if (!pjson.keywords || pjson.keywords.indexOf("homebridge-plugin") == -1) {
|
||||
throw new Error("Plugin " + pluginPath + " package.json does not contain the keyword 'homebridge-plugin'.");
|
||||
}
|
||||
|
||||
return pjson;
|
||||
}
|
||||
|
||||
Plugin.getDefaultPaths = function() {
|
||||
var win32 = process.platform === 'win32';
|
||||
var paths = [];
|
||||
|
||||
// add the paths used by require()
|
||||
paths = paths.concat(require.main.paths);
|
||||
|
||||
// THIS SECTION FROM: https://github.com/yeoman/environment/blob/master/lib/resolver.js
|
||||
|
||||
// Adding global npm directories
|
||||
// We tried using npm to get the global modules path, but it haven't work out
|
||||
// because of bugs in the parseable implementation of `ls` command and mostly
|
||||
// performance issues. So, we go with our best bet for now.
|
||||
if (process.env.NODE_PATH) {
|
||||
paths = process.env.NODE_PATH.split(path.delimiter)
|
||||
.filter(function(p) { return !!p; }) // trim out empty values
|
||||
.concat(paths);
|
||||
} else {
|
||||
// Default paths for each system
|
||||
if (win32) {
|
||||
paths.push(path.join(process.env.APPDATA, 'npm/node_modules'));
|
||||
} else {
|
||||
paths.push('/usr/local/lib/node_modules');
|
||||
paths.push('/usr/lib/node_modules');
|
||||
}
|
||||
}
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
// All search paths we will use to discover installed plugins
|
||||
Plugin.paths = Plugin.getDefaultPaths();
|
||||
|
||||
Plugin.addPluginPath = function(pluginPath) {
|
||||
Plugin.paths.unshift(path.resolve(process.cwd(), pluginPath));
|
||||
}
|
||||
|
||||
// Gets all plugins installed on the local system
|
||||
Plugin.installed = function() {
|
||||
|
||||
var plugins = [];
|
||||
var pluginsByName = {}; // don't add duplicate plugins
|
||||
var searchedPaths = {}; // don't search the same paths twice
|
||||
|
||||
// search for plugins among all known paths, in order
|
||||
for (var index in Plugin.paths) {
|
||||
var requirePath = Plugin.paths[index];
|
||||
|
||||
// did we already search this path?
|
||||
if (searchedPaths[requirePath])
|
||||
continue;
|
||||
|
||||
searchedPaths[requirePath] = true;
|
||||
|
||||
// just because this path is in require.main.paths doesn't mean it necessarily exists!
|
||||
if (!fs.existsSync(requirePath))
|
||||
continue;
|
||||
|
||||
var names = fs.readdirSync(requirePath);
|
||||
|
||||
// does this path point inside a single plugin and not a directory containing plugins?
|
||||
if (fs.existsSync(path.join(requirePath, "package.json")))
|
||||
names = [""];
|
||||
|
||||
// read through each directory in this node_modules folder
|
||||
for (var index2 in names) {
|
||||
var name = names[index2];
|
||||
|
||||
// reconstruct full path
|
||||
var pluginPath = path.join(requirePath, name);
|
||||
|
||||
// we only care about directories
|
||||
if (!fs.statSync(pluginPath).isDirectory()) continue;
|
||||
|
||||
// does this module contain a package.json?
|
||||
var pjson;
|
||||
try {
|
||||
// throws an Error if this isn't a homebridge plugin
|
||||
pjson = Plugin.loadPackageJSON(pluginPath);
|
||||
}
|
||||
catch (err) {
|
||||
// is this "trying" to be a homebridge plugin? if so let you know what went wrong.
|
||||
if (!name || name.indexOf('homebridge-') == 0) {
|
||||
log.warn(err.message);
|
||||
}
|
||||
|
||||
// skip this module
|
||||
continue;
|
||||
}
|
||||
|
||||
// get actual name if this path points inside a single plugin
|
||||
if (!name) name = pjson.name;
|
||||
|
||||
// add it to the return list
|
||||
if (!pluginsByName[name]) {
|
||||
pluginsByName[name] = pluginPath;
|
||||
plugins.push(new Plugin(pluginPath));
|
||||
}
|
||||
else {
|
||||
log.warn("Warning: skipping plugin found at '" + pluginPath + "' since we already loaded the same plugin from '" + pluginsByName[name] + "'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return plugins;
|
||||
}
|
||||
586
lib/server.js
Normal file
586
lib/server.js
Normal file
@@ -0,0 +1,586 @@
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
var uuid = require("hap-nodejs").uuid;
|
||||
var accessoryStorage = require('node-persist').create();
|
||||
var Bridge = require("hap-nodejs").Bridge;
|
||||
var Accessory = require("hap-nodejs").Accessory;
|
||||
var Service = require("hap-nodejs").Service;
|
||||
var Characteristic = require("hap-nodejs").Characteristic;
|
||||
var AccessoryLoader = require("hap-nodejs").AccessoryLoader;
|
||||
var once = require("hap-nodejs/lib/util/once").once;
|
||||
var Plugin = require('./plugin').Plugin;
|
||||
var User = require('./user').User;
|
||||
var API = require('./api').API;
|
||||
var PlatformAccessory = require("./platformAccessory").PlatformAccessory;
|
||||
var BridgeSetupManager = require("./bridgeSetupManager").BridgeSetupManager;
|
||||
var log = require("./logger")._system;
|
||||
var Logger = require('./logger').Logger;
|
||||
var mac = require("./util/mac");
|
||||
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
Server: Server
|
||||
}
|
||||
|
||||
function Server(insecureAccess) {
|
||||
// Setup Accessory Cache Storage
|
||||
accessoryStorage.initSync({ dir: User.cachedAccessoryPath() });
|
||||
|
||||
this._api = new API(); // object we feed to Plugins
|
||||
|
||||
this._api.on('registerPlatformAccessories', function(accessories) {
|
||||
this._handleRegisterPlatformAccessories(accessories);
|
||||
}.bind(this));
|
||||
|
||||
this._api.on('updatePlatformAccessories', function(accessories) {
|
||||
this._handleUpdatePlatformAccessories(accessories);
|
||||
}.bind(this));
|
||||
|
||||
this._api.on('unregisterPlatformAccessories', function(accessories) {
|
||||
this._handleUnregisterPlatformAccessories(accessories);
|
||||
}.bind(this));
|
||||
|
||||
this._api.on('publishCameraAccessories', function(accessories) {
|
||||
this._handlePublishCameraAccessories(accessories);
|
||||
}.bind(this));
|
||||
|
||||
this._plugins = this._loadPlugins(); // plugins[name] = Plugin instance
|
||||
this._config = this._loadConfig();
|
||||
this._cachedPlatformAccessories = this._loadCachedPlatformAccessories();
|
||||
this._bridge = this._createBridge();
|
||||
|
||||
this._activeDynamicPlugins = {};
|
||||
this._configurablePlatformPlugins = {};
|
||||
this._publishedCameras = {};
|
||||
this._setupManager = new BridgeSetupManager();
|
||||
this._setupManager.on('newConfig', this._handleNewConfig.bind(this));
|
||||
|
||||
this._setupManager.on('requestCurrentConfig', function(callback) {
|
||||
callback(this._config);
|
||||
}.bind(this));
|
||||
|
||||
// Server is "secure by default", meaning it creates a top-level Bridge accessory that
|
||||
// will not allow unauthenticated requests. This matches the behavior of actual HomeKit
|
||||
// accessories. However you can set this to true to allow all requests without authentication,
|
||||
// which can be useful for easy hacking. Note that this will expose all functions of your
|
||||
// bridged accessories, like changing charactersitics (i.e. flipping your lights on and off).
|
||||
this._allowInsecureAccess = insecureAccess || false;
|
||||
}
|
||||
|
||||
Server.prototype.run = function() {
|
||||
|
||||
// keep track of async calls we're waiting for callbacks on before we can start up
|
||||
this._asyncCalls = 0;
|
||||
this._asyncWait = true;
|
||||
|
||||
if (this._config.platforms) this._loadPlatforms();
|
||||
if (this._config.accessories) this._loadAccessories();
|
||||
this._loadDynamicPlatforms();
|
||||
this._configCachedPlatformAccessories();
|
||||
this._setupManager.configurablePlatformPlugins = this._configurablePlatformPlugins;
|
||||
this._bridge.addService(this._setupManager.service);
|
||||
|
||||
this._asyncWait = false;
|
||||
|
||||
// publish now unless we're waiting on anyone
|
||||
if (this._asyncCalls == 0)
|
||||
this._publish();
|
||||
|
||||
this._api.emit('didFinishLaunching');
|
||||
}
|
||||
|
||||
Server.prototype._publish = function() {
|
||||
// pull out our custom Bridge settings from config.json, if any
|
||||
var bridgeConfig = this._config.bridge || {};
|
||||
|
||||
var info = this._bridge.getService(Service.AccessoryInformation);
|
||||
if (bridgeConfig.manufacturer)
|
||||
info.setCharacteristic(Characteristic.Manufacturer, bridgeConfig.manufacturer);
|
||||
if (bridgeConfig.model)
|
||||
info.setCharacteristic(Characteristic.Model, bridgeConfig.model);
|
||||
if (bridgeConfig.serialNumber)
|
||||
info.setCharacteristic(Characteristic.SerialNumber, bridgeConfig.serialNumber);
|
||||
|
||||
this._printPin(bridgeConfig.pin);
|
||||
|
||||
this._bridge.on('listening', function(port) {
|
||||
log.info("Homebridge is running on port %s.", port);
|
||||
});
|
||||
|
||||
this._bridge.publish({
|
||||
username: bridgeConfig.username || "CC:22:3D:E3:CE:30",
|
||||
port: bridgeConfig.port || 0,
|
||||
pincode: bridgeConfig.pin || "031-45-154",
|
||||
category: Accessory.Categories.BRIDGE
|
||||
}, this._allowInsecureAccess);
|
||||
}
|
||||
|
||||
Server.prototype._loadPlugins = function(accessories, platforms) {
|
||||
|
||||
var plugins = {};
|
||||
var foundOnePlugin = false;
|
||||
|
||||
// load and validate plugins - check for valid package.json, etc.
|
||||
Plugin.installed().forEach(function(plugin) {
|
||||
|
||||
// attempt to load it
|
||||
try {
|
||||
plugin.load();
|
||||
}
|
||||
catch (err) {
|
||||
log.error("====================")
|
||||
log.error("ERROR LOADING PLUGIN " + plugin.name() + ":")
|
||||
log.error(err.stack);
|
||||
log.error("====================")
|
||||
plugin.loadError = err;
|
||||
}
|
||||
|
||||
if (!plugin.loadError) {
|
||||
|
||||
// add it to our dict for easy lookup later
|
||||
plugins[plugin.name()] = plugin;
|
||||
|
||||
log.info("Loaded plugin: " + plugin.name());
|
||||
|
||||
// call the plugin's initializer and pass it our API instance
|
||||
plugin.initializer(this._api);
|
||||
|
||||
log.info("---");
|
||||
foundOnePlugin = true;
|
||||
}
|
||||
|
||||
}.bind(this));
|
||||
|
||||
// Complain if you don't have any plugins.
|
||||
if (!foundOnePlugin) {
|
||||
log.warn("No plugins found. See the README for information on installing plugins.")
|
||||
}
|
||||
|
||||
return plugins;
|
||||
}
|
||||
|
||||
Server.prototype._loadConfig = function() {
|
||||
|
||||
// Look for the configuration file
|
||||
var configPath = User.configPath();
|
||||
|
||||
// Complain and exit if it doesn't exist yet
|
||||
if (!fs.existsSync(configPath)) {
|
||||
log.warn("config.json (%s) not found.", configPath);
|
||||
|
||||
var config = {};
|
||||
|
||||
config.bridge = {
|
||||
"name": "Homebridge",
|
||||
"username": "CC:22:3D:E3:CE:30",
|
||||
"pin": "031-45-154"
|
||||
};
|
||||
|
||||
return config;
|
||||
// log.error("Couldn't find a config.json file at '"+configPath+"'. Look at config-sample.json for examples of how to format your config.js and add your home accessories.");
|
||||
// process.exit(1);
|
||||
}
|
||||
|
||||
// Load up the configuration file
|
||||
var config;
|
||||
try {
|
||||
config = JSON.parse(fs.readFileSync(configPath));
|
||||
}
|
||||
catch (err) {
|
||||
log.error("There was a problem reading your config.json file.");
|
||||
log.error("Please try pasting your config.json file here to validate it: http://jsonlint.com");
|
||||
log.error("");
|
||||
throw err;
|
||||
}
|
||||
|
||||
var accessoryCount = (config.accessories && config.accessories.length) || 0;
|
||||
|
||||
var username = config.bridge.username;
|
||||
var validMac = /^([0-9A-F]{2}:){5}([0-9A-F]{2})$/;
|
||||
if (!validMac.test(username)){
|
||||
throw new Error('Not a valid username: ' + username + '. Must be 6 pairs of colon-' +
|
||||
'separated hexadecimal chars (A-F 0-9), like a MAC address.');
|
||||
}
|
||||
|
||||
var accessoryCount = (config.accessories && config.accessories.length) || 0;
|
||||
var platformCount = (config.platforms && config.platforms.length) || 0;
|
||||
log.info("Loaded config.json with %s accessories and %s platforms.", accessoryCount, platformCount);
|
||||
|
||||
log.info("---");
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
Server.prototype._loadCachedPlatformAccessories = function() {
|
||||
var cachedAccessories = accessoryStorage.getItem("cachedAccessories");
|
||||
var platformAccessories = [];
|
||||
|
||||
if (cachedAccessories) {
|
||||
for (var index in cachedAccessories) {
|
||||
var serializedAccessory = cachedAccessories[index];
|
||||
var platformAccessory = new PlatformAccessory(serializedAccessory.displayName, serializedAccessory.UUID, serializedAccessory.category);
|
||||
platformAccessory._configFromData(serializedAccessory);
|
||||
|
||||
platformAccessories.push(platformAccessory);
|
||||
}
|
||||
}
|
||||
|
||||
return platformAccessories;
|
||||
}
|
||||
|
||||
Server.prototype._createBridge = function() {
|
||||
// pull out our custom Bridge settings from config.json, if any
|
||||
var bridgeConfig = this._config.bridge || {};
|
||||
|
||||
// Create our Bridge which will host all loaded Accessories
|
||||
return new Bridge(bridgeConfig.name || 'Homebridge', uuid.generate("HomeBridge"));
|
||||
}
|
||||
|
||||
Server.prototype._loadAccessories = function() {
|
||||
|
||||
// Instantiate all accessories in the config
|
||||
log.info("Loading " + this._config.accessories.length + " accessories...");
|
||||
|
||||
for (var i=0; i<this._config.accessories.length; i++) {
|
||||
|
||||
var accessoryConfig = this._config.accessories[i];
|
||||
|
||||
// Load up the class for this accessory
|
||||
var accessoryType = accessoryConfig["accessory"]; // like "Lockitron"
|
||||
var accessoryConstructor = this._api.accessory(accessoryType); // like "LockitronAccessory", a JavaScript constructor
|
||||
|
||||
if (!accessoryConstructor)
|
||||
throw new Error("Your config.json is requesting the accessory '" + accessoryType + "' which has not been published by any installed plugins.");
|
||||
|
||||
// Create a custom logging function that prepends the device display name for debugging
|
||||
var accessoryName = accessoryConfig["name"];
|
||||
var accessoryLogger = Logger.withPrefix(accessoryName);
|
||||
|
||||
accessoryLogger("Initializing %s accessory...", accessoryType);
|
||||
|
||||
var accessoryInstance = new accessoryConstructor(accessoryLogger, accessoryConfig);
|
||||
var accessory = this._createAccessory(accessoryInstance, accessoryName, accessoryType, accessoryConfig.uuid_base); //pass accessoryType for UUID generation, and optional parameter uuid_base which can be used instead of displayName for UUID generation
|
||||
|
||||
// add it to the bridge
|
||||
this._bridge.addBridgedAccessory(accessory);
|
||||
}
|
||||
}
|
||||
|
||||
Server.prototype._loadPlatforms = function() {
|
||||
|
||||
log.info("Loading " + this._config.platforms.length + " platforms...");
|
||||
|
||||
for (var i=0; i<this._config.platforms.length; i++) {
|
||||
|
||||
var platformConfig = this._config.platforms[i];
|
||||
|
||||
// Load up the class for this accessory
|
||||
var platformType = platformConfig["platform"]; // like "Wink"
|
||||
var platformName = platformConfig["name"];
|
||||
var platformConstructor = this._api.platform(platformType); // like "WinkPlatform", a JavaScript constructor
|
||||
|
||||
if (!platformConstructor)
|
||||
throw new Error("Your config.json is requesting the platform '" + platformType + "' which has not been published by any installed plugins.");
|
||||
|
||||
// Create a custom logging function that prepends the platform name for debugging
|
||||
var platformLogger = Logger.withPrefix(platformName);
|
||||
|
||||
platformLogger("Initializing %s platform...", platformType);
|
||||
|
||||
var platformInstance = new platformConstructor(platformLogger, platformConfig, this._api);
|
||||
|
||||
if (platformInstance.configureAccessory == undefined) {
|
||||
// Plugin 1.0, load accessories
|
||||
this._loadPlatformAccessories(platformInstance, platformLogger, platformType);
|
||||
} else {
|
||||
this._activeDynamicPlugins[platformType] = platformInstance;
|
||||
}
|
||||
|
||||
if (platformInstance.configurationRequestHandler != undefined) {
|
||||
this._configurablePlatformPlugins[platformType] = platformInstance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Server.prototype._loadDynamicPlatforms = function() {
|
||||
for (var dynamicPluginName in this._api._dynamicPlatforms) {
|
||||
if (!this._activeDynamicPlugins[dynamicPluginName] && !this._activeDynamicPlugins[dynamicPluginName.split(".")[1]]) {
|
||||
console.log("Load " + dynamicPluginName);
|
||||
var platformConstructor = this._api._dynamicPlatforms[dynamicPluginName];
|
||||
var platformLogger = Logger.withPrefix(dynamicPluginName);
|
||||
var platformInstance = new platformConstructor(platformLogger, null, this._api);
|
||||
this._activeDynamicPlugins[dynamicPluginName] = platformInstance;
|
||||
|
||||
if (platformInstance.configurationRequestHandler != undefined) {
|
||||
this._configurablePlatformPlugins[dynamicPluginName] = platformInstance;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Server.prototype._configCachedPlatformAccessories = function() {
|
||||
for (var index in this._cachedPlatformAccessories) {
|
||||
var accessory = this._cachedPlatformAccessories[index];
|
||||
|
||||
if (!(accessory instanceof PlatformAccessory)) {
|
||||
console.log("Unexpected Accessory!");
|
||||
continue;
|
||||
}
|
||||
|
||||
var fullName = accessory._associatedPlugin + "." + accessory._associatedPlatform;
|
||||
var platformInstance = this._activeDynamicPlugins[fullName];
|
||||
|
||||
if (!platformInstance) {
|
||||
platformInstance = this._activeDynamicPlugins[accessory._associatedPlatform];
|
||||
}
|
||||
|
||||
if (platformInstance) {
|
||||
platformInstance.configureAccessory(accessory);
|
||||
} else {
|
||||
console.log("Failed to find plugin to handle accessory " + accessory.displayName);
|
||||
}
|
||||
|
||||
accessory._prepareAssociatedHAPAccessory();
|
||||
this._bridge.addBridgedAccessory(accessory._associatedHAPAccessory);
|
||||
}
|
||||
}
|
||||
|
||||
Server.prototype._loadPlatformAccessories = function(platformInstance, log, platformType) {
|
||||
this._asyncCalls++;
|
||||
platformInstance.accessories(once(function(foundAccessories){
|
||||
this._asyncCalls--;
|
||||
|
||||
// loop through accessories adding them to the list and registering them
|
||||
for (var i = 0; i < foundAccessories.length; i++) {
|
||||
var accessoryInstance = foundAccessories[i];
|
||||
var accessoryName = accessoryInstance.name; // assume this property was set
|
||||
|
||||
log("Initializing platform accessory '%s'...", accessoryName);
|
||||
|
||||
var accessory = this._createAccessory(accessoryInstance, accessoryName, platformType, accessoryInstance.uuid_base);
|
||||
|
||||
// add it to the bridge
|
||||
this._bridge.addBridgedAccessory(accessory);
|
||||
}
|
||||
|
||||
// were we the last callback?
|
||||
if (this._asyncCalls === 0 && !this._asyncWait)
|
||||
this._publish();
|
||||
}.bind(this)));
|
||||
}
|
||||
|
||||
Server.prototype._createAccessory = function(accessoryInstance, displayName, accessoryType, uuid_base) {
|
||||
|
||||
var services = accessoryInstance.getServices();
|
||||
|
||||
if (!(services[0] instanceof Service)) {
|
||||
// The returned "services" for this accessory is assumed to be the old style: a big array
|
||||
// of JSON-style objects that will need to be parsed by HAP-NodeJS's AccessoryLoader.
|
||||
|
||||
// Create the actual HAP-NodeJS "Accessory" instance
|
||||
return AccessoryLoader.parseAccessoryJSON({
|
||||
displayName: displayName,
|
||||
services: services
|
||||
});
|
||||
}
|
||||
else {
|
||||
// The returned "services" for this accessory are simply an array of new-API-style
|
||||
// Service instances which we can add to a created HAP-NodeJS Accessory directly.
|
||||
|
||||
var accessoryUUID = uuid.generate(accessoryType + ":" + (uuid_base || displayName));
|
||||
|
||||
var accessory = new Accessory(displayName, accessoryUUID);
|
||||
|
||||
// listen for the identify event if the accessory instance has defined an identify() method
|
||||
if (accessoryInstance.identify)
|
||||
accessory.on('identify', function(paired, callback) { accessoryInstance.identify(callback); });
|
||||
|
||||
services.forEach(function(service) {
|
||||
|
||||
// if you returned an AccessoryInformation service, merge its values with ours
|
||||
if (service instanceof Service.AccessoryInformation) {
|
||||
var existingService = accessory.getService(Service.AccessoryInformation);
|
||||
|
||||
// pull out any values you may have defined
|
||||
var manufacturer = service.getCharacteristic(Characteristic.Manufacturer).value;
|
||||
var model = service.getCharacteristic(Characteristic.Model).value;
|
||||
var serialNumber = service.getCharacteristic(Characteristic.SerialNumber).value;
|
||||
|
||||
if (manufacturer) existingService.setCharacteristic(Characteristic.Manufacturer, manufacturer);
|
||||
if (model) existingService.setCharacteristic(Characteristic.Model, model);
|
||||
if (serialNumber) existingService.setCharacteristic(Characteristic.SerialNumber, serialNumber);
|
||||
}
|
||||
else {
|
||||
accessory.addService(service);
|
||||
}
|
||||
});
|
||||
|
||||
return accessory;
|
||||
}
|
||||
}
|
||||
|
||||
Server.prototype._handleRegisterPlatformAccessories = function(accessories) {
|
||||
var hapAccessories = [];
|
||||
for (var index in accessories) {
|
||||
var accessory = accessories[index];
|
||||
|
||||
accessory._prepareAssociatedHAPAccessory();
|
||||
hapAccessories.push(accessory._associatedHAPAccessory);
|
||||
|
||||
this._cachedPlatformAccessories.push(accessory);
|
||||
}
|
||||
|
||||
this._bridge.addBridgedAccessories(hapAccessories);
|
||||
this._updateCachedAccessories();
|
||||
}
|
||||
|
||||
Server.prototype._handleUpdatePlatformAccessories = function(accessories) {
|
||||
// Update persisted accessories
|
||||
this._updateCachedAccessories();
|
||||
}
|
||||
|
||||
Server.prototype._handleUnregisterPlatformAccessories = function(accessories) {
|
||||
var hapAccessories = [];
|
||||
for (var index in accessories) {
|
||||
var accessory = accessories[index];
|
||||
|
||||
if (accessory._associatedHAPAccessory) {
|
||||
hapAccessories.push(accessory._associatedHAPAccessory);
|
||||
}
|
||||
|
||||
for (var targetIndex in this._cachedPlatformAccessories) {
|
||||
var existing = this._cachedPlatformAccessories[targetIndex];
|
||||
if (existing.UUID === accessory.UUID) {
|
||||
this._cachedPlatformAccessories.splice(targetIndex, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._bridge.removeBridgedAccessories(hapAccessories);
|
||||
this._updateCachedAccessories();
|
||||
}
|
||||
|
||||
Server.prototype._handlePublishCameraAccessories = function(accessories) {
|
||||
var accessoryPin = (this._config.bridge || {}).pin || "031-45-154";
|
||||
|
||||
for (var index in accessories) {
|
||||
var accessory = accessories[index];
|
||||
|
||||
accessory._prepareAssociatedHAPAccessory();
|
||||
var hapAccessory = accessory._associatedHAPAccessory;
|
||||
var advertiseAddress = mac.generate(accessory.UUID);
|
||||
|
||||
if (this._publishedCameras[advertiseAddress]) {
|
||||
throw new Error("Camera accessory %s experienced an address collision.", accessory.displayName);
|
||||
} else {
|
||||
this._publishedCameras[advertiseAddress] = accessory;
|
||||
}
|
||||
|
||||
hapAccessory.on('listening', function(port) {
|
||||
log.info("%s is running on port %s.", accessory.displayName, port);
|
||||
});
|
||||
|
||||
hapAccessory.publish({
|
||||
username: advertiseAddress,
|
||||
pincode: accessoryPin,
|
||||
category: accessory.category
|
||||
}, this._allowInsecureAccess);
|
||||
}
|
||||
}
|
||||
|
||||
Server.prototype._updateCachedAccessories = function() {
|
||||
var serializedAccessories = [];
|
||||
|
||||
for (var index in this._cachedPlatformAccessories) {
|
||||
var accessory = this._cachedPlatformAccessories[index];
|
||||
serializedAccessories.push(accessory._dictionaryPresentation());
|
||||
}
|
||||
|
||||
accessoryStorage.setItemSync("cachedAccessories", serializedAccessories);
|
||||
}
|
||||
|
||||
Server.prototype._handleNewConfig = function(type, name, replace, config) {
|
||||
if (type === "accessory") {
|
||||
// TODO: Load new accessory
|
||||
if (!this._config.accessories) {
|
||||
this._config.accessories = [];
|
||||
}
|
||||
|
||||
if (!replace) {
|
||||
this._config.accessories.push(config);
|
||||
} else {
|
||||
var targetName;
|
||||
if (name.indexOf('.') !== -1) {
|
||||
targetName = name.split(".")[1];
|
||||
}
|
||||
var found = false;
|
||||
for (var index in this._config.accessories) {
|
||||
var accessoryConfig = this._config.accessories[index];
|
||||
if (accessoryConfig.accessory === name) {
|
||||
this._config.accessories[index] = config;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (targetName && (accessoryConfig.accessory === targetName)) {
|
||||
this._config.accessories[index] = config;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
this._config.accessories.push(config);
|
||||
}
|
||||
}
|
||||
} else if (type === "platform") {
|
||||
if (!this._config.platforms) {
|
||||
this._config.platforms = [];
|
||||
}
|
||||
|
||||
if (!replace) {
|
||||
this._config.platforms.push(config);
|
||||
} else {
|
||||
var targetName;
|
||||
if (name.indexOf('.') !== -1) {
|
||||
targetName = name.split(".")[1];
|
||||
}
|
||||
|
||||
var found = false;
|
||||
for (var index in this._config.platforms) {
|
||||
var platformConfig = this._config.platforms[index];
|
||||
if (platformConfig.platform === name) {
|
||||
this._config.platforms[index] = config;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (targetName && (platformConfig.platform === targetName)) {
|
||||
this._config.platforms[index] = config;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
this._config.platforms.push(config);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var serializedConfig = JSON.stringify(this._config, null, ' ');
|
||||
var configPath = User.configPath();
|
||||
fs.writeFileSync(configPath, serializedConfig, 'utf8');
|
||||
}
|
||||
|
||||
// Returns the setup code in a scannable format.
|
||||
Server.prototype._printPin = function(pin) {
|
||||
console.log("Scan this code with your HomeKit App on your iOS device to pair with Homebridge:");
|
||||
console.log("\x1b[30;47m%s\x1b[0m", " ");
|
||||
console.log("\x1b[30;47m%s\x1b[0m", " ┌────────────┐ ");
|
||||
console.log("\x1b[30;47m%s\x1b[0m", " │ " + pin + " │ ");
|
||||
console.log("\x1b[30;47m%s\x1b[0m", " └────────────┘ ");
|
||||
console.log("\x1b[30;47m%s\x1b[0m", " ");
|
||||
}
|
||||
47
lib/user.js
Normal file
47
lib/user.js
Normal file
@@ -0,0 +1,47 @@
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
User: User
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages user settings and storage locations.
|
||||
*/
|
||||
|
||||
// global cached config
|
||||
var config;
|
||||
|
||||
// optional custom storage path
|
||||
var customStoragePath;
|
||||
|
||||
function User() {
|
||||
}
|
||||
|
||||
User.config = function() {
|
||||
return config || (config = Config.load(User.configPath()));
|
||||
}
|
||||
|
||||
User.storagePath = function() {
|
||||
if (customStoragePath) return customStoragePath;
|
||||
var home = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
|
||||
return path.join(home, ".homebridge");
|
||||
}
|
||||
|
||||
User.configPath = function() {
|
||||
return path.join(User.storagePath(), "config.json");
|
||||
}
|
||||
|
||||
User.persistPath = function() {
|
||||
return path.join(User.storagePath(), "persist");
|
||||
}
|
||||
|
||||
User.cachedAccessoryPath = function() {
|
||||
return path.join(User.storagePath(), "accessories");
|
||||
}
|
||||
|
||||
User.setStoragePath = function(path) {
|
||||
customStoragePath = path;
|
||||
}
|
||||
18
lib/util/mac.js
Normal file
18
lib/util/mac.js
Normal file
@@ -0,0 +1,18 @@
|
||||
var crypto = require('crypto');
|
||||
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
generate: generate
|
||||
}
|
||||
|
||||
function generate(data) {
|
||||
var sha1sum = crypto.createHash('sha1');
|
||||
sha1sum.update(data);
|
||||
var s = sha1sum.digest('hex');
|
||||
var i = -1;
|
||||
return 'xx:xx:xx:xx:xx:xx'.replace(/[x]/g, function(c) {
|
||||
i += 1;
|
||||
return s[i];
|
||||
}).toUpperCase();
|
||||
};
|
||||
12
lib/version.js
Normal file
12
lib/version.js
Normal file
@@ -0,0 +1,12 @@
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
|
||||
'use strict';
|
||||
|
||||
module.exports = getVersion();
|
||||
|
||||
function getVersion() {
|
||||
var packageJSONPath = path.join(__dirname, '../package.json');
|
||||
var packageJSON = JSON.parse(fs.readFileSync(packageJSONPath));
|
||||
return packageJSON.version;
|
||||
}
|
||||
34
package.json
34
package.json
@@ -1,27 +1,33 @@
|
||||
{
|
||||
"name": "homebridge",
|
||||
"description": "HomeKit support for the impatient",
|
||||
"version": "0.0.0",
|
||||
"version": "0.4.5",
|
||||
"scripts": {
|
||||
"start": "node app.js"
|
||||
"dev": "DEBUG=* ./bin/homebridge -D -P example-plugins/ || true"
|
||||
},
|
||||
"author": {
|
||||
"name": "Nick Farina"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/nfarina/homebridge.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "http://github.com/nfarina/homebridge/issues"
|
||||
},
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"homebridge": "bin/homebridge"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.3.2"
|
||||
},
|
||||
"preferGlobal": true,
|
||||
"dependencies": {
|
||||
"hap-nodejs": "git+https://github.com/khaost/HAP-NodeJS#2a1bc8d99a2009317ab5da93faebea34c89f197c",
|
||||
"ad2usb": "git+https://github.com/alistairg/node-ad2usb.git#local",
|
||||
"request": "2.49.x",
|
||||
"node-persist": "0.0.x",
|
||||
"xmldoc": "0.1.x",
|
||||
"node-hue-api": "^1.0.5",
|
||||
"xml2js": "0.4.x",
|
||||
"carwingsjs": "0.0.x",
|
||||
"sonos": "0.8.x",
|
||||
"wemo": "0.2.x",
|
||||
"wink-js": "0.0.5",
|
||||
"elkington": "kevinohara80/elkington"
|
||||
"chalk": "^1.1.1",
|
||||
"commander": "2.8.1",
|
||||
"hap-nodejs": "0.4.10",
|
||||
"semver": "5.0.3",
|
||||
"node-persist": "^0.0.8"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,320 +0,0 @@
|
||||
// Domoticz Platform Shim for HomeBridge
|
||||
// Written by Joep Verhaeg (http://www.joepverhaeg.nl)
|
||||
//
|
||||
// Revisions:
|
||||
//
|
||||
// 12 June 2015 [GizMoCuz]
|
||||
// - Added support for RGB lights
|
||||
// - Added support for Scenes
|
||||
// - Sorting device names
|
||||
//
|
||||
// Domoticz JSON API required
|
||||
// https://www.domoticz.com/wiki/Domoticz_API/JSON_URL's#Lights_and_switches
|
||||
//
|
||||
// Remember to add platform to config.json. Example:
|
||||
// "platforms": [
|
||||
// {
|
||||
// "platform": "Domoticz",
|
||||
// "name": "Domoticz",
|
||||
// "server": "127.0.0.1",
|
||||
// "port": "8080",
|
||||
// "roomid": 123 (0=no roomplan)
|
||||
// }
|
||||
// ],
|
||||
//
|
||||
// When you attempt to add a device, it will ask for a "PIN code".
|
||||
// The default code for all HomeBridge accessories is 031-45-154.
|
||||
//
|
||||
var types = require("HAP-NodeJS/accessories/types.js");
|
||||
var request = require("request");
|
||||
|
||||
function DomoticzPlatform(log, config){
|
||||
this.log = log;
|
||||
this.server = config["server"];
|
||||
this.port = config["port"];
|
||||
this.roomid = 0;
|
||||
if (typeof config["roomid"] != 'undefined') {
|
||||
this.roomid = config["roomid"];
|
||||
}
|
||||
}
|
||||
|
||||
function sortByKey(array, key) {
|
||||
return array.sort(function(a, b) {
|
||||
var x = a[key]; var y = b[key];
|
||||
return ((x < y) ? -1 : ((x > y) ? 1 : 0));
|
||||
});
|
||||
}
|
||||
|
||||
DomoticzPlatform.prototype = {
|
||||
accessories: function(callback) {
|
||||
this.log("Fetching Domoticz lights and switches...");
|
||||
|
||||
var that = this;
|
||||
var foundAccessories = [];
|
||||
if (this.roomid == 0) {
|
||||
//Get Lights
|
||||
request.get({
|
||||
url: "http://" + this.server + ":" + this.port + "/json.htm?type=devices&filter=light&used=true&order=Name",
|
||||
json: true
|
||||
}, function(err, response, json) {
|
||||
if (!err && response.statusCode == 200) {
|
||||
if (json['result'] != undefined) {
|
||||
var sArray=sortByKey(json['result'],"Name");
|
||||
sArray.map(function(s) {
|
||||
accessory = new DomoticzAccessory(that.log, that.server, that.port, false, s.idx, s.Name, s.HaveDimmer, s.MaxDimLevel, (s.SubType=="RGB")||(s.SubType=="RGBW"));
|
||||
foundAccessories.push(accessory);
|
||||
})
|
||||
}
|
||||
callback(foundAccessories);
|
||||
} else {
|
||||
that.log("There was a problem connecting to Domoticz.");
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
//Get all devices specified in the room
|
||||
request.get({
|
||||
url: "http://" + this.server + ":" + this.port + "/json.htm?type=devices&plan=" + this.roomid,
|
||||
json: true
|
||||
}, function(err, response, json) {
|
||||
if (!err && response.statusCode == 200) {
|
||||
if (json['result'] != undefined) {
|
||||
var sArray=sortByKey(json['result'],"Name");
|
||||
sArray.map(function(s) {
|
||||
//only accept switches for now
|
||||
if (typeof s.SwitchType != 'undefined') {
|
||||
accessory = new DomoticzAccessory(that.log, that.server, that.port, false, s.idx, s.Name, s.HaveDimmer, s.MaxDimLevel, (s.SubType=="RGB")||(s.SubType=="RGBW"));
|
||||
foundAccessories.push(accessory);
|
||||
}
|
||||
})
|
||||
}
|
||||
callback(foundAccessories);
|
||||
} else {
|
||||
that.log("There was a problem connecting to Domoticz.");
|
||||
}
|
||||
});
|
||||
}
|
||||
//Get Scenes
|
||||
foundAccessories = [];
|
||||
request.get({
|
||||
url: "http://" + this.server + ":" + this.port + "/json.htm?type=scenes",
|
||||
json: true
|
||||
}, function(err, response, json) {
|
||||
if (!err && response.statusCode == 200) {
|
||||
if (json['result'] != undefined) {
|
||||
var sArray=sortByKey(json['result'],"Name");
|
||||
sArray.map(function(s) {
|
||||
accessory = new DomoticzAccessory(that.log, that.server, that.port, true, s.idx, s.Name, false, 0, false);
|
||||
foundAccessories.push(accessory);
|
||||
})
|
||||
}
|
||||
callback(foundAccessories);
|
||||
} else {
|
||||
that.log("There was a problem connecting to Domoticz.");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function DomoticzAccessory(log, server, port, IsScene, idx, name, HaveDimmer, MaxDimLevel, HaveRGB) {
|
||||
// device info
|
||||
this.IsScene = IsScene;
|
||||
this.idx = idx;
|
||||
this.name = name;
|
||||
this.HaveDimmer = HaveDimmer;
|
||||
this.MaxDimLevel = MaxDimLevel;
|
||||
this.HaveRGB = HaveRGB;
|
||||
this.log = log;
|
||||
this.server = server;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
DomoticzAccessory.prototype = {
|
||||
command: function(c,value) {
|
||||
this.log(this.name + " sending command " + c + " with value " + value);
|
||||
if (this.IsScene == false) {
|
||||
//Lights
|
||||
if (c == "On" || c == "Off") {
|
||||
url = "http://" + this.server + ":" + this.port + "/json.htm?type=command¶m=switchlight&idx=" + this.idx + "&switchcmd=" + c + "&level=0";
|
||||
}
|
||||
else if (c == "setHue") {
|
||||
url = "http://" + this.server + ":" + this.port + "/json.htm?type=command¶m=setcolbrightnessvalue&idx=" + this.idx + "&hue=" + value + "&brightness=100" + "&iswhite=false";
|
||||
}
|
||||
else if (c == "setLevel") {
|
||||
url = "http://" + this.server + ":" + this.port + "/json.htm?type=command¶m=switchlight&idx=" + this.idx + "&switchcmd=Set%20Level&level=" + value;
|
||||
}
|
||||
else if (value != undefined) {
|
||||
this.log(this.name + " Unhandled Light command! cmd=" + c + ", value=" + value);
|
||||
}
|
||||
}
|
||||
else {
|
||||
//Scenes
|
||||
if (c == "On" || c == "Off") {
|
||||
url = "http://" + this.server + ":" + this.port + "/json.htm?type=command¶m=switchscene&idx=" + this.idx + "&switchcmd=" + c;
|
||||
}
|
||||
else if (value != undefined) {
|
||||
this.log(this.name + " Unhandled Scene command! cmd=" + c + ", value=" + value);
|
||||
}
|
||||
}
|
||||
|
||||
var that = this;
|
||||
request.put({ url: url }, function(err, response) {
|
||||
if (err) {
|
||||
that.log("There was a problem sending command " + c + " to" + that.name);
|
||||
that.log(url);
|
||||
} else {
|
||||
that.log(that.name + " sent command " + c);
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
informationCharacteristics: function() {
|
||||
return [
|
||||
{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: this.name,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of the accessory",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MANUFACTURER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "Domoticz",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Manufacturer",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MODEL_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "Rev-1",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Model",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.SERIAL_NUMBER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "A1S2NASF88EW",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "SN",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.IDENTIFY_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pw"],
|
||||
format: "bool",
|
||||
initialValue: false,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Identify Accessory",
|
||||
designedMaxLength: 1
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
controlCharacteristics: function(that) {
|
||||
cTypes = [{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: this.name,
|
||||
supportEvents: true,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of service",
|
||||
designedMaxLength: 255
|
||||
}]
|
||||
|
||||
if (this.idx != undefined) {
|
||||
cTypes.push({
|
||||
cType: types.POWER_STATE_CTYPE,
|
||||
onUpdate: function(value) {
|
||||
if (value == 0) {
|
||||
that.command("Off")
|
||||
} else {
|
||||
that.command("On")
|
||||
}
|
||||
},
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "bool",
|
||||
initialValue: 0,
|
||||
supportEvents: true,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Change the power state",
|
||||
designedMaxLength: 1
|
||||
})
|
||||
}
|
||||
|
||||
if (this.HaveDimmer == true) {
|
||||
cTypes.push({
|
||||
cType: types.BRIGHTNESS_CTYPE,
|
||||
onUpdate: function(value) { that.command("setLevel", value); },
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "int",
|
||||
initialValue: 0,
|
||||
supportEvents: true,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Adjust Brightness of Light",
|
||||
designedMinValue: 0,
|
||||
designedMaxValue: this.MaxDimLevel,
|
||||
designedMinStep: 1,
|
||||
unit: "%"
|
||||
})
|
||||
}
|
||||
if (this.HaveRGB == true) {
|
||||
cTypes.push({
|
||||
cType: types.HUE_CTYPE,
|
||||
onUpdate: function(value) { that.command("setHue", value); },
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "int",
|
||||
initialValue: 0,
|
||||
supportEvents: true,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Adjust Hue of Light",
|
||||
designedMinValue: 0,
|
||||
designedMaxValue: 360,
|
||||
designedMinStep: 1,
|
||||
unit: "arcdegrees"
|
||||
})
|
||||
}
|
||||
|
||||
return cTypes
|
||||
},
|
||||
|
||||
sType: function() {
|
||||
if (this.HaveDimmer == true) {
|
||||
return types.LIGHTBULB_STYPE
|
||||
} else {
|
||||
return types.SWITCH_STYPE
|
||||
}
|
||||
},
|
||||
|
||||
getServices: function() {
|
||||
var that = this;
|
||||
var services = [{
|
||||
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||
characteristics: this.informationCharacteristics(),
|
||||
},
|
||||
{
|
||||
sType: this.sType(),
|
||||
characteristics: this.controlCharacteristics(that)
|
||||
}];
|
||||
this.log("Loaded services for " + this.name)
|
||||
return services;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.accessory = DomoticzAccessory;
|
||||
module.exports.platform = DomoticzPlatform;
|
||||
385
platforms/ISY.js
385
platforms/ISY.js
@@ -1,385 +0,0 @@
|
||||
var types = require("HAP-NodeJS/accessories/types.js");
|
||||
var xml2js = require('xml2js');
|
||||
var request = require('request');
|
||||
var util = require('util');
|
||||
|
||||
var parser = new xml2js.Parser();
|
||||
|
||||
|
||||
var power_state_ctype = {
|
||||
cType: types.POWER_STATE_CTYPE,
|
||||
onUpdate: function(value) { return; },
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "bool",
|
||||
initialValue: 0,
|
||||
supportEvents: true,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Change the power state",
|
||||
designedMaxLength: 1
|
||||
};
|
||||
|
||||
function ISYURL(user, pass, host, port, path) {
|
||||
return util.format("http://%s:%s@%s:%d%s", user, pass, host, port, encodeURI(path));
|
||||
}
|
||||
|
||||
function ISYPlatform(log, config) {
|
||||
this.host = config["host"];
|
||||
this.port = config["port"];
|
||||
this.user = config["username"];
|
||||
this.pass = config["password"];
|
||||
|
||||
this.log = log;
|
||||
}
|
||||
|
||||
ISYPlatform.prototype = {
|
||||
accessories: function(callback) {
|
||||
this.log("Fetching ISY Devices.");
|
||||
|
||||
var that = this;
|
||||
var url = ISYURL(this.user, this.pass, this.host, this.port, "/rest/nodes");
|
||||
|
||||
var options = {
|
||||
url: url,
|
||||
method: 'GET'
|
||||
};
|
||||
|
||||
var foundAccessories = [];
|
||||
|
||||
request(options, function(error, response, body) {
|
||||
if (error)
|
||||
{
|
||||
console.trace("Requesting ISY devices.");
|
||||
that.log(error);
|
||||
return error;
|
||||
}
|
||||
|
||||
parser.parseString(body, function(err, result) {
|
||||
result.nodes.node.forEach(function(obj) {
|
||||
var enabled = obj.enabled[0] == 'true';
|
||||
|
||||
if (enabled)
|
||||
{
|
||||
var device = new ISYAccessory(
|
||||
that.log,
|
||||
that.host,
|
||||
that.port,
|
||||
that.user,
|
||||
that.pass,
|
||||
obj.name[0],
|
||||
obj.address[0],
|
||||
obj.property[0].$.uom
|
||||
);
|
||||
|
||||
foundAccessories.push(device);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
callback(foundAccessories.sort(function (a,b) {
|
||||
return (a.name > b.name) - (a.name < b.name);
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function ISYAccessory(log, host, port, user, pass, name, address, uom) {
|
||||
this.log = log;
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
this.user = user;
|
||||
this.pass = pass;
|
||||
this.name = name;
|
||||
this.address = address;
|
||||
this.uom = uom;
|
||||
}
|
||||
|
||||
ISYAccessory.prototype = {
|
||||
query: function() {
|
||||
var path = util.format("/rest/status/%s", encodeURI(this.address));
|
||||
var url = ISYURL(this.user, this.pass, this.host, this.port, path);
|
||||
|
||||
var options = { url: url, method: 'GET' };
|
||||
request(options, function(error, response, body) {
|
||||
if (error)
|
||||
{
|
||||
console.trace("Requesting Device Status.");
|
||||
that.log(error);
|
||||
return error;
|
||||
}
|
||||
|
||||
parser.parseString(body, function(err, result) {
|
||||
var value = result.properties.property[0].$.value;
|
||||
return value;
|
||||
});
|
||||
|
||||
});
|
||||
},
|
||||
|
||||
command: function(c, value) {
|
||||
this.log(this.name + " sending command " + c + " with value " + value);
|
||||
|
||||
switch (c)
|
||||
{
|
||||
case 'On':
|
||||
path = "/rest/nodes/" + this.address + "/cmd/DFON";
|
||||
break;
|
||||
case 'Off':
|
||||
path = "/rest/nodes/" + this.address + "/cmd/DFOF";
|
||||
break;
|
||||
case 'Low':
|
||||
path = "/rest/nodes/" + this.address + "/cmd/DON/85";
|
||||
break;
|
||||
case 'Medium':
|
||||
path = "/rest/nodes/" + this.address + "/cmd/DON/128";
|
||||
break;
|
||||
case 'High':
|
||||
path = "/rest/nodes/" + this.address + "/cmd/DON/255";
|
||||
break;
|
||||
case 'setLevel':
|
||||
if (value > 0)
|
||||
{
|
||||
path = "/rest/nodes/" + this.address + "/cmd/DON/" + Math.floor(255 * (value / 100));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
this.log("Unimplemented command sent to " + this.name + " Command " + c);
|
||||
break;
|
||||
}
|
||||
|
||||
if (path)
|
||||
{
|
||||
var url = ISYURL(this.user, this.pass, this.host, this.port, path);
|
||||
var options = {
|
||||
url: url,
|
||||
method: 'GET'
|
||||
};
|
||||
|
||||
var that = this;
|
||||
request(options, function(error, response, body) {
|
||||
if (error)
|
||||
{
|
||||
console.trace("Sending Command.");
|
||||
that.log(error);
|
||||
return error;
|
||||
}
|
||||
that.log("Sent command " + path + " to " + that.name);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
informationCharacteristics: function() {
|
||||
return [
|
||||
{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: this.name,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of the accessory",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MANUFACTURER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "SmartHome",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Manufacturer",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MODEL_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "Rev-1",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Model",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.SERIAL_NUMBER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: this.address,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "SN",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.IDENTIFY_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pw"],
|
||||
format: "bool",
|
||||
initialValue: false,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Identify Accessory",
|
||||
designedMaxLength: 1
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
controlCharacteristics: function(that) {
|
||||
cTypes = [{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: this.name,
|
||||
supportEvents: true,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of service",
|
||||
designedMaxLength: 255
|
||||
}]
|
||||
|
||||
if (this.uom == "%/on/off") {
|
||||
cTypes.push({
|
||||
cType: types.POWER_STATE_CTYPE,
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "bool",
|
||||
initialValue: 0,
|
||||
supportEvents: true,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Change the power state",
|
||||
designedMaxLength: 1,
|
||||
onUpdate: function(value) {
|
||||
if (value == 0) {
|
||||
that.command("Off")
|
||||
} else {
|
||||
that.command("On")
|
||||
}
|
||||
},
|
||||
onRead: function() {
|
||||
return this.query();
|
||||
}
|
||||
});
|
||||
cTypes.push({
|
||||
cType: types.BRIGHTNESS_CTYPE,
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "int",
|
||||
initialValue: 0,
|
||||
supportEvents: true,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Adjust Brightness of Light",
|
||||
designedMinValue: 0,
|
||||
designedMaxValue: 100,
|
||||
designedMinStep: 1,
|
||||
unit: "%",
|
||||
onUpdate: function(value) {
|
||||
that.command("setLevel", value);
|
||||
},
|
||||
onRead: function() {
|
||||
var val = this.query();
|
||||
that.log("Query: " + val);
|
||||
return val;
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (this.uom == "off/low/med/high")
|
||||
{
|
||||
cTypes.push({
|
||||
cType: types.POWER_STATE_CTYPE,
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "bool",
|
||||
initialValue: 0,
|
||||
supportEvents: true,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Change the power state",
|
||||
designedMaxLength: 1,
|
||||
onUpdate: function(value) {
|
||||
if (value == 0) {
|
||||
that.command("Off")
|
||||
} else {
|
||||
that.command("On")
|
||||
}
|
||||
},
|
||||
onRead: function() {
|
||||
return this.query();
|
||||
}
|
||||
});
|
||||
cTypes.push({
|
||||
cType: types.ROTATION_SPEED_CTYPE,
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "bool",
|
||||
initialValue: 0,
|
||||
supportEvents: true,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Change the speed of the fan",
|
||||
designedMaxLength: 1,
|
||||
onUpdate: function(value) {
|
||||
if (value == 0) {
|
||||
that.command("Off");
|
||||
} else if (value > 0 && value < 40) {
|
||||
that.command("Low");
|
||||
} else if (value > 40 && value < 75) {
|
||||
that.command("Medium");
|
||||
} else {
|
||||
that.command("High");
|
||||
}
|
||||
},
|
||||
onRead: function() {
|
||||
return this.query();
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (this.uom == "on/off")
|
||||
{
|
||||
cTypes.push({
|
||||
cType: types.POWER_STATE_CTYPE,
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "bool",
|
||||
initialValue: 0,
|
||||
supportEvents: true,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Change the power state",
|
||||
designedMaxLength: 1,
|
||||
onUpdate: function(value) {
|
||||
if (value == 0) {
|
||||
that.command("Off")
|
||||
} else {
|
||||
that.command("On")
|
||||
}
|
||||
},
|
||||
onRead: function() {
|
||||
return this.query();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return cTypes;
|
||||
},
|
||||
|
||||
sType: function() {
|
||||
if (this.uom == "%/on/off") {
|
||||
return types.LIGHTBULB_STYPE;
|
||||
} else if (this.uom == "on/off") {
|
||||
return types.SWITCH_STYPE;
|
||||
} else if (this.uom == "off/low/med/high") {
|
||||
return types.FAN_STYPE;
|
||||
}
|
||||
|
||||
return types.SWITCH_STYPE;
|
||||
},
|
||||
|
||||
getServices: function() {
|
||||
var that = this;
|
||||
var services = [{
|
||||
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||
characteristics: this.informationCharacteristics(),
|
||||
},
|
||||
{
|
||||
sType: this.sType(),
|
||||
characteristics: this.controlCharacteristics(that)
|
||||
}];
|
||||
|
||||
//that.log("Loaded services for " + that.name);
|
||||
return services;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.accessory = ISYAccessory;
|
||||
module.exports.platform = ISYPlatform;
|
||||
@@ -1,369 +0,0 @@
|
||||
// Philips Hue Platform Shim for HomeBridge
|
||||
//
|
||||
// Remember to add platform to config.json. Example:
|
||||
// "platforms": [
|
||||
// {
|
||||
// "platform": "PhilipsHue",
|
||||
// "name": "Philips Hue",
|
||||
// "ip_address": "127.0.0.1",
|
||||
// "username": "252deadbeef0bf3f34c7ecb810e832f"
|
||||
// }
|
||||
// ],
|
||||
//
|
||||
// If you do not know the IP address of your Hue Bridge, simply leave it blank and your Bridge
|
||||
// will be discovered automatically.
|
||||
//
|
||||
// If you do not have a "username" for your Hue API already, simply leave the field blank and
|
||||
// you will be prompted to press the link button on your Hue Bridge before running HomeBridge.
|
||||
// A username will be created for you and printed out, then the server will exit so you may
|
||||
// enter it in your config.json.
|
||||
//
|
||||
//
|
||||
// When you attempt to add a device, it will ask for a "PIN code".
|
||||
// The default code for all HomeBridge accessories is 031-45-154.
|
||||
//
|
||||
|
||||
/* jslint node: true */
|
||||
/* globals require: false */
|
||||
/* globals config: false */
|
||||
|
||||
"use strict";
|
||||
|
||||
var hue = require("node-hue-api"),
|
||||
HueApi = hue.HueApi,
|
||||
lightState = hue.lightState;
|
||||
|
||||
var types = require("HAP-NodeJS/accessories/types.js");
|
||||
|
||||
function PhilipsHuePlatform(log, config) {
|
||||
this.log = log;
|
||||
this.ip_address = config["ip_address"];
|
||||
this.username = config["username"];
|
||||
}
|
||||
|
||||
function PhilipsHueAccessory(log, device, api) {
|
||||
this.id = device.id;
|
||||
this.name = device.name;
|
||||
this.model = device.modelid;
|
||||
this.device = device;
|
||||
this.api = api;
|
||||
this.log = log;
|
||||
}
|
||||
|
||||
// Get the ip address of the first available bridge with meethue.com or a network scan.
|
||||
var locateBridge = function (callback) {
|
||||
var that = this;
|
||||
|
||||
// Report the results of the scan to the user
|
||||
var getIp = function (err, bridges) {
|
||||
if (!bridges || bridges.length === 0) {
|
||||
that.log("No Philips Hue bridges found.");
|
||||
callback(err || new Error("No bridges found"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (bridges.length > 1) {
|
||||
that.log("Warning: Multiple Philips Hue bridges detected. The first bridge will be used automatically. To use a different bridge, set the `ip_address` manually in the configuration.");
|
||||
}
|
||||
|
||||
that.log(
|
||||
"Philips Hue bridges found:\n" +
|
||||
(bridges.map(function (bridge) {
|
||||
// Bridge name is only returned from meethue.com so use id instead if it isn't there
|
||||
return "\t" + bridge.ipaddress + ' - ' + (bridge.name || bridge.id);
|
||||
})).join("\n")
|
||||
);
|
||||
|
||||
callback(null, bridges[0].ipaddress);
|
||||
};
|
||||
|
||||
// Try to discover the bridge ip using meethue.com
|
||||
that.log("Attempting to discover Philips Hue bridge with meethue.com...");
|
||||
hue.nupnpSearch(function (locateError, bridges) {
|
||||
if (locateError) {
|
||||
that.log("Philips Hue bridge discovery with meethue.com failed. Register your bridge with the meethue.com for more reliable discovery.");
|
||||
|
||||
that.log("Attempting to discover Philips Hue bridge with network scan...");
|
||||
|
||||
// Timeout after one minute
|
||||
hue.upnpSearch(60000)
|
||||
.then(function (bridges) {
|
||||
that.log("Scan complete");
|
||||
getIp(null, bridges);
|
||||
})
|
||||
.fail(function (scanError) {
|
||||
that.log("Philips Hue bridge discovery with network scan failed. Check your network connection or set ip_address manually in configuration.");
|
||||
getIp(new Error("Scan failed: " + scanError.message));
|
||||
}).done();
|
||||
} else {
|
||||
getIp(null, bridges);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
PhilipsHuePlatform.prototype = {
|
||||
|
||||
accessories: function(callback) {
|
||||
this.log("Fetching Philips Hue lights...");
|
||||
var that = this;
|
||||
var getLights = function () {
|
||||
var api = new HueApi(that.ip_address, that.username);
|
||||
|
||||
// Connect to the API
|
||||
// Get a dump of all lights, so as not to hit rate limiting for installations with larger amounts of bulbs
|
||||
|
||||
api.fullState(function(err, response) {
|
||||
if (err) throw err;
|
||||
|
||||
var foundAccessories = [];
|
||||
for (var deviceId in response.lights) {
|
||||
var device = response.lights[deviceId];
|
||||
device.id = deviceId;
|
||||
var accessory = new PhilipsHueAccessory(that.log, device, api);
|
||||
foundAccessories.push(accessory);
|
||||
}
|
||||
callback(foundAccessories);
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
// Create a new user if needed
|
||||
function checkUsername() {
|
||||
if (!that.username) {
|
||||
var api = new HueApi(that.ip_address);
|
||||
api.createUser(that.ip_address, null, null, function(err, user) {
|
||||
|
||||
// try and help explain this particular error
|
||||
if (err && err.message == "link button not pressed")
|
||||
throw "Please press the link button on your Philips Hue bridge, then start the HomeBridge server within 30 seconds.";
|
||||
|
||||
if (err) throw err;
|
||||
|
||||
throw "Created a new username " + JSON.stringify(user) + " for your Philips Hue. Please add it to your config.json then start the HomeBridge server again: ";
|
||||
});
|
||||
}
|
||||
else {
|
||||
getLights();
|
||||
}
|
||||
}
|
||||
|
||||
// Discover the bridge if needed
|
||||
if (!this.ip_address) {
|
||||
locateBridge.call(this, function (err, ip_address) {
|
||||
if (err) throw err;
|
||||
|
||||
// TODO: Find a way to persist this
|
||||
that.ip_address = ip_address;
|
||||
that.log("Save the Philips Hue bridge ip address "+ ip_address +" to your config to skip discovery.");
|
||||
checkUsername();
|
||||
});
|
||||
} else {
|
||||
checkUsername();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
PhilipsHueAccessory.prototype = {
|
||||
// Convert 0-65535 to 0-360
|
||||
hueToArcDegrees: function(value) {
|
||||
value = value/65535;
|
||||
value = value*100;
|
||||
value = Math.round(value);
|
||||
return value;
|
||||
},
|
||||
// Convert 0-360 to 0-65535
|
||||
arcDegreesToHue: function(value) {
|
||||
value = value/360;
|
||||
value = value*65535;
|
||||
value = Math.round(value);
|
||||
return value;
|
||||
},
|
||||
// Convert 0-255 to 0-100
|
||||
bitsToPercentage: function(value) {
|
||||
value = value/255;
|
||||
value = value*100;
|
||||
value = Math.round(value);
|
||||
return value;
|
||||
},
|
||||
// Create and set a light state
|
||||
executeChange: function(api, device, characteristic, value) {
|
||||
var that = this;
|
||||
var state = lightState.create();
|
||||
switch(characteristic.toLowerCase()) {
|
||||
case 'identify':
|
||||
state.alert('select');
|
||||
break;
|
||||
case 'power':
|
||||
if (value) {
|
||||
state.on();
|
||||
}
|
||||
else {
|
||||
state.off();
|
||||
}
|
||||
break;
|
||||
case 'hue':
|
||||
state.hue(this.arcDegreesToHue(value));
|
||||
break;
|
||||
case 'brightness':
|
||||
state.brightness(value);
|
||||
break;
|
||||
case 'saturation':
|
||||
state.saturation(value);
|
||||
break;
|
||||
}
|
||||
api.setLightState(device.id, state, function(err, lights) {
|
||||
if (!err) {
|
||||
that.log(device.name + ", characteristic: " + characteristic + ", value: " + value + ".");
|
||||
}
|
||||
else {
|
||||
that.log(err);
|
||||
}
|
||||
});
|
||||
},
|
||||
// Get Services
|
||||
getServices: function() {
|
||||
var that = this;
|
||||
var bulb_characteristics = [
|
||||
{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: this.name,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of service",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.POWER_STATE_CTYPE,
|
||||
onUpdate: function(value) {
|
||||
that.executeChange(that.api, that.device, "power", value);
|
||||
},
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "bool",
|
||||
initialValue: that.device.state.on,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Turn On the Light",
|
||||
designedMaxLength: 1
|
||||
},{
|
||||
cType: types.BRIGHTNESS_CTYPE,
|
||||
onUpdate: function(value) {
|
||||
that.executeChange(that.api, that.device, "brightness", value);
|
||||
},
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "int",
|
||||
initialValue: that.bitsToPercentage(that.device.state.bri),
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Adjust Brightness of Light",
|
||||
designedMinValue: 0,
|
||||
designedMaxValue: 100,
|
||||
designedMinStep: 1,
|
||||
unit: "%"
|
||||
}
|
||||
];
|
||||
// Handle the Hue/Hue Lux divergence
|
||||
if (that.device.state.hasOwnProperty('hue') && that.device.state.hasOwnProperty('sat')) {
|
||||
bulb_characteristics.push({
|
||||
cType: types.HUE_CTYPE,
|
||||
onUpdate: function(value) {
|
||||
that.executeChange(that.api, that.device, "hue", value);
|
||||
},
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "int",
|
||||
initialValue: that.hueToArcDegrees(that.device.state.hue),
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Adjust Hue of Light",
|
||||
designedMinValue: 0,
|
||||
designedMaxValue: 360,
|
||||
designedMinStep: 1,
|
||||
unit: "arcdegrees"
|
||||
});
|
||||
bulb_characteristics.push({
|
||||
cType: types.SATURATION_CTYPE,
|
||||
onUpdate: function(value) {
|
||||
that.executeChange(that.api, that.device, "saturation", value);
|
||||
},
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "int",
|
||||
initialValue: that.bitsToPercentage(that.device.state.sat),
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Adjust Saturation of Light",
|
||||
designedMinValue: 0,
|
||||
designedMaxValue: 100,
|
||||
designedMinStep: 1,
|
||||
unit: "%"
|
||||
});
|
||||
}
|
||||
var accessory_data = [
|
||||
{
|
||||
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||
characteristics: [
|
||||
{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: this.name,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of the accessory",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MANUFACTURER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "Philips",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Manufacturer",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MODEL_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: that.model,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Model",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.SERIAL_NUMBER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: that.device.uniqueid,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "SN",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.IDENTIFY_CTYPE,
|
||||
onUpdate: function(value) {
|
||||
that.executeChange(that.api, that.device, "identify", value);
|
||||
},
|
||||
perms: ["pw"],
|
||||
format: "bool",
|
||||
initialValue: false,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Identify Accessory",
|
||||
designedMaxLength: 1
|
||||
}
|
||||
]
|
||||
},{
|
||||
sType: types.LIGHTBULB_STYPE,
|
||||
// `bulb_characteristics` defined based on bulb type
|
||||
characteristics: bulb_characteristics
|
||||
}
|
||||
];
|
||||
return accessory_data;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.platform = PhilipsHuePlatform;
|
||||
@@ -1,242 +0,0 @@
|
||||
// SmartThings JSON API SmartApp required
|
||||
// https://github.com/jnewland/SmartThings/blob/master/JSON.groovy
|
||||
//
|
||||
var types = require("HAP-NodeJS/accessories/types.js");
|
||||
var request = require("request");
|
||||
|
||||
function SmartThingsPlatform(log, config){
|
||||
this.log = log;
|
||||
this.app_id = config["app_id"];
|
||||
this.access_token = config["access_token"];
|
||||
}
|
||||
|
||||
SmartThingsPlatform.prototype = {
|
||||
accessories: function(callback) {
|
||||
this.log("Fetching SmartThings devices...");
|
||||
|
||||
var that = this;
|
||||
var foundAccessories = [];
|
||||
|
||||
request.get({
|
||||
url: "https://graph.api.smartthings.com/api/smartapps/installations/"+this.app_id+"/devices?access_token="+this.access_token,
|
||||
json: true
|
||||
}, function(err, response, json) {
|
||||
if (!err && response.statusCode == 200) {
|
||||
if (json['switches'] != undefined) {
|
||||
json['switches'].map(function(s) {
|
||||
accessory = new SmartThingsAccessory(that.log, s.name, s.commands);
|
||||
foundAccessories.push(accessory);
|
||||
})
|
||||
}
|
||||
if (json['hues'] != undefined) {
|
||||
json['hues'].map(function(s) {
|
||||
accessory = new SmartThingsAccessory(that.log, s.name, s.commands);
|
||||
foundAccessories.push(accessory);
|
||||
})
|
||||
}
|
||||
callback(foundAccessories);
|
||||
} else {
|
||||
that.log("There was a problem authenticating with SmartThings.");
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function SmartThingsAccessory(log, name, commands) {
|
||||
// device info
|
||||
this.name = name;
|
||||
this.commands = commands;
|
||||
this.log = log;
|
||||
}
|
||||
|
||||
SmartThingsAccessory.prototype = {
|
||||
|
||||
command: function(c,value) {
|
||||
this.log(this.name + " sending command " + c);
|
||||
var url = this.commands[c];
|
||||
if (value != undefined) {
|
||||
url = this.commands[c] + "&value="+value
|
||||
}
|
||||
|
||||
var that = this;
|
||||
request.put({
|
||||
url: url
|
||||
}, function(err, response) {
|
||||
if (err) {
|
||||
that.log("There was a problem sending command " + c + " to" + that.name);
|
||||
that.log(url);
|
||||
} else {
|
||||
that.log(that.name + " sent command " + c);
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
informationCharacteristics: function() {
|
||||
return [
|
||||
{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: this.name,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of the accessory",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MANUFACTURER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "SmartThings",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Manufacturer",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MODEL_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "Rev-1",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Model",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.SERIAL_NUMBER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "A1S2NASF88EW",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "SN",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.IDENTIFY_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pw"],
|
||||
format: "bool",
|
||||
initialValue: false,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Identify Accessory",
|
||||
designedMaxLength: 1
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
controlCharacteristics: function(that) {
|
||||
cTypes = [{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: this.name,
|
||||
supportEvents: true,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of service",
|
||||
designedMaxLength: 255
|
||||
}]
|
||||
|
||||
if (this.commands['on'] != undefined) {
|
||||
cTypes.push({
|
||||
cType: types.POWER_STATE_CTYPE,
|
||||
onUpdate: function(value) {
|
||||
if (value == 0) {
|
||||
that.command("off")
|
||||
} else {
|
||||
that.command("on")
|
||||
}
|
||||
},
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "bool",
|
||||
initialValue: 0,
|
||||
supportEvents: true,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Change the power state",
|
||||
designedMaxLength: 1
|
||||
})
|
||||
}
|
||||
|
||||
if (this.commands['on'] != undefined) {
|
||||
cTypes.push({
|
||||
cType: types.BRIGHTNESS_CTYPE,
|
||||
onUpdate: function(value) { that.command("setLevel", value); },
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "int",
|
||||
initialValue: 0,
|
||||
supportEvents: true,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Adjust Brightness of Light",
|
||||
designedMinValue: 0,
|
||||
designedMaxValue: 100,
|
||||
designedMinStep: 1,
|
||||
unit: "%"
|
||||
})
|
||||
}
|
||||
|
||||
if (this.commands['setHue'] != undefined) {
|
||||
cTypes.push({
|
||||
cType: types.HUE_CTYPE,
|
||||
onUpdate: function(value) { that.command("setHue", value); },
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "int",
|
||||
initialValue: 0,
|
||||
supportEvents: true,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Adjust Hue of Light",
|
||||
designedMinValue: 0,
|
||||
designedMaxValue: 360,
|
||||
designedMinStep: 1,
|
||||
unit: "arcdegrees"
|
||||
})
|
||||
}
|
||||
|
||||
if (this.commands['setSaturation'] != undefined) {
|
||||
cTypes.push({
|
||||
cType: types.SATURATION_CTYPE,
|
||||
onUpdate: function(value) { that.command("setSaturation", value); },
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "int",
|
||||
initialValue: 0,
|
||||
supportEvents: true,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Adjust Brightness of Light",
|
||||
designedMinValue: 0,
|
||||
designedMaxValue: 100,
|
||||
designedMinStep: 1,
|
||||
unit: "%"
|
||||
})
|
||||
}
|
||||
|
||||
return cTypes
|
||||
},
|
||||
|
||||
sType: function() {
|
||||
if (this.commands['setLevel'] != undefined) {
|
||||
return types.LIGHTBULB_STYPE
|
||||
} else {
|
||||
return types.SWITCH_STYPE
|
||||
}
|
||||
},
|
||||
|
||||
getServices: function() {
|
||||
var that = this;
|
||||
var services = [{
|
||||
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||
characteristics: this.informationCharacteristics(),
|
||||
},
|
||||
{
|
||||
sType: this.sType(),
|
||||
characteristics: this.controlCharacteristics(that)
|
||||
}];
|
||||
this.log("Loaded services for " + this.name)
|
||||
return services;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.accessory = SmartThingsAccessory;
|
||||
module.exports.platform = SmartThingsPlatform;
|
||||
@@ -1,252 +0,0 @@
|
||||
var types = require("HAP-NodeJS/accessories/types.js");
|
||||
var wink = require('wink-js');
|
||||
|
||||
var model = {
|
||||
light_bulbs: require('wink-js/lib/model/light')
|
||||
};
|
||||
|
||||
|
||||
function WinkPlatform(log, config){
|
||||
|
||||
// auth info
|
||||
this.client_id = config["client_id"];
|
||||
this.client_secret = config["client_secret"];
|
||||
this.username = config["username"];
|
||||
this.password = config["password"];
|
||||
|
||||
this.log = log;
|
||||
}
|
||||
|
||||
WinkPlatform.prototype = {
|
||||
accessories: function(callback) {
|
||||
this.log("Fetching Wink devices.");
|
||||
|
||||
var that = this;
|
||||
var foundAccessories = [];
|
||||
|
||||
wink.init({
|
||||
"client_id": this.client_id,
|
||||
"client_secret": this.client_secret,
|
||||
"username": this.username,
|
||||
"password": this.password
|
||||
}, function(auth_return) {
|
||||
if ( auth_return === undefined ) {
|
||||
that.log("There was a problem authenticating with Wink.");
|
||||
} else {
|
||||
// success
|
||||
wink.user().devices('light_bulbs', function(devices) {
|
||||
for (var i=0; i<devices.data.length; i++){
|
||||
device = model.light_bulbs(devices.data[i], wink)
|
||||
accessory = new WinkAccessory(that.log, device);
|
||||
foundAccessories.push(accessory);
|
||||
}
|
||||
callback(foundAccessories);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function WinkAccessory(log, device) {
|
||||
// device info
|
||||
this.name = device.name;
|
||||
this.device = device;
|
||||
this.log = log;
|
||||
}
|
||||
|
||||
WinkAccessory.prototype = {
|
||||
getPowerState: function(callback){
|
||||
if (!this.device) {
|
||||
this.log("No '"+this.name+"' device found (yet?)");
|
||||
return;
|
||||
}
|
||||
|
||||
var that = this;
|
||||
|
||||
this.log("checking power state for: " + this.name);
|
||||
wink.user().device(this.name, function(light_obj){
|
||||
powerState = light_obj.desired_state.powered
|
||||
that.log("power state for " + that.name + " is: " + powerState)
|
||||
callback(powerState);
|
||||
});
|
||||
|
||||
|
||||
},
|
||||
|
||||
getBrightness: function(callback){
|
||||
if (!this.device) {
|
||||
this.log("No '"+this.name+"' device found (yet?)");
|
||||
return;
|
||||
}
|
||||
|
||||
var that = this;
|
||||
|
||||
this.log("checking brightness level for: " + this.name);
|
||||
wink.user().device(this.name, function(light_obj){
|
||||
level = light_obj.desired_state.brightness * 100
|
||||
that.log("brightness level for " + that.name + " is: " + level)
|
||||
callback(level);
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
setPowerState: function(powerOn) {
|
||||
if (!this.device) {
|
||||
this.log("No '"+this.name+"' device found (yet?)");
|
||||
return;
|
||||
}
|
||||
|
||||
var that = this;
|
||||
|
||||
if (powerOn) {
|
||||
this.log("Setting power state on the '"+this.name+"' to on");
|
||||
this.device.power.on(function(response) {
|
||||
if (response === undefined) {
|
||||
that.log("Error setting power state on the '"+that.name+"'")
|
||||
} else {
|
||||
that.log("Successfully set power state on the '"+that.name+"' to on");
|
||||
}
|
||||
});
|
||||
}else{
|
||||
this.log("Setting power state on the '"+this.name+"' to off");
|
||||
this.device.power.off(function(response) {
|
||||
if (response === undefined) {
|
||||
that.log("Error setting power state on the '"+that.name+"'")
|
||||
} else {
|
||||
that.log("Successfully set power state on the '"+that.name+"' to off");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
setBrightness: function(level) {
|
||||
if (!this.device) {
|
||||
this.log("No '"+this.name+"' device found (yet?)");
|
||||
return;
|
||||
}
|
||||
|
||||
var that = this;
|
||||
|
||||
this.log("Setting brightness on the '"+this.name+"' to " + level);
|
||||
this.device.brightness(level, function(response) {
|
||||
if (response === undefined) {
|
||||
that.log("Error setting brightness on the '"+that.name+"'")
|
||||
} else {
|
||||
that.log("Successfully set brightness on the '"+that.name+"' to " + level);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getServices: function() {
|
||||
var that = this;
|
||||
return [{
|
||||
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||
characteristics: [{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: this.name,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of the accessory",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MANUFACTURER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "Wink",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Manufacturer",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MODEL_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "Rev-1",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Model",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.SERIAL_NUMBER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "A1S2NASF88EW",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "SN",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.IDENTIFY_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pw"],
|
||||
format: "bool",
|
||||
initialValue: false,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Identify Accessory",
|
||||
designedMaxLength: 1
|
||||
}]
|
||||
},{
|
||||
sType: types.LIGHTBULB_STYPE,
|
||||
characteristics: [{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: this.name,
|
||||
supportEvents: true,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of service",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.POWER_STATE_CTYPE,
|
||||
onUpdate: function(value) {
|
||||
that.setPowerState(value);
|
||||
},
|
||||
onRead: function(callback) {
|
||||
that.getPowerState(function(powerState){
|
||||
callback(powerState);
|
||||
});
|
||||
},
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "bool",
|
||||
initialValue: 0,
|
||||
supportEvents: true,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Change the power state of the Bulb",
|
||||
designedMaxLength: 1
|
||||
},{
|
||||
cType: types.BRIGHTNESS_CTYPE,
|
||||
onUpdate: function(value) {
|
||||
that.setBrightness(value);
|
||||
},
|
||||
onRead: function(callback) {
|
||||
that.getBrightness(function(level){
|
||||
callback(level);
|
||||
});
|
||||
},
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "int",
|
||||
initialValue: 0,
|
||||
supportEvents: true,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Adjust Brightness of Light",
|
||||
designedMinValue: 0,
|
||||
designedMaxValue: 100,
|
||||
designedMinStep: 1,
|
||||
unit: "%"
|
||||
}]
|
||||
}];
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.accessory = WinkAccessory;
|
||||
module.exports.platform = WinkPlatform;
|
||||
Reference in New Issue
Block a user