mirror of
https://github.com/mtan93/homebridge.git
synced 2026-03-08 05:31:55 +00:00
Compare commits
462 Commits
provider-a
...
osx-launch
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
febed4cae6 | ||
|
|
17f13a51a4 | ||
|
|
d2fd510aa4 | ||
|
|
45af486cfd | ||
|
|
d866f9abb6 | ||
|
|
e72309fab0 | ||
|
|
1ad323b0e8 | ||
|
|
1f7db5e661 | ||
|
|
e2157aed9c | ||
|
|
48e0f8b812 | ||
|
|
b09835600d | ||
|
|
afef06467d | ||
|
|
386636975b | ||
|
|
bfc151e70a | ||
|
|
8d7f986f84 | ||
|
|
997906bab8 | ||
|
|
1a95978e0a | ||
|
|
13e8846138 | ||
|
|
7c7ac5d828 | ||
|
|
70ec9a8891 | ||
|
|
00574f60b4 | ||
|
|
2c36f1745d | ||
|
|
8ebd6ecdd2 | ||
|
|
e9e637c43d | ||
|
|
d7bca45e70 | ||
|
|
e8553c7203 | ||
|
|
626871680c | ||
|
|
42f88864f9 | ||
|
|
0cb57d2a44 | ||
|
|
92667b5666 | ||
|
|
7b24d142b4 | ||
|
|
74e37cc488 | ||
|
|
8fcd5d39a6 | ||
|
|
6604c2b69a | ||
|
|
6e5c35ec88 | ||
|
|
e30b504583 | ||
|
|
7b110191f9 | ||
|
|
74a72c1096 | ||
|
|
5066b108d2 | ||
|
|
6008374b9e | ||
|
|
3c53b319cb | ||
|
|
2d80e5f838 | ||
|
|
b2aa70d40a | ||
|
|
a45779cf48 | ||
|
|
bf125078a1 | ||
|
|
7ce190a7d0 | ||
|
|
915e674583 | ||
|
|
ad62bd4b15 | ||
|
|
15e9219c80 | ||
|
|
38cb94c012 | ||
|
|
ffc4edbc2a | ||
|
|
196cda8063 | ||
|
|
3d03824e6a | ||
|
|
385908fa89 | ||
|
|
f2d22584fc | ||
|
|
97cb18389f | ||
|
|
896d6b40d9 | ||
|
|
c5499c122e | ||
|
|
831480d035 | ||
|
|
286f700622 | ||
|
|
ecf9afee64 | ||
|
|
bb1c193bc0 | ||
|
|
c22c14584d | ||
|
|
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 | ||
|
|
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 | ||
|
|
2b11db4560 | ||
|
|
91f6ccb2d4 | ||
|
|
93ea0deded | ||
|
|
f802d84509 | ||
|
|
24a8dcb8cc | ||
|
|
76a5fd3b7f | ||
|
|
c7ab475dd4 | ||
|
|
6f77e55c7b | ||
|
|
e7f35dfcbb | ||
|
|
cf885b6d29 | ||
|
|
ddf9fa01cf | ||
|
|
73148b060d | ||
|
|
5720e82abd | ||
|
|
6f14108905 | ||
|
|
6fe4fe8249 | ||
|
|
a05a4b6f71 | ||
|
|
2135e7eccb | ||
|
|
dbb130be02 | ||
|
|
72b9175b78 | ||
|
|
65ec517fd8 | ||
|
|
aa823baa8e | ||
|
|
118a4529b9 | ||
|
|
9580d259db | ||
|
|
ace364644e | ||
|
|
0da4fe5d22 | ||
|
|
cd33f2e6c8 | ||
|
|
68f764864d | ||
|
|
267470c871 | ||
|
|
2c118e9649 | ||
|
|
66439f6353 | ||
|
|
773eb8fd0e | ||
|
|
651cdfa786 | ||
|
|
0f89a6ae36 | ||
|
|
ec8b556618 | ||
|
|
eb6c881d28 | ||
|
|
7f753f79f6 | ||
|
|
f3e08b0a15 | ||
|
|
e06e982063 | ||
|
|
f9465ebc8e | ||
|
|
2a430ad75c | ||
|
|
8ceaccfca4 | ||
|
|
d6b3fc7667 | ||
|
|
abd848fd31 | ||
|
|
a041407104 | ||
|
|
d36827f92d | ||
|
|
983f136271 | ||
|
|
13d1ed75cf | ||
|
|
ae2d53262f | ||
|
|
241b22db87 | ||
|
|
8d94366e3c | ||
|
|
950e6a8211 | ||
|
|
f3cab9a529 | ||
|
|
25981deac4 | ||
|
|
6540c25baf | ||
|
|
6f71faf355 | ||
|
|
8131f6936e | ||
|
|
ac0c967ec0 | ||
|
|
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 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -13,4 +13,8 @@ npm-debug.log
|
||||
|
||||
# HomeBridge
|
||||
config.json
|
||||
config.test.json
|
||||
persist/
|
||||
log/
|
||||
|
||||
.AppleDouble
|
||||
|
||||
70
README.md
70
README.md
@@ -1,9 +1,9 @@
|
||||
|
||||
# HomeBridge
|
||||
# Homebridge
|
||||
|
||||
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 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.
|
||||
|
||||
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:
|
||||
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))
|
||||
@@ -11,12 +11,13 @@ Since Siri supports devices added through HomeKit, this means that with HomeBrid
|
||||
* _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))
|
||||
* _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), [Home Assistant](http://home-assistant.io) [LimitlessLED/MiLight/Easybulb](http://www.limitlessled.com/), [LIFx](http://www.lifx.com/))
|
||||
* _Siri, set the movie scene._ ([Logitech Harmony](http://myharmony.com/))
|
||||
|
||||
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.
|
||||
|
||||
# Shim types
|
||||
There are 2 types of shims supported in HomeBridge.
|
||||
There are 2 types of shims supported in Homebridge.
|
||||
|
||||
* Accessory - Individual device
|
||||
* Platform - A full bridge to another system
|
||||
@@ -27,9 +28,9 @@ Accessories are individual devices you would like to bridge to HomeKit. You set
|
||||
|
||||
## Platforms
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
# Why?
|
||||
|
||||
@@ -37,7 +38,7 @@ Technically, the device manufacturers should be the ones implementing the HomeKi
|
||||
|
||||
# Credit
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
# Before you Begin
|
||||
|
||||
@@ -49,31 +50,28 @@ I would call this project a "novelty" in its current form, and is for **intrepid
|
||||
|
||||
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.
|
||||
|
||||
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:
|
||||
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.
|
||||
|
||||
**Note:** If you're running on Linux, you'll need to make sure you have the `libavahi-compat-libdnssd-dev` package installed.
|
||||
|
||||
First, clone this repo:
|
||||
|
||||
$ git clone https://github.com/nfarina/homebridge.git
|
||||
$ cd homebridge
|
||||
$ npm install
|
||||
$ script/bootstrap
|
||||
|
||||
**Node**: You'll need to have NodeJS version 0.12.x or better installed for required submodule `HAP-NodeJS` to load.
|
||||
**Node**: You'll need to have NodeJS version 0.12.x or better installed for required submodule `HAP-NodeJS` to load as well as the `forever` node package..
|
||||
|
||||
The server won't do anything until you've edited your `config.json` file containing your home devices (or _accessories_ in HomeKit parlance) or platforms you wish to make available to iOS. The sample configuration has been copied for you into `config.json`. It includes declarations for all supported accessories and platforms. Remove everything except for the accessories and platforms you'll be using.
|
||||
|
||||
Now you should be able to run the homebridge server:
|
||||
|
||||
$ cd homebridge
|
||||
$ npm run start
|
||||
Starting HomeBridge server...
|
||||
Couldn't find a config.json file [snip]
|
||||
$ script/server
|
||||
|
||||
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.
|
||||
|
||||
Once you've added your devices and/or platforms, you should be able to run the server again and see them initialize:
|
||||
|
||||
$ npm run start
|
||||
Starting HomeBridge server...
|
||||
Starting Homebridge server...
|
||||
Loading 6 accessories...
|
||||
[Speakers] Initializing 'Sonos' accessory...
|
||||
[Coffee Maker] Initializing 'WeMo' accessory...
|
||||
@@ -85,24 +83,42 @@ Once you've added your devices and/or platforms, you should be able to run the s
|
||||
|
||||
Your server is now ready to receive commands from iOS.
|
||||
|
||||
# Installing Homebridge to Run at Boot and in the Background
|
||||
Homebridge can be run at boot and in the background on OS X and any Linux variation that uses SysVinit (/etc/init.d scripts) to launch services. To install homebridge as
|
||||
a service:
|
||||
|
||||
$ script/install
|
||||
|
||||
It should load for you in the background. You can find logs in `log/logs.log`. To uninstall it you can run `script/uninstall`. To restart it you can run `script/restart`.
|
||||
|
||||
# Upgrading
|
||||
|
||||
If you want to upgrade homebridge, simply run:
|
||||
|
||||
$ script/upgrade
|
||||
|
||||
It will pull the newest version from the repo on GitHub and restart itself.
|
||||
|
||||
# Adding your devices to iOS
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
Fortunately, there are now a few apps in the App Store that can manage your HomeKit devices. The most comprehensive one I've used is [MyTouchHome](https://itunes.apple.com/us/app/mytouchhome/id965142360?mt=8&at=11lvmd&ct=mhweb) which costs $2.
|
||||
|
||||
There are also some free apps that work OK. 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.
|
||||
|
||||
If you are a member of the iOS developer program, I highly recommend Apple's [HomeKit Catalog](https://developer.apple.com/library/ios/samplecode/HomeKitCatalog/Introduction/Intro.html) app, as it is reliable and comprehensive and free (and open source).
|
||||
|
||||
## Adding HomeKit Accessories
|
||||
|
||||
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.
|
||||
Once you've gotten a HomeKit app running on your iOS device, you can use it to add your Homebridge devices. The app should "discover" the single accessory "Homebridge", assuming that you're still running the Homebridge server 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 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.
|
||||
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`). This process will create some files in the `persist` directory of the Homebridge server, which stores the pairing relationship.
|
||||
|
||||
# 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 shim in `config-samples.json` is "Speakers".
|
||||
|
||||
# Final Notes
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
var types = require("HAP-NodeJS/accessories/types.js");
|
||||
var types = require("hap-nodejs/accessories/types.js");
|
||||
var AD2USB = require('ad2usb');
|
||||
var CUSTOM_PANEL_LCD_TEXT_CTYPE = "A3E7B8F9-216E-42C1-A21C-97D4E3BE52C8";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
var types = require("HAP-NodeJS/accessories/types.js");
|
||||
var types = require("hap-nodejs/accessories/types.js");
|
||||
var carwings = require("carwingsjs");
|
||||
|
||||
function CarwingsAccessory(log, config) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
var types = require("HAP-NodeJS/accessories/types.js");
|
||||
var types = require("hap-nodejs/accessories/types.js");
|
||||
var elkington = require("elkington");
|
||||
|
||||
function ElkM1Accessory(log, config) {
|
||||
|
||||
76
accessories/FileSensor.js
Normal file
76
accessories/FileSensor.js
Normal file
@@ -0,0 +1,76 @@
|
||||
var Service = require("hap-nodejs").Service;
|
||||
var Characteristic = require("hap-nodejs").Characteristic;
|
||||
var chokidar = require("chokidar");
|
||||
var debug = require("debug")("FileSensorAccessory");
|
||||
var crypto = require("crypto");
|
||||
|
||||
module.exports = {
|
||||
accessory: FileSensorAccessory
|
||||
}
|
||||
|
||||
function FileSensorAccessory(log, config) {
|
||||
this.log = log;
|
||||
|
||||
// url info
|
||||
this.name = config["name"];
|
||||
this.path = config["path"];
|
||||
this.window_seconds = config["window_seconds"] || 5;
|
||||
this.sensor_type = config["sensor_type"] || "m";
|
||||
this.inverse = config["inverse"] || false;
|
||||
|
||||
if(config["sn"]){
|
||||
this.sn = config["sn"];
|
||||
} else {
|
||||
var shasum = crypto.createHash('sha1');
|
||||
shasum.update(this.path);
|
||||
this.sn = shasum.digest('base64');
|
||||
debug('Computed SN ' + this.sn);
|
||||
}
|
||||
}
|
||||
|
||||
FileSensorAccessory.prototype = {
|
||||
|
||||
getServices: function() {
|
||||
|
||||
// you can OPTIONALLY create an information service if you wish to override
|
||||
// the default values for things like serial number, model, etc.
|
||||
var informationService = new Service.AccessoryInformation();
|
||||
|
||||
informationService
|
||||
.setCharacteristic(Characteristic.Name, this.name)
|
||||
.setCharacteristic(Characteristic.Manufacturer, "Homebridge")
|
||||
.setCharacteristic(Characteristic.Model, "File Sensor")
|
||||
.setCharacteristic(Characteristic.SerialNumber, this.sn);
|
||||
|
||||
var service, changeAction;
|
||||
if(this.sensor_type === "c"){
|
||||
service = new Service.ContactSensor();
|
||||
changeAction = function(newState){
|
||||
service.getCharacteristic(Characteristic.ContactSensorState)
|
||||
.setValue(newState ? Characteristic.ContactSensorState.CONTACT_DETECTED : Characteristic.ContactSensorState.CONTACT_NOT_DETECTED);
|
||||
};
|
||||
} else {
|
||||
service = new Service.MotionSensor();
|
||||
changeAction = function(newState){
|
||||
service.getCharacteristic(Characteristic.MotionDetected)
|
||||
.setValue(newState);
|
||||
};
|
||||
}
|
||||
|
||||
var changeHandler = function(path, stats){
|
||||
var d = new Date();
|
||||
if(d.getTime() - stats.mtime.getTime() <= (this.window_seconds * 1000)){
|
||||
var newState = this.inverse ? false : true;
|
||||
changeAction(newState);
|
||||
if(this.timer !== undefined) clearTimeout(this.timer);
|
||||
this.timer = setTimeout(function(){changeAction(!newState);}, this.window_seconds * 1000);
|
||||
}
|
||||
}.bind(this);
|
||||
|
||||
var watcher = chokidar.watch(this.path, {alwaysStat: true});
|
||||
watcher.on('add', changeHandler);
|
||||
watcher.on('change', changeHandler);
|
||||
|
||||
return [informationService, service];
|
||||
}
|
||||
};
|
||||
58
accessories/GenericRS232Device.js
Normal file
58
accessories/GenericRS232Device.js
Normal file
@@ -0,0 +1,58 @@
|
||||
var Service = require("hap-nodejs").Service;
|
||||
var Characteristic = require("hap-nodejs").Characteristic;
|
||||
var SerialPort = require("serialport").SerialPort;
|
||||
|
||||
module.exports = {
|
||||
accessory: GenericRS232DeviceAccessory
|
||||
}
|
||||
|
||||
function GenericRS232DeviceAccessory(log, config) {
|
||||
this.log = log;
|
||||
this.id = config["id"];
|
||||
this.name = config["name"];
|
||||
this.model_name = config["model_name"];
|
||||
this.manufacturer = config["manufacturer"];
|
||||
this.on_command = config["on_command"];
|
||||
this.off_command = config["off_command"];
|
||||
this.device = config["device"];
|
||||
this.baudrate = config["baudrate"];
|
||||
}
|
||||
|
||||
GenericRS232DeviceAccessory.prototype = {
|
||||
setPowerState: function(powerOn, callback) {
|
||||
var that = this;
|
||||
var command = powerOn ? that.on_command : that.off_command;
|
||||
var serialPort = new SerialPort(that.device, { baudrate: that.baudrate }, false);
|
||||
serialPort.open(function (error) {
|
||||
if (error) {
|
||||
callback(new Error('Can not communicate with ' + that.name + " (" + error + ")"))
|
||||
} else {
|
||||
serialPort.write(command, function(err, results) {
|
||||
if (error) {
|
||||
callback(new Error('Can not send power command to ' + that.name + " (" + err + ")"))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getServices: function() {
|
||||
var switchService = new Service.Switch(this.name);
|
||||
var informationService = new Service.AccessoryInformation();
|
||||
|
||||
informationService
|
||||
.setCharacteristic(Characteristic.Manufacturer, this.manufacturer)
|
||||
.setCharacteristic(Characteristic.Model, this.model_name)
|
||||
.setCharacteristic(Characteristic.SerialNumber, this.id);
|
||||
|
||||
switchService
|
||||
.getCharacteristic(Characteristic.On)
|
||||
.on('set', this.setPowerState.bind(this));
|
||||
|
||||
return [informationService, switchService];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.accessory = GenericRS232DeviceAccessory;
|
||||
27
accessories/HomeMatic.js
Executable file → Normal file
27
accessories/HomeMatic.js
Executable file → Normal file
@@ -1,4 +1,4 @@
|
||||
var types = require("HAP-NodeJS/accessories/types.js");
|
||||
var types = require("hap-nodejs/accessories/types.js");
|
||||
var request = require("request");
|
||||
|
||||
function HomeMatic(log, config) {
|
||||
@@ -30,7 +30,31 @@ HomeMatic.prototype = {
|
||||
}
|
||||
});
|
||||
},
|
||||
getPowerState: function(callback) {
|
||||
var that = this;
|
||||
|
||||
this.log("Getting Power State of CCU");
|
||||
request.get({
|
||||
url: "http://"+this.ccuIP+"/config/xmlapi/state.cgi?datapoint_id="+this.ccuID,
|
||||
}, function(err, response, body) {
|
||||
|
||||
if (!err && response.statusCode == 200) {
|
||||
|
||||
//that.log("Response:"+response.body);
|
||||
var responseString = response.body.substring(83,87);
|
||||
//that.log(responseString);
|
||||
switch(responseString){
|
||||
case "true": {modvalue = "1";break;}
|
||||
case "fals": {modvalue = "0";break;}
|
||||
}
|
||||
callback(parseInt(modvalue));
|
||||
that.log("Getting Power State complete.");
|
||||
}
|
||||
else {
|
||||
that.log("Error '"+err+"' getting Power State: " + body);
|
||||
}
|
||||
});
|
||||
},
|
||||
getServices: function() {
|
||||
var that = this;
|
||||
return [{
|
||||
@@ -101,6 +125,7 @@ HomeMatic.prototype = {
|
||||
},{
|
||||
cType: types.POWER_STATE_CTYPE,
|
||||
onUpdate: function(value) { that.setPowerState(value); },
|
||||
onRead: function(callback) { that.getPowerState(callback); },
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "bool",
|
||||
initialValue: false,
|
||||
|
||||
264
accessories/HomeMaticThermo.js
Normal file
264
accessories/HomeMaticThermo.js
Normal file
@@ -0,0 +1,264 @@
|
||||
var types = require("hap-nodejs/accessories/types.js");
|
||||
var request = require("request");
|
||||
|
||||
function HomeMaticThermo(log, config) {
|
||||
this.log = log;
|
||||
this.name = config["name"];
|
||||
this.ccuIDTargetTemp = config["ccu_id_TargetTemp"];
|
||||
this.ccuIDCurrentTemp = config["ccu_id_CurrentTemp"];
|
||||
this.ccuIDControlMode = config["ccu_id_ControlMode"];
|
||||
this.ccuIDManuMode = config["ccu_id_ManuMode"];
|
||||
this.ccuIDAutoMode = config["ccu_id_AutoMode"];
|
||||
this.ccuIP = config["ccu_ip"];
|
||||
}
|
||||
|
||||
HomeMaticThermo.prototype = {
|
||||
|
||||
setTargetTemperature: function(value) {
|
||||
|
||||
var that = this;
|
||||
|
||||
this.log("Setting target Temperature of CCU to " + value);
|
||||
this.log(this.ccuIDTargetTemp + " " + value);
|
||||
|
||||
request.put({
|
||||
url: "http://"+this.ccuIP+"/config/xmlapi/statechange.cgi?ise_id="+this.ccuIDTargetTemp+"&new_value="+ value,
|
||||
}, function(err, response, body) {
|
||||
|
||||
if (!err && response.statusCode == 200) {
|
||||
that.log("State change complete.");
|
||||
}
|
||||
else {
|
||||
that.log("Error '"+err+"' setting Temperature: " + body);
|
||||
}
|
||||
});
|
||||
},
|
||||
getCurrentTemperature: function(callback) {
|
||||
|
||||
var that = this;
|
||||
|
||||
this.log("Getting current Temperature of CCU");
|
||||
request.get({
|
||||
url: "http://"+this.ccuIP+"/config/xmlapi/state.cgi?datapoint_id="+this.ccuIDCurrentTemp,
|
||||
}, function(err, response, body) {
|
||||
|
||||
if (!err && response.statusCode == 200) {
|
||||
|
||||
//that.log("Response:"+response.body);
|
||||
var responseString = response.body.substring(83,87);
|
||||
//that.log(responseString);
|
||||
callback(parseFloat(responseString));
|
||||
//that.log("Getting current temperature complete.");
|
||||
}
|
||||
else {
|
||||
that.log("Error '"+err+"' getting Temperature: " + body);
|
||||
}
|
||||
});
|
||||
},
|
||||
getTargetTemperature: function(callback) {
|
||||
|
||||
var that = this;
|
||||
|
||||
this.log("Getting target Temperature of CCU");
|
||||
request.get({
|
||||
url: "http://"+this.ccuIP+"/config/xmlapi/state.cgi?datapoint_id="+this.ccuIDTargetTemp,
|
||||
}, function(err, response, body) {
|
||||
|
||||
if (!err && response.statusCode == 200) {
|
||||
|
||||
//that.log("Response:"+response.body);
|
||||
var responseString = response.body.substring(83,87);
|
||||
//that.log(responseString);
|
||||
callback(parseFloat(responseString));
|
||||
//that.log("Getting target temperature complete.");
|
||||
}
|
||||
else {
|
||||
that.log("Error '"+err+"' getting Temperature: " + body);
|
||||
}
|
||||
});
|
||||
},
|
||||
getMode: function(callback) {
|
||||
|
||||
var that = this;
|
||||
|
||||
//this.log("Getting target Mode of CCU");
|
||||
//this.log(this.ccuID+ value);
|
||||
|
||||
request.get({
|
||||
url: "http://"+this.ccuIP+"/config/xmlapi/state.cgi?datapoint_id="+this.ccuIDControlMode,
|
||||
}, function(err, response, body) {
|
||||
|
||||
if (!err && response.statusCode == 200) {
|
||||
|
||||
//that.log("Response:"+response.body);
|
||||
var responseInt = response.body.substring(83,84);
|
||||
//that.log(responseString);
|
||||
if (responseInt == 1)
|
||||
{ callback(parseInt("0")); }
|
||||
if (responseInt == 0)
|
||||
{ callback(parseInt("1")); }
|
||||
//that.log("Getting mode complete.");
|
||||
}
|
||||
else {
|
||||
that.log("Error '"+err+"' getting Mode: " + body);
|
||||
}
|
||||
});
|
||||
},
|
||||
setMode: function(value) {
|
||||
|
||||
var that = this;
|
||||
|
||||
//this.log("Seting target Mode of CCU:" + value);
|
||||
var modvalue;
|
||||
var dpID;
|
||||
switch(value) {
|
||||
case 3: {modvalue = "true";dpID=this.ccuIDAutoMode;break;} //auto
|
||||
case 1: {modvalue = "true";dpID=this.ccuIDAutoMode;break;} //heating => auto
|
||||
default: {modvalue = "1";dpID=this.ccuIDManuMode;} //default => off (manual)
|
||||
}
|
||||
|
||||
request.put({
|
||||
url: "http://"+this.ccuIP+"/config/xmlapi/statechange.cgi?ise_id="+dpID+"&new_value="+ modvalue,
|
||||
}, function(err, response, body) {
|
||||
|
||||
if (!err && response.statusCode == 200) {
|
||||
//that.log("Setting Mode complete.");
|
||||
}
|
||||
else {
|
||||
that.log("Error '"+err+"' setting Mode: " + 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: "test",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Manufacturer",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MODEL_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "test",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Model",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.SERIAL_NUMBER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "A1S2NREF88EW",
|
||||
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.THERMOSTAT_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.CURRENTHEATINGCOOLING_CTYPE,
|
||||
onRead: function(callback) { that.getMode(callback); },
|
||||
perms: ["pr","ev"],
|
||||
format: "int",
|
||||
initialValue: 0,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Current Mode",
|
||||
designedMaxLength: 1,
|
||||
designedMinValue: 0,
|
||||
designedMaxValue: 2,
|
||||
designedMinStep: 1,
|
||||
},{
|
||||
cType: types.TARGETHEATINGCOOLING_CTYPE,
|
||||
onRead: function(callback) { that.getMode(callback); },
|
||||
onUpdate: function(value) { that.setMode(value);},
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "int",
|
||||
initialValue: 0,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Target Mode",
|
||||
designedMinValue: 0,
|
||||
designedMaxValue: 3,
|
||||
designedMinStep: 1,
|
||||
},{
|
||||
cType: types.CURRENT_TEMPERATURE_CTYPE,
|
||||
onRead: function(callback) { that.getCurrentTemperature(callback); },
|
||||
onUpdate: null,
|
||||
perms: ["pr","ev"],
|
||||
format: "float",
|
||||
initialValue: 13.0,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Current Temperature",
|
||||
unit: "celsius"
|
||||
},{
|
||||
cType: types.TARGET_TEMPERATURE_CTYPE,
|
||||
onUpdate: function(value) { that.setTargetTemperature(value); },
|
||||
onRead: function(callback) { that.getTargetTemperature(callback); },
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "float",
|
||||
initialValue: 19.0,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Target Temperature",
|
||||
designedMinValue: 4,
|
||||
designedMaxValue: 25,
|
||||
designedMinStep: 0.1,
|
||||
unit: "celsius"
|
||||
},{
|
||||
cType: types.TEMPERATURE_UNITS_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr","ev"],
|
||||
format: "int",
|
||||
initialValue: 0,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Unit"
|
||||
}]
|
||||
}];
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.accessory = HomeMaticThermo;
|
||||
@@ -1,64 +1,41 @@
|
||||
var types = require("HAP-NodeJS/accessories/types.js");
|
||||
var sonos = require('sonos');
|
||||
var types = require("hap-nodejs/accessories/types.js");
|
||||
var Characteristic = require("hap-nodejs").Characteristic;
|
||||
var request = require("request");
|
||||
|
||||
function SonosAccessory(log, config) {
|
||||
function HomeMaticWindow(log, config) {
|
||||
this.log = log;
|
||||
this.name = config["name"];
|
||||
this.playVolume = config["play_volume"];
|
||||
this.device = null;
|
||||
this.search();
|
||||
this.ccuID = config["ccu_id"];
|
||||
this.ccuIP = config["ccu_ip"];
|
||||
}
|
||||
|
||||
SonosAccessory.prototype = {
|
||||
HomeMaticWindow.prototype = {
|
||||
|
||||
search: function() {
|
||||
|
||||
getPowerState: function(callback) {
|
||||
var that = this;
|
||||
|
||||
this.log("Getting Window State of CCU");
|
||||
request.get({
|
||||
url: "http://"+this.ccuIP+"/config/xmlapi/state.cgi?datapoint_id="+this.ccuID,
|
||||
}, function(err, response, body) {
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
if (!err && response.statusCode == 200) {
|
||||
|
||||
//that.log("Response:"+response.body);
|
||||
var responseString = response.body.substring(83,84);
|
||||
//that.log(responseString);
|
||||
switch(responseString){
|
||||
case "0": {callback(Characteristic.ContactSensorState.CONTACT_DETECTED);break;}
|
||||
case "1": {callback(Characteristic.ContactSensorState.CONTACT_NOT_DETECTED);break;}
|
||||
case "2": {callback(Characteristic.ContactSensorState.CONTACT_NOT_DETECTED);break;}
|
||||
}
|
||||
that.log("Getting Window State complete.");
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.device.stop(function(err, success) {
|
||||
that.log("Stop attempt with success: " + success);
|
||||
});
|
||||
}
|
||||
else {
|
||||
that.log("Error '"+err+"' getting Window State: " + body);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getServices: function() {
|
||||
@@ -80,7 +57,7 @@ SonosAccessory.prototype = {
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "Sonos",
|
||||
initialValue: "Homematic",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Manufacturer",
|
||||
@@ -90,7 +67,7 @@ SonosAccessory.prototype = {
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "Rev-1",
|
||||
initialValue: "HM-Sec-RHS",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Model",
|
||||
@@ -117,30 +94,30 @@ SonosAccessory.prototype = {
|
||||
designedMaxLength: 1
|
||||
}]
|
||||
},{
|
||||
sType: types.SWITCH_STYPE,
|
||||
sType: types.CONTACT_SENSOR_STYPE,
|
||||
characteristics: [{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "Speakers",
|
||||
initialValue: this.name,
|
||||
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"],
|
||||
cType: types.CONTACT_SENSOR_STATE_CTYPE,
|
||||
onRead: function(callback) { that.getPowerState(callback); },
|
||||
perms: ["pr","ev"],
|
||||
format: "bool",
|
||||
initialValue: false,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Change the playback state of the sonos",
|
||||
manfDescription: "Get Window state of a Variable",
|
||||
designedMaxLength: 1
|
||||
}]
|
||||
}];
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.accessory = SonosAccessory;
|
||||
module.exports.accessory = HomeMaticWindow;
|
||||
@@ -1,6 +1,11 @@
|
||||
var types = require("HAP-NodeJS/accessories/types.js");
|
||||
var Service = require("hap-nodejs").Service;
|
||||
var Characteristic = require("hap-nodejs").Characteristic;
|
||||
var request = require("request");
|
||||
|
||||
module.exports = {
|
||||
accessory: HttpAccessory
|
||||
}
|
||||
|
||||
function HttpAccessory(log, config) {
|
||||
this.log = log;
|
||||
|
||||
@@ -9,9 +14,6 @@ function HttpAccessory(log, config) {
|
||||
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 = {
|
||||
@@ -26,135 +28,73 @@ HttpAccessory.prototype = {
|
||||
})
|
||||
},
|
||||
|
||||
setPowerState: function(powerOn) {
|
||||
setPowerState: function(powerOn, callback) {
|
||||
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");
|
||||
url = this.on_url;
|
||||
this.log("Setting power state to on");
|
||||
}
|
||||
else {
|
||||
url = this.off_url;
|
||||
this.log("Setting power state to off");
|
||||
}
|
||||
|
||||
this.httpRequest(url, this.http_method, function(error, response, body){
|
||||
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!');
|
||||
this.log('HTTP power function failed: %s', error.message);
|
||||
callback(error);
|
||||
}
|
||||
});
|
||||
|
||||
else {
|
||||
this.log('HTTP power function succeeded!');
|
||||
callback();
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
setBrightness: function(level) {
|
||||
setBrightness: function(level, callback) {
|
||||
var url = this.brightness_url.replace("%b", level)
|
||||
|
||||
this.log("Setting brightness on the '"+this.name+"' to " + level);
|
||||
this.log("Setting brightness to %s", level);
|
||||
|
||||
this.httpRequest(url, this.http_method, function(error, response, body){
|
||||
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!');
|
||||
this.log('HTTP brightness function failed: %s', error);
|
||||
callback(error);
|
||||
}
|
||||
});
|
||||
|
||||
else {
|
||||
this.log('HTTP brightness function succeeded!');
|
||||
callback();
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
|
||||
identify: function(callback) {
|
||||
this.log("Identify requested!");
|
||||
callback(); // 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: "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: "%"
|
||||
}]
|
||||
}];
|
||||
|
||||
// you can OPTIONALLY create an information service if you wish to override
|
||||
// the default values for things like serial number, model, etc.
|
||||
var informationService = new Service.AccessoryInformation();
|
||||
|
||||
informationService
|
||||
.setCharacteristic(Characteristic.Manufacturer, "HTTP Manufacturer")
|
||||
.setCharacteristic(Characteristic.Model, "HTTP Model")
|
||||
.setCharacteristic(Characteristic.SerialNumber, "HTTP Serial Number");
|
||||
|
||||
var lightbulbService = new Service.Lightbulb();
|
||||
|
||||
lightbulbService
|
||||
.getCharacteristic(Characteristic.On)
|
||||
.on('set', this.setPowerState.bind(this));
|
||||
|
||||
lightbulbService
|
||||
.addCharacteristic(new Characteristic.Brightness())
|
||||
.on('set', this.setBrightness.bind(this));
|
||||
|
||||
return [informationService, lightbulbService];
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.accessory = HttpAccessory;
|
||||
|
||||
71
accessories/HttpHygrometer.js
Normal file
71
accessories/HttpHygrometer.js
Normal file
@@ -0,0 +1,71 @@
|
||||
var Service = require("hap-nodejs").Service;
|
||||
var Characteristic = require("hap-nodejs").Characteristic;
|
||||
var request = require("request");
|
||||
|
||||
module.exports = {
|
||||
accessory: HygrometerAccessory
|
||||
}
|
||||
|
||||
function HygrometerAccessory(log, config) {
|
||||
this.log = log;
|
||||
|
||||
// url info
|
||||
this.url = config["url"];
|
||||
this.http_method = config["http_method"];
|
||||
}
|
||||
|
||||
|
||||
HygrometerAccessory.prototype = {
|
||||
|
||||
httpRequest: function(url, method, callback) {
|
||||
request({
|
||||
url: url,
|
||||
method: method
|
||||
},
|
||||
function (error, response, body) {
|
||||
callback(error, response, body)
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
identify: function(callback) {
|
||||
this.log("Identify requested!");
|
||||
callback(); // success
|
||||
},
|
||||
|
||||
getCurrentRelativeHumidity: function (callback) {
|
||||
var that = this;
|
||||
that.log ("getting CurrentCurrentRelativeHumidity");
|
||||
|
||||
this.httpRequest(this.url, this.http_method, function(error, response, body) {
|
||||
if (error) {
|
||||
this.log('HTTP function failed: %s', error);
|
||||
callback(error);
|
||||
}
|
||||
else {
|
||||
this.log('HTTP function succeeded - %s', body);
|
||||
callback(null, Number(body));
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
getServices: function() {
|
||||
|
||||
// you can OPTIONALLY create an information service if you wish to override
|
||||
// the default values for things like serial number, model, etc.
|
||||
var informationService = new Service.AccessoryInformation();
|
||||
|
||||
informationService
|
||||
.setCharacteristic(Characteristic.Manufacturer, "HTTP Manufacturer")
|
||||
.setCharacteristic(Characteristic.Model, "HTTP Hygrometer")
|
||||
.setCharacteristic(Characteristic.SerialNumber, "HTTP Serial Number");
|
||||
|
||||
var humidityService = new Service.HumiditySensor();
|
||||
|
||||
humidityService
|
||||
.getCharacteristic(Characteristic.CurrentRelativeHumidity)
|
||||
.on('get', this.getCurrentRelativeHumidity.bind(this));
|
||||
|
||||
return [informationService, humidityService];
|
||||
}
|
||||
};
|
||||
79
accessories/HttpThermometer.js
Normal file
79
accessories/HttpThermometer.js
Normal file
@@ -0,0 +1,79 @@
|
||||
var Service = require("hap-nodejs").Service;
|
||||
var Characteristic = require("hap-nodejs").Characteristic;
|
||||
var request = require("request");
|
||||
|
||||
module.exports = {
|
||||
accessory: ThermometerAccessory
|
||||
}
|
||||
|
||||
function ThermometerAccessory(log, config) {
|
||||
this.log = log;
|
||||
|
||||
// url info
|
||||
this.url = config["url"];
|
||||
this.http_method = config["http_method"];
|
||||
}
|
||||
|
||||
|
||||
ThermometerAccessory.prototype = {
|
||||
|
||||
httpRequest: function(url, method, callback) {
|
||||
request({
|
||||
url: url,
|
||||
method: method
|
||||
},
|
||||
function (error, response, body) {
|
||||
callback(error, response, body)
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
identify: function(callback) {
|
||||
this.log("Identify requested!");
|
||||
callback(); // success
|
||||
},
|
||||
|
||||
getCurrentTemperature: function (callback) {
|
||||
var that = this;
|
||||
that.log ("getting CurrentTemperature");
|
||||
|
||||
|
||||
this.httpRequest(this.url, this.http_method, function(error, response, body) {
|
||||
if (error) {
|
||||
this.log('HTTP function failed: %s', error);
|
||||
callback(error);
|
||||
}
|
||||
else {
|
||||
this.log('HTTP function succeeded - %s', body);
|
||||
callback(null, Number(body));
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
getTemperatureUnits: function (callback) {
|
||||
var that = this;
|
||||
that.log ("getTemperature Units");
|
||||
// 1 = F and 0 = C
|
||||
callback (null, 0);
|
||||
},
|
||||
|
||||
getServices: function() {
|
||||
|
||||
// you can OPTIONALLY create an information service if you wish to override
|
||||
// the default values for things like serial number, model, etc.
|
||||
var informationService = new Service.AccessoryInformation();
|
||||
|
||||
informationService
|
||||
.setCharacteristic(Characteristic.Manufacturer, "HTTP Manufacturer")
|
||||
.setCharacteristic(Characteristic.Model, "HTTP Thermometer")
|
||||
.setCharacteristic(Characteristic.SerialNumber, "HTTP Serial Number");
|
||||
|
||||
var temperatureService = new Service.TemperatureSensor();
|
||||
|
||||
temperatureService
|
||||
.getCharacteristic(Characteristic.CurrentTemperature)
|
||||
.on('get', this.getCurrentTemperature.bind(this));
|
||||
|
||||
return [informationService, temperatureService];
|
||||
}
|
||||
};
|
||||
221
accessories/Hyperion.js
Normal file
221
accessories/Hyperion.js
Normal file
@@ -0,0 +1,221 @@
|
||||
var types = require("hap-nodejs/accessories/types.js");
|
||||
var net = require('net');
|
||||
var Color = require('color');
|
||||
|
||||
function HyperionAccessory(log, config) {
|
||||
this.log = log;
|
||||
this.host = config["host"];
|
||||
this.port = config["port"];
|
||||
this.name = config["name"];
|
||||
this.color = Color().hsv([0, 0, 0]);
|
||||
this.prevColor = Color().hsv([0,0,100]);
|
||||
}
|
||||
|
||||
|
||||
HyperionAccessory.prototype = {
|
||||
|
||||
sendHyperionCommand: function(command, cmdParams, priority) {
|
||||
var that = this;
|
||||
var client = new net.Socket();
|
||||
var data = {};
|
||||
|
||||
if (typeof priority === 'undefined') { priority = 100; }
|
||||
|
||||
switch (command) {
|
||||
case 'color':
|
||||
data = {"command":"color", "priority":priority,"color":cmdParams};
|
||||
break;
|
||||
case 'blacklevel':
|
||||
data = {"command":"transform","transform":{"blacklevel":cmdParams}}
|
||||
break;
|
||||
default:
|
||||
that.log("Hyperion command not found");
|
||||
return;
|
||||
}
|
||||
|
||||
//that.log(JSON.stringify(data));
|
||||
|
||||
client.connect(that.port, that.host, function() {
|
||||
client.write(JSON.stringify(data) + "\n");
|
||||
});
|
||||
|
||||
client.on('data', function(data){
|
||||
that.log("Response: " + data.toString().trim());
|
||||
that.log("***** Color HSV:" + that.color.hsvArray() + "*****");
|
||||
that.log("***** Color RGB:" + that.color.rgbArray() + "*****");
|
||||
client.end();
|
||||
});
|
||||
},
|
||||
|
||||
setPowerState: function(powerOn) {
|
||||
var that = this;
|
||||
|
||||
if (powerOn) {
|
||||
that.log("Setting power state on the '"+that.name+"' to on");
|
||||
that.color.rgb(that.prevColor.rgb());
|
||||
that.sendHyperionCommand('color', that.color.rgbArray());
|
||||
} else {
|
||||
that.log("Setting power state on the '"+that.name+"' to off");
|
||||
that.prevColor.rgb(that.color.rgb());
|
||||
that.color.value(0);
|
||||
that.sendHyperionCommand('color', that.color.rgbArray());
|
||||
that.sendHyperionCommand('blacklevel', [0,0,0]);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
setBrightness: function(level) {
|
||||
var that = this;
|
||||
|
||||
that.color.value(level);
|
||||
that.log("Setting brightness on the '"+that.name+"' to '" + level + "'");
|
||||
that.sendHyperionCommand('color', that.color.rgbArray());
|
||||
|
||||
},
|
||||
|
||||
setHue: function(level) {
|
||||
var that = this;
|
||||
|
||||
that.color.hue(level);
|
||||
that.prevColor.hue(level);
|
||||
that.log("Setting hue on the '"+that.name+"' to '" + level + "'");
|
||||
that.sendHyperionCommand('color', that.color.rgbArray());
|
||||
|
||||
},
|
||||
|
||||
setSaturation: function(level) {
|
||||
var that = this;
|
||||
|
||||
that.color.saturationv(level);
|
||||
that.prevColor.saturationv(level);
|
||||
that.log("Setting saturation on the '"+that.name+"' to '" + level + "'");
|
||||
that.sendHyperionCommand('color', that.color.rgbArray());
|
||||
|
||||
},
|
||||
|
||||
getServices: function() {
|
||||
var that = this;
|
||||
return [{
|
||||
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||
characteristics: [{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: that.name,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of the accessory",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MANUFACTURER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "Hyperion",
|
||||
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: "DEADBEEF",
|
||||
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: that.name,
|
||||
supportEvents: true,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of service",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.POWER_STATE_CTYPE,
|
||||
onUpdate: function(value) { that.setPowerState(value); },
|
||||
onRead: ((that.color.value() > 0) ? true : false),
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "bool",
|
||||
initialValue: 0,
|
||||
supportEvents: true,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Turn on the light",
|
||||
designedMaxLength: 1
|
||||
},{
|
||||
cType: types.BRIGHTNESS_CTYPE,
|
||||
onUpdate: function(value) { that.setBrightness(value); },
|
||||
onRead: that.color.value(),
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "int",
|
||||
initialValue: that.color.value(),
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Adjust Brightness",
|
||||
designedMinValue: 0,
|
||||
designedMaxValue: 100,
|
||||
designedMinStep: 1,
|
||||
unit: "%"
|
||||
},{
|
||||
cType: types.HUE_CTYPE,
|
||||
onUpdate: function(value) { that.setHue(value) },
|
||||
onRead: that.color.hue(),
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "int",
|
||||
initialValue: that.color.hue(),
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Adjust Hue",
|
||||
designedMinValue: 0,
|
||||
designedMaxValue: 360,
|
||||
designedMinStep: 1,
|
||||
unit: "arcdegrees"
|
||||
},{
|
||||
cType: types.SATURATION_CTYPE,
|
||||
onUpdate: function(value) { that.setSaturation(value) },
|
||||
onRead: that.color.saturationv(),
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "int",
|
||||
initialValue: that.color.saturationv(),
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Adjust Saturation",
|
||||
designedMinValue: 0,
|
||||
designedMaxValue: 100,
|
||||
designedMinStep: 1,
|
||||
unit: "%"
|
||||
}]
|
||||
}];
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.accessory = HyperionAccessory;
|
||||
@@ -1,4 +1,4 @@
|
||||
var types = require("HAP-NodeJS/accessories/types.js");
|
||||
var types = require("hap-nodejs/accessories/types.js");
|
||||
var request = require("request");
|
||||
|
||||
// This seems to be the "id" of the official LiftMaster iOS app
|
||||
@@ -89,10 +89,10 @@ LiftMasterAccessory.prototype = {
|
||||
for (var i=0; i<devices.length; i++) {
|
||||
var device = devices[i];
|
||||
|
||||
if (device["MyQDeviceTypeName"] == "GarageDoorOpener") {
|
||||
if (device["MyQDeviceTypeName"] == "GarageDoorOpener" || device["MyQDeviceTypeName"] == "VGDO") {
|
||||
|
||||
// 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) {
|
||||
if (!that.requiredDeviceId) {
|
||||
var thisDeviceId = device.MyQDeviceId;
|
||||
var thisDoorName = "Unknown";
|
||||
for (var j = 0; j < device.Attributes.length; j ++) {
|
||||
@@ -103,6 +103,7 @@ LiftMasterAccessory.prototype = {
|
||||
}
|
||||
}
|
||||
foundDoors.push(thisDeviceId + " - " + thisDoorName);
|
||||
that.deviceId = thisDeviceId;
|
||||
}
|
||||
|
||||
// We specified a door ID, sanity check to make sure it's the one we expected
|
||||
@@ -302,4 +303,4 @@ LiftMasterAccessory.prototype = {
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.accessory = LiftMasterAccessory;
|
||||
module.exports.accessory = LiftMasterAccessory;
|
||||
|
||||
@@ -1,196 +1,80 @@
|
||||
var types = require("HAP-NodeJS/accessories/types.js");
|
||||
var Service = require("hap-nodejs").Service;
|
||||
var Characteristic = require("hap-nodejs").Characteristic;
|
||||
var request = require("request");
|
||||
|
||||
module.exports = {
|
||||
accessory: LockitronAccessory
|
||||
}
|
||||
|
||||
function LockitronAccessory(log, config) {
|
||||
this.log = log;
|
||||
this.name = config["name"];
|
||||
this.lockID = config["lock_id"];
|
||||
this.accessToken = config["api_token"];
|
||||
this.lockID = config["lock_id"];
|
||||
|
||||
this.service = new Service.LockMechanism(this.name);
|
||||
|
||||
this.service
|
||||
.getCharacteristic(Characteristic.LockCurrentState)
|
||||
.on('get', this.getState.bind(this));
|
||||
|
||||
this.service
|
||||
.getCharacteristic(Characteristic.LockTargetState)
|
||||
.on('get', this.getState.bind(this))
|
||||
.on('set', this.setState.bind(this));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
},
|
||||
LockitronAccessory.prototype.getState = function(callback) {
|
||||
this.log("Getting current state...");
|
||||
|
||||
setState: function(state) {
|
||||
this.log("Set state to " + state);
|
||||
request.get({
|
||||
url: "https://api.lockitron.com/v2/locks/"+this.lockID,
|
||||
qs: { access_token: this.accessToken }
|
||||
}, function(err, response, body) {
|
||||
|
||||
if (!err && response.statusCode == 200) {
|
||||
var json = JSON.parse(body);
|
||||
var state = json.state; // "lock" or "unlock"
|
||||
this.log("Lock state is %s", state);
|
||||
var locked = state == "lock"
|
||||
callback(null, locked); // success
|
||||
}
|
||||
else {
|
||||
this.log("Error getting state (status code %s): %s", response.statusCode, err);
|
||||
callback(err);
|
||||
}
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
LockitronAccessory.prototype.setState = function(state, callback) {
|
||||
var lockitronState = (state == Characteristic.LockTargetState.SECURED) ? "lock" : "unlock";
|
||||
|
||||
var lockitronState = (state == 1) ? "lock" : "unlock";
|
||||
var that = this;
|
||||
this.log("Set state to %s", lockitronState);
|
||||
|
||||
var query = {
|
||||
access_token: this.accessToken,
|
||||
state: lockitronState
|
||||
};
|
||||
request.put({
|
||||
url: "https://api.lockitron.com/v2/locks/"+this.lockID,
|
||||
qs: { access_token: this.accessToken, state: lockitronState }
|
||||
}, function(err, response, body) {
|
||||
|
||||
request.put({
|
||||
url: "https://api.lockitron.com/v2/locks/"+this.lockID,
|
||||
qs: query
|
||||
}, function(err, response, body) {
|
||||
if (!err && response.statusCode == 200) {
|
||||
this.log("State change complete.");
|
||||
|
||||
// we succeeded, so update the "current" state as well
|
||||
var currentState = (state == Characteristic.LockTargetState.SECURED) ?
|
||||
Characteristic.LockCurrentState.SECURED : Characteristic.LockCurrentState.UNSECURED;
|
||||
|
||||
this.service
|
||||
.setCharacteristic(Characteristic.LockCurrentState, currentState);
|
||||
|
||||
callback(null); // success
|
||||
}
|
||||
else {
|
||||
this.log("Error '%s' setting lock state. Response: %s", err, body);
|
||||
callback(err || new Error("Error setting lock state."));
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
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;
|
||||
LockitronAccessory.prototype.getServices = function() {
|
||||
return [this.service];
|
||||
}
|
||||
|
||||
119
accessories/Tesla.js
Normal file
119
accessories/Tesla.js
Normal file
@@ -0,0 +1,119 @@
|
||||
var types = require("hap-nodejs/accessories/types.js");
|
||||
var tesla = require("teslams");
|
||||
|
||||
function TeslaAccessory(log, config) {
|
||||
this.log = log;
|
||||
this.name = config["name"];
|
||||
this.username = config["username"];
|
||||
this.password = config["password"];
|
||||
}
|
||||
|
||||
TeslaAccessory.prototype = {
|
||||
|
||||
setPowerState: function(powerOn) {
|
||||
var that = this;
|
||||
|
||||
tesla.get_vid({email: this.username, password: this.password}, function(vehicle) {
|
||||
|
||||
if (powerOn) {
|
||||
tesla.auto_conditioning({id:vehicle, climate: 'start'}, function(response) {
|
||||
if (response.result)
|
||||
that.log("Started climate control.");
|
||||
else
|
||||
that.log("Error starting climate control: " + response.reason);
|
||||
});
|
||||
}
|
||||
else {
|
||||
tesla.auto_conditioning({id:vehicle, climate: 'stop'}, function(response) {
|
||||
if (response.result)
|
||||
that.log("Stopped climate control.");
|
||||
else
|
||||
that.log("Error stopping climate control: " + response.reason);
|
||||
});
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
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: "Tesla",
|
||||
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 = TeslaAccessory;
|
||||
@@ -1,139 +1,169 @@
|
||||
var types = require("HAP-NodeJS/accessories/types.js");
|
||||
var Service = require("hap-nodejs").Service;
|
||||
var Characteristic = require("hap-nodejs").Characteristic;
|
||||
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
|
||||
module.exports = {
|
||||
accessory: WeMoAccessory
|
||||
}
|
||||
|
||||
function WeMoAccessory(log, config) {
|
||||
this.log = log;
|
||||
this.name = config["name"];
|
||||
this.wemoName = config["wemo_name"];
|
||||
this.device = null;
|
||||
this.service = config["service"] || "Switch";
|
||||
this.wemoName = config["wemo_name"] || this.name; // fallback to "name" if you didn't specify an exact "wemo_name"
|
||||
this.device = null; // instance of WeMo, for controlling the discovered device
|
||||
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;
|
||||
WeMoAccessory.prototype.search = function() {
|
||||
wemo.Search(this.wemoName, function(err, device) {
|
||||
if (!err && device) {
|
||||
this.log("Found '"+this.wemoName+"' device at " + device.ip);
|
||||
this.device = new wemo(device.ip, device.port);
|
||||
}
|
||||
else {
|
||||
this.log("Error finding device '" + this.wemoName + "': " + err);
|
||||
this.log("Continuing search for WeMo device with exact name '" + this.wemoName + "'...");
|
||||
this.search();
|
||||
}
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
var binaryState = powerOn ? 1 : 0;
|
||||
var that = this;
|
||||
WeMoAccessory.prototype.getMotion = function(callback) {
|
||||
|
||||
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
|
||||
}]
|
||||
}];
|
||||
if (!this.device) {
|
||||
this.log("No '%s' device found (yet?)", this.wemoName);
|
||||
callback(new Error("Device not found"), false);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.accessory = WeMoAccessory;
|
||||
this.log("Getting motion state on the '%s'...", this.wemoName);
|
||||
|
||||
this.device.getBinaryState(function(err, result) {
|
||||
if (!err) {
|
||||
var binaryState = parseInt(result);
|
||||
var powerOn = binaryState > 0;
|
||||
this.log("Motion state for the '%s' is %s", this.wemoName, binaryState);
|
||||
callback(null, powerOn);
|
||||
}
|
||||
else {
|
||||
this.log("Error getting motion state on the '%s': %s", this.wemoName, err.message);
|
||||
callback(err);
|
||||
}
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
WeMoAccessory.prototype.getPowerOn = function(callback) {
|
||||
|
||||
if (!this.device) {
|
||||
this.log("No '%s' device found (yet?)", this.wemoName);
|
||||
callback(new Error("Device not found"), false);
|
||||
return;
|
||||
}
|
||||
|
||||
this.log("Getting power state on the '%s'...", this.wemoName);
|
||||
|
||||
this.device.getBinaryState(function(err, result) {
|
||||
if (!err) {
|
||||
var binaryState = parseInt(result);
|
||||
var powerOn = binaryState > 0;
|
||||
this.log("Power state for the '%s' is %s", this.wemoName, binaryState);
|
||||
callback(null, powerOn);
|
||||
}
|
||||
else {
|
||||
this.log("Error getting power state on the '%s': %s", this.wemoName, err.message);
|
||||
callback(err);
|
||||
}
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
WeMoAccessory.prototype.setPowerOn = function(powerOn, callback) {
|
||||
|
||||
if (!this.device) {
|
||||
this.log("No '%s' device found (yet?)", this.wemoName);
|
||||
callback(new Error("Device not found"));
|
||||
return;
|
||||
}
|
||||
|
||||
var binaryState = powerOn ? 1 : 0; // wemo langauge
|
||||
this.log("Setting power state on the '%s' to %s", this.wemoName, binaryState);
|
||||
|
||||
this.device.setBinaryState(binaryState, function(err, result) {
|
||||
if (!err) {
|
||||
this.log("Successfully set power state on the '%s' to %s", this.wemoName, binaryState);
|
||||
callback(null);
|
||||
}
|
||||
else {
|
||||
this.log("Error setting power state to %s on the '%s'", binaryState, this.wemoName);
|
||||
callback(err);
|
||||
}
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
WeMoAccessory.prototype.setTargetDoorState = function(targetDoorState, callback) {
|
||||
|
||||
if (!this.device) {
|
||||
this.log("No '%s' device found (yet?)", this.wemoName);
|
||||
callback(new Error("Device not found"));
|
||||
return;
|
||||
}
|
||||
|
||||
this.log("Activating WeMo switch '%s'", this.wemoName);
|
||||
|
||||
this.device.setBinaryState(1, function(err, result) {
|
||||
if (!err) {
|
||||
this.log("Successfully activated WeMo switch '%s'", this.wemoName);
|
||||
callback(null);
|
||||
}
|
||||
else {
|
||||
this.log("Error activating WeMo switch '%s'", this.wemoName);
|
||||
callback(err);
|
||||
}
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
WeMoAccessory.prototype.getServices = function() {
|
||||
|
||||
if (this.service == "Switch") {
|
||||
var switchService = new Service.Switch(this.name);
|
||||
|
||||
switchService
|
||||
.getCharacteristic(Characteristic.On)
|
||||
.on('get', this.getPowerOn.bind(this))
|
||||
.on('set', this.setPowerOn.bind(this));
|
||||
|
||||
return [switchService];
|
||||
}
|
||||
else if (this.service == "GarageDoor") {
|
||||
var garageDoorService = new Service.GarageDoorOpener("Garage Door Opener");
|
||||
|
||||
garageDoorService
|
||||
.getCharacteristic(Characteristic.TargetDoorState)
|
||||
.on('set', this.setTargetDoorState.bind(this));
|
||||
|
||||
return [garageDoorService];
|
||||
}
|
||||
else if (this.service == "Light") {
|
||||
var lightbulbService = new Service.Lightbulb(this.name);
|
||||
|
||||
lightbulbService
|
||||
.getCharacteristic(Characteristic.On)
|
||||
.on('get', this.getPowerOn.bind(this))
|
||||
.on('set', this.setPowerOn.bind(this));
|
||||
|
||||
return [lightbulbService];
|
||||
}
|
||||
else if (this.service == "MotionSensor") {
|
||||
var motionSensorService = new Service.MotionSensor(this.name);
|
||||
|
||||
motionSensorService
|
||||
.getCharacteristic(Characteristic.MotionDetected)
|
||||
.on('get', this.getMotion.bind(this));
|
||||
|
||||
return [motionSensorService];
|
||||
}
|
||||
else {
|
||||
throw new Error("Unknown service type '%s'", this.service);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
var types = require("HAP-NodeJS/accessories/types.js");
|
||||
var types = require("hap-nodejs/accessories/types.js");
|
||||
var request = require("request");
|
||||
|
||||
function X10(log, config) {
|
||||
|
||||
@@ -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;
|
||||
130
accessories/iControl.js
Normal file
130
accessories/iControl.js
Normal file
@@ -0,0 +1,130 @@
|
||||
var iControl = require('node-icontrol').iControl;
|
||||
var Service = require("hap-nodejs").Service;
|
||||
var Characteristic = require("hap-nodejs").Characteristic;
|
||||
|
||||
module.exports = {
|
||||
accessory: iControlAccessory
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a Security System accessory for an iControl-based security system like Xfinity Home.
|
||||
*/
|
||||
|
||||
function iControlAccessory(log, config) {
|
||||
this.log = log;
|
||||
|
||||
this.iControl = new iControl({
|
||||
system: iControl.Systems[config.system],
|
||||
email: config.email,
|
||||
password: config.password,
|
||||
pinCode: config.pin
|
||||
});
|
||||
|
||||
this.iControl.on('change', this._handleChange.bind(this));
|
||||
this.iControl.on('error', this._handleError.bind(this));
|
||||
|
||||
this.log("Logging into iControl...");
|
||||
this.iControl.login();
|
||||
|
||||
this._securitySystem = new Service.SecuritySystem("Security System");
|
||||
|
||||
this._securitySystem
|
||||
.getCharacteristic(Characteristic.SecuritySystemTargetState)
|
||||
.on('get', this._getTargetState.bind(this))
|
||||
.on('set', this._setTargetState.bind(this));
|
||||
|
||||
this._securitySystem
|
||||
.getCharacteristic(Characteristic.SecuritySystemCurrentState)
|
||||
.on('get', this._getCurrentState.bind(this));
|
||||
}
|
||||
|
||||
iControlAccessory.prototype._getTargetState = function(callback) {
|
||||
this.iControl.getArmState(function(err, armState) {
|
||||
if (err) return callback(err);
|
||||
|
||||
var currentState = this._getHomeKitStateFromArmState(armState);
|
||||
callback(null, currentState);
|
||||
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
iControlAccessory.prototype._getCurrentState = function(callback) {
|
||||
this.iControl.getArmState(function(err, armState) {
|
||||
if (err) return callback(err);
|
||||
|
||||
var currentState = this._getHomeKitStateFromArmState(armState);
|
||||
callback(null, currentState);
|
||||
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
iControlAccessory.prototype._setTargetState = function(targetState, callback, context) {
|
||||
if (context == "internal") return callback(null); // we set this state ourself, no need to react to it
|
||||
|
||||
var armState = this._getArmStateFromHomeKitState(targetState);
|
||||
this.log("Setting target state to %s", armState);
|
||||
|
||||
this.iControl.setArmState(armState, function(err) {
|
||||
if (err) return callback(err);
|
||||
|
||||
this.log("Successfully set target state to %s", armState);
|
||||
|
||||
// also update current state
|
||||
this._securitySystem
|
||||
.getCharacteristic(Characteristic.SecuritySystemCurrentState)
|
||||
.setValue(targetState);
|
||||
|
||||
callback(null); // success!
|
||||
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
iControlAccessory.prototype._handleChange = function(armState) {
|
||||
this.log("Arm state changed to %s", armState);
|
||||
|
||||
var homeKitState = this._getHomeKitStateFromArmState(armState);
|
||||
|
||||
this._securitySystem
|
||||
.getCharacteristic(Characteristic.SecuritySystemCurrentState)
|
||||
.setValue(homeKitState);
|
||||
|
||||
this._securitySystem
|
||||
.getCharacteristic(Characteristic.SecuritySystemTargetState)
|
||||
.setValue(homeKitState, null, "internal"); // these characteristics happen to share underlying values
|
||||
}
|
||||
|
||||
iControlAccessory.prototype._handleError = function(err) {
|
||||
this.log(err.message);
|
||||
}
|
||||
|
||||
iControlAccessory.prototype.getServices = function() {
|
||||
return [this._securitySystem];
|
||||
}
|
||||
|
||||
iControlAccessory.prototype._getHomeKitStateFromArmState = function(armState) {
|
||||
switch (armState) {
|
||||
case "disarmed": return Characteristic.SecuritySystemCurrentState.DISARMED;
|
||||
case "away": return Characteristic.SecuritySystemCurrentState.AWAY_ARM;
|
||||
case "night": return Characteristic.SecuritySystemCurrentState.NIGHT_ARM;
|
||||
case "stay": return Characteristic.SecuritySystemCurrentState.STAY_ARM;
|
||||
}
|
||||
}
|
||||
|
||||
iControlAccessory.prototype._getArmStateFromHomeKitState = function(homeKitState) {
|
||||
switch (homeKitState) {
|
||||
case Characteristic.SecuritySystemCurrentState.DISARMED: return "disarmed";
|
||||
case Characteristic.SecuritySystemCurrentState.AWAY_ARM: return "away";
|
||||
case Characteristic.SecuritySystemCurrentState.NIGHT_ARM: return "night";
|
||||
case Characteristic.SecuritySystemCurrentState.STAY_ARM: return "stay";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* TESTING
|
||||
*/
|
||||
|
||||
if (require.main === module) {
|
||||
var config = JSON.parse(require('fs').readFileSync("config.json")).accessories[0];
|
||||
var accessory = new iControlAccessory(console.log, config);
|
||||
}
|
||||
1032
accessories/knxdevice.js
Normal file
1032
accessories/knxdevice.js
Normal file
File diff suppressed because it is too large
Load Diff
89
accessories/mpdclient.js
Normal file
89
accessories/mpdclient.js
Normal file
@@ -0,0 +1,89 @@
|
||||
var Service = require("hap-nodejs").Service;
|
||||
var Characteristic = require("hap-nodejs").Characteristic;
|
||||
var request = require("request");
|
||||
var komponist = require('komponist')
|
||||
|
||||
module.exports = {
|
||||
accessory: MpdClient
|
||||
}
|
||||
|
||||
function MpdClient(log, config) {
|
||||
this.log = log;
|
||||
this.host = config["host"] || 'localhost';
|
||||
this.port = config["port"] || 6600;
|
||||
}
|
||||
|
||||
MpdClient.prototype = {
|
||||
|
||||
setPowerState: function(powerOn, callback) {
|
||||
|
||||
var log = this.log;
|
||||
|
||||
komponist.createConnection(this.port, this.host, function(error, client) {
|
||||
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
if (powerOn) {
|
||||
client.play(function(error) {
|
||||
log("start playing");
|
||||
client.destroy();
|
||||
callback(error);
|
||||
});
|
||||
} else {
|
||||
client.stop(function(error) {
|
||||
log("stop playing");
|
||||
client.destroy();
|
||||
callback(error);
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
},
|
||||
|
||||
getPowerState: function(callback) {
|
||||
|
||||
komponist.createConnection(this.port, this.host, function(error, client) {
|
||||
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
client.status(function(error, status) {
|
||||
|
||||
client.destroy();
|
||||
|
||||
if (status['state'] == 'play') {
|
||||
callback(error, 1);
|
||||
} else {
|
||||
callback(error, 0);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
},
|
||||
|
||||
identify: function(callback) {
|
||||
this.log("Identify requested!");
|
||||
callback(); // success
|
||||
},
|
||||
|
||||
getServices: function() {
|
||||
|
||||
var informationService = new Service.AccessoryInformation();
|
||||
|
||||
informationService
|
||||
.setCharacteristic(Characteristic.Manufacturer, "MPD")
|
||||
.setCharacteristic(Characteristic.Model, "MPD Client")
|
||||
.setCharacteristic(Characteristic.SerialNumber, "81536334");
|
||||
|
||||
var switchService = new Service.Switch();
|
||||
|
||||
switchService.getCharacteristic(Characteristic.On)
|
||||
.on('get', this.getPowerState.bind(this))
|
||||
.on('set', this.setPowerState.bind(this));
|
||||
|
||||
return [informationService, switchService];
|
||||
}
|
||||
};
|
||||
286
app.js
286
app.js
@@ -1,10 +1,24 @@
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var storage = require('node-persist');
|
||||
var crypto = require('crypto');
|
||||
var hap = require("hap-nodejs");
|
||||
var uuid = require("hap-nodejs").uuid;
|
||||
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;
|
||||
|
||||
console.log("Starting HomeBridge server...");
|
||||
|
||||
console.log("_____________________________________________________________________");
|
||||
console.log("IMPORTANT: Homebridge is in the middle of some big changes.");
|
||||
console.log(" Read more about it here:");
|
||||
console.log(" https://github.com/nfarina/homebridge/wiki/Migration-Guide");
|
||||
console.log("_____________________________________________________________________");
|
||||
console.log("");
|
||||
|
||||
// Look for the configuration file
|
||||
var configPath = path.join(__dirname, "config.json");
|
||||
|
||||
@@ -14,176 +28,196 @@ if (!fs.existsSync(configPath)) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Initialize persistent storage
|
||||
storage.initSync();
|
||||
// Initialize HAP-NodeJS
|
||||
hap.init();
|
||||
|
||||
// Load up the configuration file
|
||||
var config = JSON.parse(fs.readFileSync(configPath));
|
||||
var config;
|
||||
try {
|
||||
config = JSON.parse(fs.readFileSync(configPath));
|
||||
}
|
||||
catch (err) {
|
||||
console.log("There was a problem reading your config.json file.");
|
||||
console.log("Please try pasting your config.json file here to validate it: http://jsonlint.com");
|
||||
console.log("");
|
||||
throw err;
|
||||
}
|
||||
|
||||
// Just to prevent them getting garbage collected
|
||||
var accessories = [];
|
||||
// pull out our custom Bridge settings from config.json, if any
|
||||
var bridgeConfig = config.bridge || {};
|
||||
|
||||
// Start by creating our Bridge which will host all loaded Accessories
|
||||
var bridge = new Bridge(bridgeConfig.name || 'Homebridge', uuid.generate("HomeBridge"));
|
||||
|
||||
// keep track of async calls we're waiting for callbacks on before we can start up
|
||||
// this is hacky but this is all going away once we build proper plugin support
|
||||
var asyncCalls = 0;
|
||||
var asyncWait = false;
|
||||
|
||||
function startup() {
|
||||
asyncWait = true;
|
||||
if (config.platforms) loadPlatforms();
|
||||
if (config.accessories) loadAccessories();
|
||||
asyncWait = false;
|
||||
|
||||
// publish now unless we're waiting on anyone
|
||||
if (asyncCalls == 0)
|
||||
publish();
|
||||
}
|
||||
|
||||
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 accessoryType = accessoryConfig["accessory"]; // like "WeMo"
|
||||
var accessoryModule = require('./accessories/' + accessoryType + ".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);
|
||||
var accessoryName = accessoryConfig["name"];
|
||||
var log = createLog(accessoryName);
|
||||
|
||||
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);
|
||||
log("Initializing %s accessory...", accessoryType);
|
||||
|
||||
var accessoryInstance = new accessoryConstructor(log, accessoryConfig);
|
||||
var accessory = 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
|
||||
bridge.addBridgedAccessory(accessory);
|
||||
}
|
||||
}
|
||||
|
||||
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 platformType = platformConfig["platform"]; // like "Wink"
|
||||
var platformName = platformConfig["name"];
|
||||
var platformModule = require('./platforms/' + platformType + ".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);
|
||||
// Create a custom logging function that prepends the platform name for debugging
|
||||
var log = createLog(platformName);
|
||||
|
||||
log("Initializing " + platformName + " platform...");
|
||||
log("Initializing %s platform...", platformType);
|
||||
|
||||
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);
|
||||
})
|
||||
var platformInstance = new platformConstructor(log, platformConfig);
|
||||
loadPlatformAccessories(platformInstance, log, platformType);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Creates the actual HAP servers which listen on different sockets
|
||||
//
|
||||
function loadPlatformAccessories(platformInstance, log, platformType) {
|
||||
asyncCalls++;
|
||||
platformInstance.accessories(once(function(foundAccessories){
|
||||
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 = createAccessory(accessoryInstance, accessoryName, platformType, accessoryInstance.uuid_base);
|
||||
|
||||
// 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++;
|
||||
// add it to the bridge
|
||||
bridge.addBridgedAccessory(accessory);
|
||||
}
|
||||
|
||||
// were we the last callback?
|
||||
if (asyncCalls === 0 && !asyncWait)
|
||||
publish();
|
||||
}));
|
||||
}
|
||||
|
||||
// Creates a unique "username" for HomeKit from a hash of the given string
|
||||
function createUsername(str) {
|
||||
function createAccessory(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.
|
||||
|
||||
// Hash str into something like "098F6BCD4621D373CADE4E832627B4F6"
|
||||
var hash = crypto.createHash('md5').update(str).digest("hex").toUpperCase();
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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];
|
||||
// Returns the setup code in a scannable format.
|
||||
function printPin(pin) {
|
||||
console.log("Scan this code with your HomeKit App on your iOS device:");
|
||||
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", " ");
|
||||
}
|
||||
|
||||
// Returns a logging function that prepends messages with the given name in [brackets].
|
||||
function createLog(name) {
|
||||
return function(message) {
|
||||
var rest = Array.prototype.slice.call(arguments, 1 ); // any arguments after message
|
||||
var args = ["[%s] " + message, name].concat(rest);
|
||||
console.log.apply(console, args);
|
||||
}
|
||||
}
|
||||
|
||||
function publish() {
|
||||
printPin(bridgeConfig.pin);
|
||||
bridge.publish({
|
||||
username: bridgeConfig.username || "CC:22:3D:E3:CE:30",
|
||||
port: bridgeConfig.port || 51826,
|
||||
pincode: bridgeConfig.pin || "031-45-154",
|
||||
category: Accessory.Categories.OTHER
|
||||
});
|
||||
}
|
||||
|
||||
startup();
|
||||
|
||||
@@ -1,7 +1,32 @@
|
||||
{
|
||||
"bridge": {
|
||||
"name": "Homebridge",
|
||||
"username": "CC:22:3D:E3:CE:30",
|
||||
"port": 51826,
|
||||
"pin": "031-45-154"
|
||||
},
|
||||
|
||||
"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" : "Nest",
|
||||
"name" : "Nest",
|
||||
"username" : "username",
|
||||
"password" : "password"
|
||||
},
|
||||
{
|
||||
"platform" : "TelldusLive",
|
||||
"name" : "Telldus Live!",
|
||||
"public_key" : "telldus public key",
|
||||
"private_key" : "telldus private key",
|
||||
"token" : "telldus token",
|
||||
"token_secret" : "telldus token secret"
|
||||
},
|
||||
{
|
||||
"platform" : "Telldus",
|
||||
"name" : "Telldus"
|
||||
},
|
||||
{
|
||||
"platform": "Wink",
|
||||
"name": "Wink",
|
||||
@@ -20,7 +45,9 @@
|
||||
"platform": "Domoticz",
|
||||
"name": "Domoticz",
|
||||
"server": "127.0.0.1",
|
||||
"port": "8005"
|
||||
"port": "8080",
|
||||
"roomid": 0,
|
||||
"loadscenes": 1
|
||||
},
|
||||
{
|
||||
"platform": "PhilipsHue",
|
||||
@@ -34,6 +61,50 @@
|
||||
"port": "8000",
|
||||
"username": "username",
|
||||
"password": "password"
|
||||
},
|
||||
{
|
||||
"platform": "LogitechHarmony",
|
||||
"name": "Living Room Harmony Hub"
|
||||
},
|
||||
{
|
||||
"platform": "Sonos",
|
||||
"name": "Sonos",
|
||||
"play_volume": 25
|
||||
},
|
||||
{
|
||||
"platform": "YamahaAVR",
|
||||
"play_volume": -35,
|
||||
"setMainInputTo": "AirPlay"
|
||||
},
|
||||
{
|
||||
"platform": "ZWayServer",
|
||||
"url": "http://192.168.1.10:8083/",
|
||||
"login": "zwayusername",
|
||||
"password": "zwayuserpassword",
|
||||
"poll_interval": 2,
|
||||
"split_services": false
|
||||
},
|
||||
{
|
||||
"platform": "MiLight",
|
||||
"name": "MiLight",
|
||||
"ip_address": "255.255.255.255",
|
||||
"port": 8899,
|
||||
"type": "rgbw",
|
||||
"delay": 30,
|
||||
"repeat": 3,
|
||||
"zones":["Kitchen Lamp","Bedroom Lamp","Living Room Lamp","Hallway Lamp"]
|
||||
},
|
||||
{
|
||||
"platform": "HomeAssistant",
|
||||
"name": "HomeAssistant",
|
||||
"host": "http://192.168.1.10:8123",
|
||||
"password": "XXXXX",
|
||||
"supported_types": ["light", "switch", "media_player", "scene"]
|
||||
},
|
||||
{
|
||||
"platform": "LIFx",
|
||||
"name": "LIFx",
|
||||
"access_token": "XXXXXXXX generate at https://cloud.lifx.com/settings"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -52,12 +123,6 @@
|
||||
"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",
|
||||
@@ -73,21 +138,39 @@
|
||||
"password" : "your-carwings-password"
|
||||
},
|
||||
{
|
||||
"accessory": "XfinityHome",
|
||||
"accessory": "iControl",
|
||||
"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",
|
||||
"description": "This shim supports iControl-based security systems like Xfinity Home.",
|
||||
"system": "XFINITY_HOME",
|
||||
"email": "your-comcast-email",
|
||||
"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": "HomeMaticWindow",
|
||||
"name": "Contact",
|
||||
"description": "Control HomeMatic devices (The XMP-API addon for the CCU is required)",
|
||||
"ccu_id": "The XMP-API id of your HomeMatic device (type HM-Sec-RHS)",
|
||||
"ccu_ip": "The IP-Adress of your HomeMatic CCU device"
|
||||
},
|
||||
{
|
||||
"accessory": "HomeMaticThermo",
|
||||
"name": "Contact",
|
||||
"description": "Control HomeMatic devices (The XMP-API addon for the CCU is required)",
|
||||
"ccu_id_TargetTemp": "The XMP-API id of your HomeMatic device (type HM-CC-RT-DN )",
|
||||
"ccu_id_CurrentTemp": "The XMP-API id of your HomeMatic device (type HM-CC-RT-DN )",
|
||||
"ccu_id_ControlMode": "The XMP-API id of your HomeMatic device (type HM-CC-RT-DN )",
|
||||
"ccu_id_ManuMode": "The XMP-API id of your HomeMatic device (type HM-CC-RT-DN )",
|
||||
"ccu_id_AutoMode": "The XMP-API id of your HomeMatic device (type HM-CC-RT-DN )",
|
||||
"ccu_ip": "The IP-Adress of your HomeMatic CCU device"
|
||||
},
|
||||
{
|
||||
"accessory": "X10",
|
||||
"name": "Lamp",
|
||||
@@ -103,7 +186,20 @@
|
||||
"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": "HttpHygrometer",
|
||||
"name": "Kitchen",
|
||||
"url": "http://host/URL",
|
||||
"http_method": "GET"
|
||||
},
|
||||
{
|
||||
"accessory": "HttpThermometer",
|
||||
"name": "Garage",
|
||||
"url": "http://home/URL",
|
||||
"http_method": "GET"
|
||||
},
|
||||
{
|
||||
"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.",
|
||||
@@ -120,6 +216,47 @@
|
||||
"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
|
||||
},
|
||||
{
|
||||
"accessory": "Tesla",
|
||||
"name": "Tesla",
|
||||
"description": "This shim supports controlling climate control on the Tesla Model S.",
|
||||
"username": "tesla_email",
|
||||
"password" : "tesla_password"
|
||||
},
|
||||
{
|
||||
"accessory": "Hyperion",
|
||||
"name": "TV Backlight",
|
||||
"description": "Control the Hyperion TV backlight server. https://github.com/tvdzwan/hyperion",
|
||||
"host": "localhost",
|
||||
"port": "19444"
|
||||
},
|
||||
{
|
||||
"accessory": "mpdclient",
|
||||
"name" : "mpd",
|
||||
"host" : "localhost",
|
||||
"port" : 6600,
|
||||
"description": "Allows some control of an MPD server"
|
||||
},
|
||||
{
|
||||
"accessory": "FileSensor",
|
||||
"name": "File Time Motion Sensor",
|
||||
"path": "/tmp/CameraDump/",
|
||||
"window_seconds": 5,
|
||||
"sensor_type": "m",
|
||||
"inverse": false
|
||||
},
|
||||
{
|
||||
"accessory": "GenericRS232Device",
|
||||
"name": "Projector",
|
||||
"description": "Make sure you set a 'Siri-Name' for your iOS-Device (example: 'Home Cinema') otherwise it might not work.",
|
||||
"id": "TYDYMU044UVNP",
|
||||
"baudrate": 9600,
|
||||
"device": "/dev/tty.usbserial",
|
||||
"manufacturer": "Acer",
|
||||
"model_name": "H6510BD",
|
||||
"on_command": "* 0 IR 001\r",
|
||||
"off_command": "* 0 IR 002\r"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
40
package.json
40
package.json
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "homebridge",
|
||||
"description": "HomeKit support for the impatient",
|
||||
"version": "0.0.0",
|
||||
"version": "0.1.1",
|
||||
"scripts": {
|
||||
"start": "node app.js"
|
||||
"start": "DEBUG=* node app.js || true"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -11,17 +11,39 @@
|
||||
},
|
||||
"license": "ISC",
|
||||
"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",
|
||||
"async": "^1.4.2",
|
||||
"carwingsjs": "0.0.x",
|
||||
"chokidar": "^1.0.5",
|
||||
"color": "0.10.x",
|
||||
"debug": "^2.2.0",
|
||||
"eibd": "^0.3.1",
|
||||
"elkington": "kevinohara80/elkington",
|
||||
"hap-nodejs": "^0.0.2",
|
||||
"harmonyhubjs-client": "^1.1.6",
|
||||
"harmonyhubjs-discover": "git+https://github.com/swissmanu/harmonyhubjs-discover.git",
|
||||
"isy-js": "",
|
||||
"komponist": "0.1.0",
|
||||
"lifx": "git+https://github.com/magicmonkey/lifxjs.git",
|
||||
"lifx-api": "^1.0.1",
|
||||
"mdns": "^2.2.4",
|
||||
"node-hue-api": "^1.0.5",
|
||||
"node-icontrol": "^0.1.5",
|
||||
"node-milight-promise": "0.0.x",
|
||||
"node-persist": "0.0.x",
|
||||
"node-xmpp-client": "1.0.0-alpha23",
|
||||
"q": "1.4.x",
|
||||
"queue": "^3.1.0",
|
||||
"request": "2.49.x",
|
||||
"sonos": "0.8.x",
|
||||
"telldus-live": "^0.2.1",
|
||||
"teslams": "1.0.1",
|
||||
"tough-cookie": "^2.0.0",
|
||||
"unofficial-nest-api": "git+https://github.com/hachidorii/unofficial_nodejs_nest.git#d8d48edc952b049ff6320ef99afa7b2f04cdee98",
|
||||
"wemo": "0.2.x",
|
||||
"wink-js": "0.0.5",
|
||||
"elkington": "kevinohara80/elkington"
|
||||
"xml2js": "0.4.x",
|
||||
"xmldoc": "0.1.x",
|
||||
"yamaha-nodejs": "0.4.x"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,22 @@
|
||||
// - Added support for Scenes
|
||||
// - Sorting device names
|
||||
//
|
||||
// 22 July 2015 [lukeredpath]
|
||||
// - Added SSL and basic auth support
|
||||
//
|
||||
// 26 August 2015 [EddyK69]
|
||||
// - Added parameter in config.json: 'loadscenes' for enabling/disabling loading scenes
|
||||
// - Fixed issue with dimmer-range; was 0-100, should be 0-16
|
||||
//
|
||||
// 27 August 2015 [EddyK69]
|
||||
// - Fixed issue that 'on/off'-type lights showed as dimmers in HomeKit. Checking now on SwitchType instead of HaveDimmer
|
||||
// - Fixed issue that 'on-off'-type lights would not react on Siri 'Switch on/off light'; On/Off types are now handled as Lights instead of Switches
|
||||
// (Cannot determine if 'on/off'-type device is a Light or a Switch :( )
|
||||
//
|
||||
// 14 September 2015 [lukeredpath]
|
||||
// - Fixed incorrect dimmer range for LightwaveRF lights (0-32 required, MaxDimLevel should be honored)
|
||||
//
|
||||
//
|
||||
// Domoticz JSON API required
|
||||
// https://www.domoticz.com/wiki/Domoticz_API/JSON_URL's#Lights_and_switches
|
||||
//
|
||||
@@ -18,24 +34,40 @@
|
||||
// "name": "Domoticz",
|
||||
// "server": "127.0.0.1",
|
||||
// "port": "8080",
|
||||
// "roomid": 123 (0=no roomplan)
|
||||
// "roomid": 123, (0=no roomplan)
|
||||
// "loadscenes": 1 (0=disable scenes)
|
||||
// }
|
||||
// ],
|
||||
//
|
||||
// If your server uses HTTPS, you can specify "ssl": true in your config. If
|
||||
// your server uses a self-signed certificate, you'll need to run the following
|
||||
// before starting the server or you will get an error:
|
||||
//
|
||||
// export NODE_TLS_REJECT_UNAUTHORIZED=0
|
||||
//
|
||||
// For basic auth support, specify the "user" and "password" in your config.
|
||||
//
|
||||
// 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 types = require("hap-nodejs/accessories/types.js");
|
||||
var request = require("request");
|
||||
|
||||
function DomoticzPlatform(log, config){
|
||||
this.log = log;
|
||||
this.user = config["user"];
|
||||
this.password = config["password"];
|
||||
this.server = config["server"];
|
||||
this.port = config["port"];
|
||||
this.protocol = config["ssl"] ? "https" : "http";
|
||||
this.roomid = 0;
|
||||
if (typeof config["roomid"] != 'undefined') {
|
||||
this.roomid = config["roomid"];
|
||||
}
|
||||
this.loadscenes = 1;
|
||||
if (typeof config["loadscenes"] != 'undefined') {
|
||||
this.loadscenes = config["loadscenes"];
|
||||
}
|
||||
}
|
||||
|
||||
function sortByKey(array, key) {
|
||||
@@ -46,35 +78,50 @@ function sortByKey(array, key) {
|
||||
}
|
||||
|
||||
DomoticzPlatform.prototype = {
|
||||
urlForQuery: function(query) {
|
||||
var serverString = this.server;
|
||||
if (this.user && this.password) {
|
||||
serverString = this.user + ":" + this.password + "@" + serverString;
|
||||
}
|
||||
return this.protocol + "://" + serverString + ":" + this.port + "/json.htm?" + query;
|
||||
},
|
||||
|
||||
accessories: function(callback) {
|
||||
this.log("Fetching Domoticz lights and switches...");
|
||||
|
||||
var that = this;
|
||||
var foundAccessories = [];
|
||||
if (this.roomid == 0) {
|
||||
this.log("Fetching Domoticz lights and switches...");
|
||||
var that = this;
|
||||
var foundAccessories = [];
|
||||
|
||||
// mechanism to ensure callback is only executed once all requests complete
|
||||
var asyncCalls = 0;
|
||||
function callbackLater() { if (--asyncCalls == 0) callback(foundAccessories); }
|
||||
|
||||
if (this.roomid == 0) {
|
||||
//Get Lights
|
||||
asyncCalls++;
|
||||
request.get({
|
||||
url: "http://" + this.server + ":" + this.port + "/json.htm?type=devices&filter=light&used=true&order=Name",
|
||||
url: this.urlForQuery("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"));
|
||||
var havedimmer = (s.SwitchType == 'Dimmer')
|
||||
accessory = new DomoticzAccessory(that.log, that, false, s.idx, s.Name, havedimmer, s.MaxDimLevel, (s.SubType=="RGB")||(s.SubType=="RGBW"));
|
||||
foundAccessories.push(accessory);
|
||||
})
|
||||
}
|
||||
callback(foundAccessories);
|
||||
callbackLater();
|
||||
} else {
|
||||
that.log("There was a problem connecting to Domoticz.");
|
||||
that.log("There was a problem connecting to Domoticz. (" + err + ")");
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
}
|
||||
else {
|
||||
//Get all devices specified in the room
|
||||
asyncCalls++;
|
||||
request.get({
|
||||
url: "http://" + this.server + ":" + this.port + "/json.htm?type=devices&plan=" + this.roomid,
|
||||
url: this.urlForQuery("type=devices&plan=" + this.roomid),
|
||||
json: true
|
||||
}, function(err, response, json) {
|
||||
if (!err && response.statusCode == 200) {
|
||||
@@ -83,40 +130,43 @@ DomoticzPlatform.prototype = {
|
||||
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"));
|
||||
var havedimmer = (s.SwitchType == 'Dimmer')
|
||||
accessory = new DomoticzAccessory(that.log, that, false, s.idx, s.Name, havedimmer, s.MaxDimLevel, (s.SubType=="RGB")||(s.SubType=="RGBW"));
|
||||
foundAccessories.push(accessory);
|
||||
}
|
||||
})
|
||||
}
|
||||
callback(foundAccessories);
|
||||
callbackLater();
|
||||
} 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);
|
||||
})
|
||||
if (this.loadscenes == 1) {
|
||||
asyncCalls++;
|
||||
request.get({
|
||||
url: this.urlForQuery("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, true, s.idx, s.Name, false, 0, false);
|
||||
foundAccessories.push(accessory);
|
||||
})
|
||||
}
|
||||
callbackLater();
|
||||
} else {
|
||||
that.log("There was a problem connecting to Domoticz.");
|
||||
}
|
||||
callback(foundAccessories);
|
||||
} else {
|
||||
that.log("There was a problem connecting to Domoticz.");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function DomoticzAccessory(log, server, port, IsScene, idx, name, HaveDimmer, MaxDimLevel, HaveRGB) {
|
||||
function DomoticzAccessory(log, platform, IsScene, idx, name, HaveDimmer, MaxDimLevel, HaveRGB) {
|
||||
// device info
|
||||
this.IsScene = IsScene;
|
||||
this.idx = idx;
|
||||
@@ -125,8 +175,7 @@ function DomoticzAccessory(log, server, port, IsScene, idx, name, HaveDimmer, Ma
|
||||
this.MaxDimLevel = MaxDimLevel;
|
||||
this.HaveRGB = HaveRGB;
|
||||
this.log = log;
|
||||
this.server = server;
|
||||
this.port = port;
|
||||
this.platform = platform;
|
||||
}
|
||||
|
||||
DomoticzAccessory.prototype = {
|
||||
@@ -135,13 +184,14 @@ DomoticzAccessory.prototype = {
|
||||
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";
|
||||
url = this.platform.urlForQuery("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";
|
||||
url = this.platform.urlForQuery("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;
|
||||
value = this.dimmerLevelForValue(value)
|
||||
url = this.platform.urlForQuery("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);
|
||||
@@ -150,7 +200,7 @@ DomoticzAccessory.prototype = {
|
||||
else {
|
||||
//Scenes
|
||||
if (c == "On" || c == "Off") {
|
||||
url = "http://" + this.server + ":" + this.port + "/json.htm?type=command¶m=switchscene&idx=" + this.idx + "&switchcmd=" + c;
|
||||
url = this.platform.urlForQuery("type=command¶m=switchscene&idx=" + this.idx + "&switchcmd=" + c);
|
||||
}
|
||||
else if (value != undefined) {
|
||||
this.log(this.name + " Unhandled Scene command! cmd=" + c + ", value=" + value);
|
||||
@@ -163,11 +213,19 @@ DomoticzAccessory.prototype = {
|
||||
that.log("There was a problem sending command " + c + " to" + that.name);
|
||||
that.log(url);
|
||||
} else {
|
||||
that.log(that.name + " sent command " + c);
|
||||
that.log(that.name + " sent command " + c + " (value: " + value + ")");
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// translates the HomeKit dim level as a percentage to whatever scale the device requires
|
||||
dimmerLevelForValue: function(value) {
|
||||
if (this.MaxDimLevel == 100) {
|
||||
return value;
|
||||
}
|
||||
return Math.round((value / 100.0) * this.MaxDimLevel)
|
||||
},
|
||||
|
||||
informationCharacteristics: function() {
|
||||
return [
|
||||
{
|
||||
@@ -294,11 +352,11 @@ DomoticzAccessory.prototype = {
|
||||
},
|
||||
|
||||
sType: function() {
|
||||
if (this.HaveDimmer == true) {
|
||||
//if (this.HaveDimmer == true) {
|
||||
return types.LIGHTBULB_STYPE
|
||||
} else {
|
||||
return types.SWITCH_STYPE
|
||||
}
|
||||
//} else {
|
||||
// return types.SWITCH_STYPE
|
||||
//}
|
||||
},
|
||||
|
||||
getServices: function() {
|
||||
@@ -317,4 +375,4 @@ DomoticzAccessory.prototype = {
|
||||
};
|
||||
|
||||
module.exports.accessory = DomoticzAccessory;
|
||||
module.exports.platform = DomoticzPlatform;
|
||||
module.exports.platform = DomoticzPlatform;
|
||||
|
||||
2159
platforms/FHEM.js
Normal file
2159
platforms/FHEM.js
Normal file
File diff suppressed because it is too large
Load Diff
253
platforms/FibaroHC2.js
Normal file
253
platforms/FibaroHC2.js
Normal file
@@ -0,0 +1,253 @@
|
||||
// Fibaro Home Center 2 Platform Shim for HomeBridge
|
||||
//
|
||||
// Remember to add platform to config.json. Example:
|
||||
// "platforms": [
|
||||
// {
|
||||
// "platform": "FibaroHC2",
|
||||
// "name": "FibaroHC2",
|
||||
// "host": "PUT IP ADDRESS OF YOUR HC2 HERE",
|
||||
// "username": "PUT USERNAME OF YOUR HC2 HERE",
|
||||
// "password": "PUT PASSWORD OF YOUR HC2 HERE"
|
||||
// }
|
||||
// ],
|
||||
//
|
||||
// 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 Service = require("hap-nodejs").Service;
|
||||
var Characteristic = require("hap-nodejs").Characteristic;
|
||||
var request = require("request");
|
||||
|
||||
function FibaroHC2Platform(log, config){
|
||||
this.log = log;
|
||||
this.host = config["host"];
|
||||
this.username = config["username"];
|
||||
this.password = config["password"];
|
||||
this.auth = "Basic " + new Buffer(this.username + ":" + this.password).toString("base64");
|
||||
this.url = "http://"+this.host+"/api/devices";
|
||||
|
||||
startPollingUpdate( this );
|
||||
}
|
||||
|
||||
FibaroHC2Platform.prototype = {
|
||||
accessories: function(callback) {
|
||||
this.log("Fetching Fibaro Home Center devices...");
|
||||
|
||||
var that = this;
|
||||
var foundAccessories = [];
|
||||
|
||||
request.get({
|
||||
url: this.url,
|
||||
headers : {
|
||||
"Authorization" : this.auth
|
||||
},
|
||||
json: true
|
||||
}, function(err, response, json) {
|
||||
if (!err && response.statusCode == 200) {
|
||||
if (json != undefined) {
|
||||
json.map(function(s) {
|
||||
that.log("Found: " + s.type);
|
||||
if (s.visible == true) {
|
||||
var accessory = null;
|
||||
if (s.type == "com.fibaro.multilevelSwitch")
|
||||
accessory = new FibaroAccessory(new Service.Lightbulb(s.name), [Characteristic.On, Characteristic.Brightness]);
|
||||
else if (s.type == "com.fibaro.FGRM222" || s.type == "com.fibaro.FGR221")
|
||||
accessory = new FibaroAccessory(new Service.WindowCovering(s.name), [Characteristic.CurrentPosition, Characteristic.TargetPosition, Characteristic.PositionState]);
|
||||
else if (s.type == "com.fibaro.binarySwitch" || s.type == "com.fibaro.developer.bxs.virtualBinarySwitch")
|
||||
accessory = new FibaroAccessory(new Service.Switch(s.name), [Characteristic.On]);
|
||||
else if (s.type == "com.fibaro.FGMS001" || s.type == "com.fibaro.motionSensor")
|
||||
accessory = new FibaroAccessory(new Service.MotionSensor(s.name), [Characteristic.MotionDetected]);
|
||||
else if (s.type == "com.fibaro.temperatureSensor")
|
||||
accessory = new FibaroAccessory(new Service.TemperatureSensor(s.name), [Characteristic.CurrentTemperature]);
|
||||
else if (s.type == "com.fibaro.doorSensor")
|
||||
accessory = new FibaroAccessory(new Service.ContactSensor(s.name), [Characteristic.ContactSensorState]);
|
||||
else if (s.type == "com.fibaro.lightSensor")
|
||||
accessory = new FibaroAccessory(new Service.LightSensor(s.name), [Characteristic.CurrentAmbientLightLevel]);
|
||||
else if (s.type == "com.fibaro.FGWP101")
|
||||
accessory = new FibaroAccessory(new Service.Outlet(s.name), [Characteristic.On, Characteristic.OutletInUse]);
|
||||
if (accessory != null) {
|
||||
accessory.getServices = function() {
|
||||
return that.getServices(accessory);
|
||||
};
|
||||
accessory.platform = that;
|
||||
accessory.remoteAccessory = s;
|
||||
accessory.id = s.id;
|
||||
accessory.name = s.name;
|
||||
accessory.model = s.type;
|
||||
accessory.manufacturer = "Fibaro";
|
||||
accessory.serialNumber = "<unknown>";
|
||||
foundAccessories.push(accessory);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
callback(foundAccessories);
|
||||
} else {
|
||||
that.log("There was a problem connecting with FibaroHC2.");
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
command: function(c,value, that) {
|
||||
var url = "http://"+this.host+"/api/devices/"+that.id+"/action/"+c;
|
||||
var body = value != undefined ? JSON.stringify({
|
||||
"args": [ value ]
|
||||
}) : null;
|
||||
var method = "post";
|
||||
request({
|
||||
url: url,
|
||||
body: body,
|
||||
method: method,
|
||||
headers: {
|
||||
"Authorization" : this.auth
|
||||
},
|
||||
}, function(err, response) {
|
||||
if (err) {
|
||||
that.platform.log("There was a problem sending command " + c + " to" + that.name);
|
||||
that.platform.log(url);
|
||||
} else {
|
||||
that.platform.log(that.name + " sent command " + c);
|
||||
that.platform.log(url);
|
||||
}
|
||||
});
|
||||
},
|
||||
getAccessoryValue: function(callback, returnBoolean, homebridgeAccessory, powerValue) {
|
||||
var url = "http://"+homebridgeAccessory.platform.host+"/api/devices/"+homebridgeAccessory.id+"/properties/";
|
||||
if (powerValue)
|
||||
url = url + "power";
|
||||
else
|
||||
url = url + "value";
|
||||
|
||||
request.get({
|
||||
headers : {
|
||||
"Authorization" : homebridgeAccessory.platform.auth
|
||||
},
|
||||
json: true,
|
||||
url: url
|
||||
}, function(err, response, json) {
|
||||
homebridgeAccessory.platform.log(url);
|
||||
if (!err && response.statusCode == 200) {
|
||||
if (powerValue) {
|
||||
callback(undefined, parseFloat(json.value) > 1.0 ? true : false);
|
||||
} else if (returnBoolean)
|
||||
callback(undefined, json.value == 0 ? 0 : 1);
|
||||
else
|
||||
callback(undefined, json.value);
|
||||
} else {
|
||||
homebridgeAccessory.platform.log("There was a problem getting value from" + homebridgeAccessory.id);
|
||||
}
|
||||
})
|
||||
},
|
||||
getInformationService: function(homebridgeAccessory) {
|
||||
var informationService = new Service.AccessoryInformation();
|
||||
informationService
|
||||
.setCharacteristic(Characteristic.Name, homebridgeAccessory.name)
|
||||
.setCharacteristic(Characteristic.Manufacturer, homebridgeAccessory.manufacturer)
|
||||
.setCharacteristic(Characteristic.Model, homebridgeAccessory.model)
|
||||
.setCharacteristic(Characteristic.SerialNumber, homebridgeAccessory.serialNumber);
|
||||
return informationService;
|
||||
},
|
||||
bindCharacteristicEvents: function(characteristic, homebridgeAccessory) {
|
||||
var onOff = characteristic.props.format == "bool" ? true : false;
|
||||
var readOnly = true;
|
||||
for (var i = 0; i < characteristic.props.perms.length; i++)
|
||||
if (characteristic.props.perms[i] == "pw")
|
||||
readOnly = false;
|
||||
var powerValue = (characteristic.UUID == "00000026-0000-1000-8000-0026BB765291") ? true : false;
|
||||
subscribeUpdate(characteristic, homebridgeAccessory, onOff);
|
||||
if (!readOnly) {
|
||||
characteristic
|
||||
.on('set', function(value, callback, context) {
|
||||
if( context !== 'fromFibaro' ) {
|
||||
if (onOff)
|
||||
homebridgeAccessory.platform.command(value == 0 ? "turnOff": "turnOn", null, homebridgeAccessory);
|
||||
else
|
||||
homebridgeAccessory.platform.command("setValue", value, homebridgeAccessory);
|
||||
}
|
||||
callback();
|
||||
}.bind(this) );
|
||||
}
|
||||
characteristic
|
||||
.on('get', function(callback) {
|
||||
homebridgeAccessory.platform.getAccessoryValue(callback, onOff, homebridgeAccessory, powerValue);
|
||||
}.bind(this) );
|
||||
},
|
||||
getServices: function(homebridgeAccessory) {
|
||||
var informationService = homebridgeAccessory.platform.getInformationService(homebridgeAccessory);
|
||||
for (var i=0; i < homebridgeAccessory.characteristics.length; i++) {
|
||||
var characteristic = homebridgeAccessory.controlService.getCharacteristic(homebridgeAccessory.characteristics[i]);
|
||||
if (characteristic == undefined)
|
||||
characteristic = homebridgeAccessory.controlService.addCharacteristic(homebridgeAccessory.characteristics[i]);
|
||||
homebridgeAccessory.platform.bindCharacteristicEvents(characteristic, homebridgeAccessory);
|
||||
}
|
||||
|
||||
return [informationService, homebridgeAccessory.controlService];
|
||||
}
|
||||
}
|
||||
|
||||
function FibaroAccessory(controlService, characteristics) {
|
||||
this.controlService = controlService;
|
||||
this.characteristics = characteristics;
|
||||
}
|
||||
|
||||
var lastPoll=0;
|
||||
var pollingUpdateRunning = false;
|
||||
|
||||
function startPollingUpdate( platform )
|
||||
{
|
||||
if( pollingUpdateRunning )
|
||||
return;
|
||||
pollingUpdateRunning = true;
|
||||
|
||||
var updateUrl = "http://"+platform.host+"/api/refreshStates?last="+lastPoll;
|
||||
|
||||
request.get({
|
||||
url: updateUrl,
|
||||
headers : {
|
||||
"Authorization" : platform.auth
|
||||
},
|
||||
json: true
|
||||
}, function(err, response, json) {
|
||||
if (!err && response.statusCode == 200) {
|
||||
if (json != undefined) {
|
||||
lastPoll = json.last;
|
||||
if (json.changes != undefined) {
|
||||
json.changes.map(function(s) {
|
||||
if (s.value != undefined) {
|
||||
|
||||
var value=parseInt(s.value);
|
||||
if (isNaN(value))
|
||||
value=(s.value === "true");
|
||||
for (i=0;i<updateSubscriptions.length; i++) {
|
||||
var subscription = updateSubscriptions[i];
|
||||
if (subscription.id == s.id) {
|
||||
if (s.power != undefined && subscription.characteristic.UUID == "00000026-0000-1000-8000-0026BB765291") {
|
||||
subscription.characteristic.setValue(parseFloat(s.power) > 1.0 ? true : false, undefined, 'fromFibaro');
|
||||
} else if ((subscription.onOff && typeof(value) == "boolean") || !subscription.onOff)
|
||||
subscription.characteristic.setValue(value, undefined, 'fromFibaro');
|
||||
else
|
||||
subscription.characteristic.setValue(value == 0 ? false : true, undefined, 'fromFibaro');
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
platform.log("There was a problem connecting with FibaroHC2.");
|
||||
}
|
||||
pollingUpdateRunning = false;
|
||||
setTimeout( function(){startPollingUpdate(platform)}, 2000 );
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
var updateSubscriptions = [];
|
||||
function subscribeUpdate(characteristic, accessory, onOff)
|
||||
{
|
||||
// TODO: optimized management of updateSubscription data structure (no array with sequential access)
|
||||
updateSubscriptions.push({ 'id': accessory.id, 'characteristic': characteristic, 'accessory': accessory, 'onOff': onOff });
|
||||
}
|
||||
|
||||
module.exports.platform = FibaroHC2Platform;
|
||||
542
platforms/HomeAssistant.js
Normal file
542
platforms/HomeAssistant.js
Normal file
@@ -0,0 +1,542 @@
|
||||
// Home Assistant
|
||||
//
|
||||
// Current Support: lights
|
||||
//
|
||||
// This is a shim to publish lights maintained by Home Assistant.
|
||||
// Home Assistant is an open-source home automation platform.
|
||||
// URL: http://home-assistant.io
|
||||
// GitHub: https://github.com/balloob/home-assistant
|
||||
//
|
||||
// HA accessories supported: Lights, Switches, Media Players, Scenes.
|
||||
//
|
||||
// Optional Devices - Edit the supported_types key in the config to pick which
|
||||
// of the 4 types you would like to expose to HomeKit from
|
||||
// Home Assistant. light, switch, media_player, scene.
|
||||
//
|
||||
//
|
||||
// Scene Support
|
||||
//
|
||||
// You can optionally import your Home Assistant scenes. These will appear to
|
||||
// HomeKit as switches. You can simply say "turn on party time". In some cases
|
||||
// scenes names are already rerved in HomeKit...like "Good Morning" and
|
||||
// "Good Night". You will be able to just say "Good Morning" or "Good Night" to
|
||||
// have these triggered.
|
||||
//
|
||||
// You might want to play with the wording to figure out what ends up working well
|
||||
// for your scene names. It's also important to not populate any actual HomeKit
|
||||
// scenes with the same names, as Siri will pick these instead of your Home
|
||||
// Assistant scenes.
|
||||
//
|
||||
//
|
||||
//
|
||||
// Media Player Support
|
||||
//
|
||||
// Media players on your Home Assistant will be added to your HomeKit as a light.
|
||||
// While this seems like a hack at first, it's actually quite useful. You can
|
||||
// turn them on, off, and set their volume (as a function of brightness).
|
||||
//
|
||||
// There are some rules to know about how on/off treats your media player. If
|
||||
// your media player supports play/pause, then turning them on and off via
|
||||
// HomeKit will play and pause them. If they do not support play/pause but then
|
||||
// support on/off they will be turned on and then off.
|
||||
//
|
||||
// HomeKit does not have a characteristic of Volume or a Speaker type. So we are
|
||||
// using the light device type here. So to turn your speaker up and down, you
|
||||
// will need to use the same language you use to set the brighness of a light.
|
||||
// You can play around with language to see what fits best.
|
||||
//
|
||||
//
|
||||
//
|
||||
// Examples
|
||||
//
|
||||
// Dim the Kitchen Speaker to 40% - sets volume to 40%
|
||||
// Dim the the Kitchen Speaker 10% - lowers the volume by 10%
|
||||
// Set the brightness of the Kitchen Speaker to 40%
|
||||
//
|
||||
// Remember to add platform to config.json. Example:
|
||||
// "platforms": [
|
||||
// {
|
||||
// "platform": "HomeAssistant",
|
||||
// "name": "HomeAssistant",
|
||||
// "host": "http://192.168.1.50:8123",
|
||||
// "password": "xxx",
|
||||
// "supported_types": ["light", "switch", "media_player", "scene"]
|
||||
// }
|
||||
// ]
|
||||
//
|
||||
// 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 Service = require("hap-nodejs").Service;
|
||||
var Characteristic = require("hap-nodejs").Characteristic;
|
||||
var url = require('url')
|
||||
var request = require("request");
|
||||
|
||||
var communicationError = new Error('Can not communicate with Home Assistant.')
|
||||
|
||||
function HomeAssistantPlatform(log, config){
|
||||
|
||||
// auth info
|
||||
this.host = config["host"];
|
||||
this.password = config["password"];
|
||||
this.supportedTypes = config["supported_types"];
|
||||
|
||||
this.log = log;
|
||||
}
|
||||
|
||||
HomeAssistantPlatform.prototype = {
|
||||
_request: function(method, path, options, callback) {
|
||||
var self = this
|
||||
var requestURL = this.host + '/api' + path
|
||||
options = options || {}
|
||||
options.query = options.query || {}
|
||||
|
||||
var reqOpts = {
|
||||
url: url.parse(requestURL),
|
||||
method: method || 'GET',
|
||||
qs: options.query,
|
||||
body: JSON.stringify(options.body),
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'x-ha-access': this.password
|
||||
}
|
||||
}
|
||||
|
||||
request(reqOpts, function onResponse(error, response, body) {
|
||||
if (error) {
|
||||
callback(error, response)
|
||||
return
|
||||
}
|
||||
|
||||
if (response.statusCode === 401) {
|
||||
callback(new Error('You are not authenticated'), response)
|
||||
return
|
||||
}
|
||||
|
||||
json = JSON.parse(body)
|
||||
callback(error, response, json)
|
||||
})
|
||||
|
||||
},
|
||||
fetchState: function(entity_id, callback){
|
||||
this._request('GET', '/states/' + entity_id, {}, function(error, response, data){
|
||||
if (error) {
|
||||
callback(null)
|
||||
}else{
|
||||
callback(data)
|
||||
}
|
||||
})
|
||||
},
|
||||
callService: function(domain, service, service_data, callback){
|
||||
var options = {}
|
||||
options.body = service_data
|
||||
|
||||
this._request('POST', '/services/' + domain + '/' + service, options, function(error, response, data){
|
||||
if (error) {
|
||||
callback(null)
|
||||
}else{
|
||||
callback(data)
|
||||
}
|
||||
})
|
||||
},
|
||||
accessories: function(callback) {
|
||||
this.log("Fetching HomeAssistant devices.");
|
||||
|
||||
var that = this;
|
||||
var foundAccessories = [];
|
||||
|
||||
this._request('GET', '/states', {}, function(error, response, data){
|
||||
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
entity = data[i]
|
||||
entity_type = entity.entity_id.split('.')[0]
|
||||
|
||||
// ignore devices that are not in the list of supported types
|
||||
if (that.supportedTypes.indexOf(entity_type) == -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// ignore hidden devices
|
||||
if (entity.attributes && entity.attributes.hidden) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var accessory = null
|
||||
|
||||
if (entity_type == 'light') {
|
||||
accessory = new HomeAssistantLight(that.log, entity, that)
|
||||
}else if (entity_type == 'switch'){
|
||||
console.log(JSON.stringify(entity))
|
||||
console.log("");
|
||||
console.log("");
|
||||
accessory = new HomeAssistantSwitch(that.log, entity, that)
|
||||
}else if (entity_type == 'scene'){
|
||||
accessory = new HomeAssistantSwitch(that.log, entity, that, 'scene')
|
||||
}else if (entity_type == 'media_player' && entity.attributes && entity.attributes.supported_media_commands){
|
||||
accessory = new HomeAssistantMediaPlayer(that.log, entity, that)
|
||||
}
|
||||
|
||||
if (accessory) {
|
||||
foundAccessories.push(accessory)
|
||||
}
|
||||
}
|
||||
|
||||
callback(foundAccessories)
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function HomeAssistantLight(log, data, client) {
|
||||
// device info
|
||||
this.domain = "light"
|
||||
this.data = data
|
||||
this.entity_id = data.entity_id
|
||||
if (data.attributes && data.attributes.friendly_name) {
|
||||
this.name = data.attributes.friendly_name
|
||||
}else{
|
||||
this.name = data.entity_id.split('.').pop().replace(/_/g, ' ')
|
||||
}
|
||||
|
||||
this.client = client
|
||||
this.log = log;
|
||||
}
|
||||
|
||||
HomeAssistantLight.prototype = {
|
||||
getPowerState: function(callback){
|
||||
this.log("fetching power state for: " + this.name);
|
||||
|
||||
this.client.fetchState(this.entity_id, function(data){
|
||||
if (data) {
|
||||
powerState = data.state == 'on'
|
||||
callback(null, powerState)
|
||||
}else{
|
||||
callback(communicationError)
|
||||
}
|
||||
}.bind(this))
|
||||
},
|
||||
getBrightness: function(callback){
|
||||
this.log("fetching brightness for: " + this.name);
|
||||
|
||||
this.client.fetchState(this.entity_id, function(data){
|
||||
if (data && data.attributes) {
|
||||
brightness = ((data.attributes.brightness || 0) / 255)*100
|
||||
callback(null, brightness)
|
||||
}else{
|
||||
callback(communicationError)
|
||||
}
|
||||
}.bind(this))
|
||||
},
|
||||
setPowerState: function(powerOn, callback) {
|
||||
var that = this;
|
||||
var service_data = {}
|
||||
service_data.entity_id = this.entity_id
|
||||
|
||||
if (powerOn) {
|
||||
this.log("Setting power state on the '"+this.name+"' to on");
|
||||
|
||||
this.client.callService(this.domain, 'turn_on', service_data, function(data){
|
||||
if (data) {
|
||||
that.log("Successfully set power state on the '"+that.name+"' to on");
|
||||
callback()
|
||||
}else{
|
||||
callback(communicationError)
|
||||
}
|
||||
}.bind(this))
|
||||
}else{
|
||||
this.log("Setting power state on the '"+this.name+"' to off");
|
||||
|
||||
this.client.callService(this.domain, 'turn_off', service_data, function(data){
|
||||
if (data) {
|
||||
that.log("Successfully set power state on the '"+that.name+"' to off");
|
||||
callback()
|
||||
}else{
|
||||
callback(communicationError)
|
||||
}
|
||||
}.bind(this))
|
||||
}
|
||||
},
|
||||
setBrightness: function(level, callback) {
|
||||
var that = this;
|
||||
var service_data = {}
|
||||
service_data.entity_id = this.entity_id
|
||||
|
||||
service_data.brightness = 255*(level/100.0)
|
||||
|
||||
this.log("Setting brightness on the '"+this.name+"' to " + level);
|
||||
|
||||
this.client.callService(this.domain, 'turn_on', service_data, function(data){
|
||||
if (data) {
|
||||
that.log("Successfully set brightness on the '"+that.name+"' to " + level);
|
||||
callback()
|
||||
}else{
|
||||
callback(communicationError)
|
||||
}
|
||||
}.bind(this))
|
||||
},
|
||||
getServices: function() {
|
||||
var lightbulbService = new Service.Lightbulb();
|
||||
var informationService = new Service.AccessoryInformation();
|
||||
|
||||
informationService
|
||||
.setCharacteristic(Characteristic.Manufacturer, "Home Assistant")
|
||||
.setCharacteristic(Characteristic.Model, "Light")
|
||||
.setCharacteristic(Characteristic.SerialNumber, "xxx");
|
||||
|
||||
lightbulbService
|
||||
.getCharacteristic(Characteristic.On)
|
||||
.on('get', this.getPowerState.bind(this))
|
||||
.on('set', this.setPowerState.bind(this));
|
||||
|
||||
lightbulbService
|
||||
.addCharacteristic(Characteristic.Brightness)
|
||||
.on('get', this.getBrightness.bind(this))
|
||||
.on('set', this.setBrightness.bind(this));
|
||||
|
||||
return [informationService, lightbulbService];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function HomeAssistantMediaPlayer(log, data, client) {
|
||||
var SUPPORT_PAUSE = 1
|
||||
var SUPPORT_SEEK = 2
|
||||
var SUPPORT_VOLUME_SET = 4
|
||||
var SUPPORT_VOLUME_MUTE = 8
|
||||
var SUPPORT_PREVIOUS_TRACK = 16
|
||||
var SUPPORT_NEXT_TRACK = 32
|
||||
var SUPPORT_YOUTUBE = 64
|
||||
var SUPPORT_TURN_ON = 128
|
||||
var SUPPORT_TURN_OFF = 256
|
||||
|
||||
// device info
|
||||
this.domain = "media_player"
|
||||
this.data = data
|
||||
this.entity_id = data.entity_id
|
||||
this.supportsVolume = false
|
||||
this.supportedMediaCommands = data.attributes.supported_media_commands
|
||||
|
||||
if (data.attributes && data.attributes.friendly_name) {
|
||||
this.name = data.attributes.friendly_name
|
||||
}else{
|
||||
this.name = data.entity_id.split('.').pop().replace(/_/g, ' ')
|
||||
}
|
||||
|
||||
if ((this.supportedMediaCommands | SUPPORT_PAUSE) == this.supportedMediaCommands) {
|
||||
this.onState = "playing"
|
||||
this.offState = "paused"
|
||||
this.onService = "media_play"
|
||||
this.offService = "media_pause"
|
||||
}else if ((this.supportedMediaCommands | SUPPORT_TURN_ON) == this.supportedMediaCommands && (this.supportedMediaCommands | SUPPORT_TURN_OFF) == this.supportedMediaCommands) {
|
||||
this.onState = "on"
|
||||
this.offState = "off"
|
||||
this.onService = "turn_on"
|
||||
this.offService = "turn_off"
|
||||
}
|
||||
|
||||
if ((this.supportedMediaCommands | SUPPORT_VOLUME_SET) == this.supportedMediaCommands) {
|
||||
this.supportsVolume = true
|
||||
}
|
||||
|
||||
this.client = client
|
||||
this.log = log;
|
||||
}
|
||||
|
||||
HomeAssistantMediaPlayer.prototype = {
|
||||
getPowerState: function(callback){
|
||||
this.log("fetching power state for: " + this.name);
|
||||
|
||||
this.client.fetchState(this.entity_id, function(data){
|
||||
if (data) {
|
||||
powerState = data.state == this.onState
|
||||
callback(null, powerState)
|
||||
}else{
|
||||
callback(communicationError)
|
||||
}
|
||||
}.bind(this))
|
||||
},
|
||||
getVolume: function(callback){
|
||||
this.log("fetching volume for: " + this.name);
|
||||
that = this
|
||||
this.client.fetchState(this.entity_id, function(data){
|
||||
if (data && data.attributes) {
|
||||
that.log(JSON.stringify(data.attributes))
|
||||
level = data.attributes.volume_level ? data.attributes.volume_level*100 : 0
|
||||
callback(null, level)
|
||||
}else{
|
||||
callback(communicationError)
|
||||
}
|
||||
}.bind(this))
|
||||
},
|
||||
setPowerState: function(powerOn, callback) {
|
||||
var that = this;
|
||||
var service_data = {}
|
||||
service_data.entity_id = this.entity_id
|
||||
|
||||
if (powerOn) {
|
||||
this.log("Setting power state on the '"+this.name+"' to on");
|
||||
|
||||
this.client.callService(this.domain, this.onService, service_data, function(data){
|
||||
if (data) {
|
||||
that.log("Successfully set power state on the '"+that.name+"' to on");
|
||||
callback()
|
||||
}else{
|
||||
callback(communicationError)
|
||||
}
|
||||
}.bind(this))
|
||||
}else{
|
||||
this.log("Setting power state on the '"+this.name+"' to off");
|
||||
|
||||
this.client.callService(this.domain, this.offService, service_data, function(data){
|
||||
if (data) {
|
||||
that.log("Successfully set power state on the '"+that.name+"' to off");
|
||||
callback()
|
||||
}else{
|
||||
callback(communicationError)
|
||||
}
|
||||
}.bind(this))
|
||||
}
|
||||
},
|
||||
setVolume: function(level, callback) {
|
||||
var that = this;
|
||||
var service_data = {}
|
||||
service_data.entity_id = this.entity_id
|
||||
|
||||
service_data.volume_level = level/100.0
|
||||
|
||||
this.log("Setting volume on the '"+this.name+"' to " + level);
|
||||
|
||||
this.client.callService(this.domain, 'volume_set', service_data, function(data){
|
||||
if (data) {
|
||||
that.log("Successfully set volume on the '"+that.name+"' to " + level);
|
||||
callback()
|
||||
}else{
|
||||
callback(communicationError)
|
||||
}
|
||||
}.bind(this))
|
||||
},
|
||||
getServices: function() {
|
||||
var lightbulbService = new Service.Lightbulb();
|
||||
var informationService = new Service.AccessoryInformation();
|
||||
|
||||
informationService
|
||||
.setCharacteristic(Characteristic.Manufacturer, "Home Assistant")
|
||||
.setCharacteristic(Characteristic.Model, "Media Player")
|
||||
.setCharacteristic(Characteristic.SerialNumber, "xxx");
|
||||
|
||||
lightbulbService
|
||||
.getCharacteristic(Characteristic.On)
|
||||
.on('get', this.getPowerState.bind(this))
|
||||
.on('set', this.setPowerState.bind(this));
|
||||
|
||||
|
||||
if (this.supportsVolume) {
|
||||
lightbulbService
|
||||
.addCharacteristic(Characteristic.Brightness)
|
||||
.on('get', this.getVolume.bind(this))
|
||||
.on('set', this.setVolume.bind(this));
|
||||
}
|
||||
|
||||
return [informationService, lightbulbService];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
function HomeAssistantSwitch(log, data, client, type) {
|
||||
// device info
|
||||
this.domain = type || "switch"
|
||||
this.data = data
|
||||
this.entity_id = data.entity_id
|
||||
if (data.attributes && data.attributes.friendly_name) {
|
||||
this.name = data.attributes.friendly_name
|
||||
}else{
|
||||
this.name = data.entity_id.split('.').pop().replace(/_/g, ' ')
|
||||
}
|
||||
|
||||
this.client = client
|
||||
this.log = log;
|
||||
}
|
||||
|
||||
HomeAssistantSwitch.prototype = {
|
||||
getPowerState: function(callback){
|
||||
this.log("fetching power state for: " + this.name);
|
||||
|
||||
this.client.fetchState(this.entity_id, function(data){
|
||||
if (data) {
|
||||
powerState = data.state == 'on'
|
||||
callback(null, powerState)
|
||||
}else{
|
||||
callback(communicationError)
|
||||
}
|
||||
}.bind(this))
|
||||
},
|
||||
setPowerState: function(powerOn, callback) {
|
||||
var that = this;
|
||||
var service_data = {}
|
||||
service_data.entity_id = this.entity_id
|
||||
|
||||
if (powerOn) {
|
||||
this.log("Setting power state on the '"+this.name+"' to on");
|
||||
|
||||
this.client.callService(this.domain, 'turn_on', service_data, function(data){
|
||||
if (data) {
|
||||
that.log("Successfully set power state on the '"+that.name+"' to on");
|
||||
callback()
|
||||
}else{
|
||||
callback(communicationError)
|
||||
}
|
||||
}.bind(this))
|
||||
}else{
|
||||
this.log("Setting power state on the '"+this.name+"' to off");
|
||||
|
||||
this.client.callService(this.domain, 'turn_off', service_data, function(data){
|
||||
if (data) {
|
||||
that.log("Successfully set power state on the '"+that.name+"' to off");
|
||||
callback()
|
||||
}else{
|
||||
callback(communicationError)
|
||||
}
|
||||
}.bind(this))
|
||||
}
|
||||
},
|
||||
getServices: function() {
|
||||
var switchService = new Service.Switch();
|
||||
var informationService = new Service.AccessoryInformation();
|
||||
var model;
|
||||
|
||||
switch (this.domain) {
|
||||
case "scene":
|
||||
model = "Scene"
|
||||
break;
|
||||
default:
|
||||
model = "Switch"
|
||||
}
|
||||
|
||||
informationService
|
||||
.setCharacteristic(Characteristic.Manufacturer, "Home Assistant")
|
||||
.setCharacteristic(Characteristic.Model, model)
|
||||
.setCharacteristic(Characteristic.SerialNumber, "xxx");
|
||||
|
||||
if (this.domain == 'switch') {
|
||||
switchService
|
||||
.getCharacteristic(Characteristic.On)
|
||||
.on('get', this.getPowerState.bind(this))
|
||||
.on('set', this.setPowerState.bind(this));
|
||||
|
||||
}else{
|
||||
switchService
|
||||
.getCharacteristic(Characteristic.On)
|
||||
.on('set', this.setPowerState.bind(this));
|
||||
}
|
||||
|
||||
return [informationService, switchService];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports.accessory = HomeAssistantLight;
|
||||
module.exports.accessory = HomeAssistantMediaPlayer;
|
||||
module.exports.accessory = HomeAssistantSwitch;
|
||||
module.exports.platform = HomeAssistantPlatform;
|
||||
1066
platforms/HomeSeer.js
Normal file
1066
platforms/HomeSeer.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
var types = require("HAP-NodeJS/accessories/types.js");
|
||||
var types = require("hap-nodejs/accessories/types.js");
|
||||
var xml2js = require('xml2js');
|
||||
var request = require('request');
|
||||
var util = require('util');
|
||||
|
||||
552
platforms/Indigo.js
Normal file
552
platforms/Indigo.js
Normal file
@@ -0,0 +1,552 @@
|
||||
// Indigo Platform Shim for HomeBridge
|
||||
// Written by Mike Riccio (https://github.com/webdeck)
|
||||
// Based on many of the other HomeBridge plartform modules
|
||||
// See http://www.indigodomo.com/ for more info on Indigo
|
||||
//
|
||||
// Remember to add platform to config.json. Example:
|
||||
// "platforms": [
|
||||
// {
|
||||
// "platform": "Indigo", // required
|
||||
// "name": "Indigo", // required
|
||||
// "host": "127.0.0.1", // required
|
||||
// "port": "8176", // required
|
||||
// "username": "username", // optional
|
||||
// "password": "password" // optional
|
||||
// }
|
||||
// ],
|
||||
//
|
||||
// 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 Characteristic = require("hap-nodejs").Characteristic;
|
||||
var request = require('request');
|
||||
var async = require('async');
|
||||
|
||||
|
||||
function IndigoPlatform(log, config) {
|
||||
this.log = log;
|
||||
|
||||
this.baseURL = "http://" + config["host"] + ":" + config["port"];
|
||||
|
||||
if (config["username"] && config["password"]) {
|
||||
this.auth = {
|
||||
'user': config["username"],
|
||||
'pass': config["password"],
|
||||
'sendImmediately': false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
IndigoPlatform.prototype = {
|
||||
accessories: function(callback) {
|
||||
var that = this;
|
||||
this.log("Discovering Indigo Devices.");
|
||||
|
||||
var options = {
|
||||
url: this.baseURL + "/devices.json/",
|
||||
method: 'GET'
|
||||
};
|
||||
if (this.auth) {
|
||||
options['auth'] = this.auth;
|
||||
}
|
||||
this.foundAccessories = [];
|
||||
this.callback = callback;
|
||||
|
||||
request(options, function(error, response, body) {
|
||||
if (error) {
|
||||
console.trace("Requesting Indigo devices.");
|
||||
that.log(error);
|
||||
return error;
|
||||
}
|
||||
|
||||
// Cheesy hack because response may have an extra comma at the start of the array, which is invalid
|
||||
var firstComma = body.indexOf(",");
|
||||
if (firstComma < 10) {
|
||||
body = "[" + body.substr(firstComma + 1);
|
||||
}
|
||||
|
||||
var json = JSON.parse(body);
|
||||
async.eachSeries(json, function(item, asyncCallback) {
|
||||
var deviceURL = that.baseURL + item.restURL;
|
||||
var deviceOptions = {
|
||||
url: deviceURL,
|
||||
method: 'GET'
|
||||
};
|
||||
if (that.auth) {
|
||||
deviceOptions['auth'] = that.auth;
|
||||
}
|
||||
|
||||
request(deviceOptions, function(deviceError, deviceResponse, deviceBody) {
|
||||
if (deviceError) {
|
||||
console.trace("Requesting Indigo device info: " + deviceURL + "\nError: " + deviceError + "\nResponse: " + deviceBody);
|
||||
}
|
||||
else {
|
||||
try {
|
||||
var deviceJson = JSON.parse(deviceBody);
|
||||
that.log("Discovered " + deviceJson.type + ": " + deviceJson.name);
|
||||
that.foundAccessories.push(
|
||||
new IndigoAccessory(that.log, that.auth, deviceURL, deviceJson));
|
||||
}
|
||||
catch (e) {
|
||||
that.log("Error parsing Indigo device info: " + deviceURL + "\nException: " + e + "\nResponse: " + deviceBody);
|
||||
}
|
||||
}
|
||||
asyncCallback();
|
||||
});
|
||||
}, function(asyncError) {
|
||||
// This will be called after all the requests complete
|
||||
if (asyncError) {
|
||||
console.trace("Requesting Indigo device info.");
|
||||
that.log(asyncError);
|
||||
}
|
||||
|
||||
that.callback(that.foundAccessories.sort(function (a,b) {
|
||||
return (a.name > b.name) - (a.name < b.name);
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function IndigoAccessory(log, auth, deviceURL, json) {
|
||||
this.log = log;
|
||||
this.auth = auth;
|
||||
this.deviceURL = deviceURL;
|
||||
|
||||
for (var prop in json) {
|
||||
if (json.hasOwnProperty(prop)) {
|
||||
this[prop] = json[prop];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IndigoAccessory.prototype = {
|
||||
getStatus: function(callback) {
|
||||
var that = this;
|
||||
|
||||
var options = {
|
||||
url: this.deviceURL,
|
||||
method: 'GET'
|
||||
};
|
||||
if (this.auth) {
|
||||
options['auth'] = this.auth;
|
||||
}
|
||||
|
||||
request(options, function(error, response, body) {
|
||||
if (error) {
|
||||
console.trace("Requesting Device Status.");
|
||||
that.log(error);
|
||||
}
|
||||
else {
|
||||
that.log("getStatus of " + that.name + ": " + body);
|
||||
try {
|
||||
var json = JSON.parse(body);
|
||||
callback(json);
|
||||
}
|
||||
catch (e) {
|
||||
console.trace("Requesting Device Status.");
|
||||
that.log("Exception: " + e + "\nResponse: " + body);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
updateStatus: function(params) {
|
||||
var that = this;
|
||||
var options = {
|
||||
url: this.deviceURL + "?" + params,
|
||||
method: 'PUT'
|
||||
};
|
||||
if (this.auth) {
|
||||
options['auth'] = this.auth;
|
||||
}
|
||||
|
||||
this.log("updateStatus of " + that.name + ": " + params);
|
||||
request(options, function(error, response, body) {
|
||||
if (error) {
|
||||
console.trace("Updating Device Status.");
|
||||
that.log(error);
|
||||
return error;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
query: function(prop, callback) {
|
||||
this.getStatus(function(json) {
|
||||
callback(json[prop]);
|
||||
});
|
||||
},
|
||||
|
||||
turnOn: function() {
|
||||
if (this.typeSupportsOnOff) {
|
||||
this.updateStatus("isOn=1");
|
||||
}
|
||||
},
|
||||
|
||||
turnOff: function() {
|
||||
if (this.typeSupportsOnOff) {
|
||||
this.updateStatus("isOn=0");
|
||||
}
|
||||
},
|
||||
|
||||
setBrightness: function(brightness) {
|
||||
if (this.typeSupportsDim && brightness >= 0 && brightness <= 100) {
|
||||
this.updateStatus("brightness=" + brightness);
|
||||
}
|
||||
},
|
||||
|
||||
setSpeedIndex: function(speedIndex) {
|
||||
if (this.typeSupportsSpeedControl && speedIndex >= 0 && speedIndex <= 3) {
|
||||
this.updateStatus("speedIndex=" + speedIndex);
|
||||
}
|
||||
},
|
||||
|
||||
getCurrentHeatingCooling: function(callback) {
|
||||
this.getStatus(function(json) {
|
||||
var mode = 0;
|
||||
if (json["hvacOperatonModeIsHeat"]) {
|
||||
mode = 1;
|
||||
}
|
||||
else if (json["hvacOperationModeIsCool"]) {
|
||||
mode = 2;
|
||||
}
|
||||
else if (json["hvacOperationModeIsAuto"]) {
|
||||
mode = 3;
|
||||
}
|
||||
callback(mode);
|
||||
});
|
||||
},
|
||||
|
||||
setTargetHeatingCooling: function(mode) {
|
||||
if (mode == 0) {
|
||||
param = "Off";
|
||||
}
|
||||
else if (mode == 1) {
|
||||
param = "Heat";
|
||||
}
|
||||
else if (mode == 2) {
|
||||
param = "Cool";
|
||||
}
|
||||
else if (mode == 3) {
|
||||
param = "Auto";
|
||||
}
|
||||
|
||||
if (param) {
|
||||
this.updateStatus("hvacOperationModeIs" + param + "=true");
|
||||
}
|
||||
},
|
||||
|
||||
// Note: HomeKit wants all temperature values to be in celsius
|
||||
getCurrentTemperature: function(callback) {
|
||||
this.query("displayRawState", function(temperature) {
|
||||
callback((temperature - 32.0) * 5.0 / 9.0);
|
||||
});
|
||||
},
|
||||
|
||||
getTargetTemperature: function(callback) {
|
||||
this.getStatus(function(json) {
|
||||
var temperature;
|
||||
if (json["hvacOperatonModeIsHeat"]) {
|
||||
temperature = json["setpointHeat"];
|
||||
}
|
||||
else if (json["hvacOperationModeIsCool"]) {
|
||||
temperature = json["setpointCool"];
|
||||
}
|
||||
else {
|
||||
temperature = (json["setpointHeat"] + json["setpointCool"]) / 2.0;
|
||||
}
|
||||
callback((temperature - 32.0) * 5.0 / 9.0);
|
||||
});
|
||||
},
|
||||
|
||||
setTargetTemperature: function(temperature) {
|
||||
var that = this;
|
||||
var t = (temperature * 9.0 / 5.0) + 32.0;
|
||||
this.getStatus(function(json) {
|
||||
if (json["hvacOperatonModeIsHeat"]) {
|
||||
that.updateStatus("setpointHeat=" + t);
|
||||
}
|
||||
else if (json["hvacOperationModeIsCool"]) {
|
||||
that.updateStatus("setpointCool=" + t);
|
||||
}
|
||||
else {
|
||||
var cool = t + 5;
|
||||
var heat = t - 5;
|
||||
that.updateStatus("setpointCool=" + cool + "&setpointHeat=" + heat);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
informationCharacteristics: function() {
|
||||
return [
|
||||
{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: [Characteristic.Perms.READ],
|
||||
format: Characteristic.Formats.STRING,
|
||||
initialValue: this.name,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of the accessory",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MANUFACTURER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: [Characteristic.Perms.READ],
|
||||
format: Characteristic.Formats.STRING,
|
||||
initialValue: "Indigo",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Manufacturer",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MODEL_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: [Characteristic.Perms.READ],
|
||||
format: Characteristic.Formats.STRING,
|
||||
initialValue: this.type,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Model",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.SERIAL_NUMBER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: [Characteristic.Perms.READ],
|
||||
format: Characteristic.Formats.STRING,
|
||||
initialValue: this.addressStr,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "SN",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.IDENTIFY_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: [Characteristic.Perms.WRITE],
|
||||
format: Characteristic.Formats.BOOL,
|
||||
initialValue: false,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Identify Accessory",
|
||||
designedMaxLength: 1
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
controlCharacteristics: function(that) {
|
||||
var hasAType = false;
|
||||
|
||||
var cTypes = [{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: [Characteristic.Perms.READ],
|
||||
format: Characteristic.Formats.STRING,
|
||||
initialValue: that.name,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of the accessory",
|
||||
designedMaxLength: 255
|
||||
}];
|
||||
|
||||
if (that.typeSupportsDim) {
|
||||
hasAType = true;
|
||||
cTypes.push({
|
||||
cType: types.BRIGHTNESS_CTYPE,
|
||||
perms: [Characteristic.Perms.WRITE,Characteristic.Perms.READ,Characteristic.Perms.NOTIFY],
|
||||
format: Characteristic.Formats.INT,
|
||||
initialValue: that.brightness,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Adjust Brightness of Light",
|
||||
designedMinValue: 0,
|
||||
designedMaxValue: 100,
|
||||
designedMinStep: 1,
|
||||
unit: Characteristic.Units.PERCENTAGE,
|
||||
onUpdate: function(value) {
|
||||
that.setBrightness(value);
|
||||
},
|
||||
onRead: function(callback) {
|
||||
that.query("brightness", callback);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (that.typeSupportsSpeedControl) {
|
||||
hasAType = true;
|
||||
cTypes.push({
|
||||
cType: types.ROTATION_SPEED_CTYPE,
|
||||
perms: [Characteristic.Perms.WRITE,Characteristic.Perms.READ,Characteristic.Perms.NOTIFY],
|
||||
format: Characteristic.Formats.INT,
|
||||
initialValue: 0,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Change the speed of the fan",
|
||||
designedMaxLength: 1,
|
||||
designedMinValue: 0,
|
||||
designedMaxValue: 3,
|
||||
designedMinStep: 1,
|
||||
onUpdate: function(value) {
|
||||
that.setSpeedIndex(value);
|
||||
},
|
||||
onRead: function(callback) {
|
||||
that.query("speedIndex", callback);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (that.typeSupportsHVAC) {
|
||||
hasAType = true;
|
||||
cTypes.push({
|
||||
cType: types.CURRENTHEATINGCOOLING_CTYPE,
|
||||
perms: [Characteristic.Perms.READ,Characteristic.Perms.NOTIFY],
|
||||
format: Characteristic.Formats.INT,
|
||||
initialValue: 0,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Current Mode",
|
||||
designedMaxLength: 1,
|
||||
designedMinValue: 0,
|
||||
designedMaxValue: 3,
|
||||
designedMinStep: 1,
|
||||
onUpdate: null,
|
||||
onRead: function(callback) {
|
||||
that.getCurrentHeatingCooling(callback);
|
||||
}
|
||||
});
|
||||
|
||||
cTypes.push({
|
||||
cType: types.TARGETHEATINGCOOLING_CTYPE,
|
||||
perms: [Characteristic.Perms.WRITE,Characteristic.Perms.READ,Characteristic.Perms.NOTIFY],
|
||||
format: Characteristic.Formats.INT,
|
||||
initialValue: 0,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Target Mode",
|
||||
designedMaxLength: 1,
|
||||
designedMinValue: 0,
|
||||
designedMaxValue: 3,
|
||||
designedMinStep: 1,
|
||||
onUpdate: function(value) {
|
||||
that.setTargetHeatingCooling(value);
|
||||
},
|
||||
onRead: function(callback) {
|
||||
that.getCurrentHeatingCooling(callback);
|
||||
}
|
||||
});
|
||||
|
||||
cTypes.push({
|
||||
cType: types.CURRENT_TEMPERATURE_CTYPE,
|
||||
perms: [Characteristic.Perms.READ,Characteristic.Perms.NOTIFY],
|
||||
format: Characteristic.Formats.INT,
|
||||
designedMinValue: 16,
|
||||
designedMaxValue: 38,
|
||||
designedMinStep: 1,
|
||||
initialValue: 20,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Current Temperature",
|
||||
unit: Characteristic.Units.FAHRENHEIT,
|
||||
onUpdate: null,
|
||||
onRead: function(callback) {
|
||||
that.getCurrentTemperature(callback);
|
||||
}
|
||||
});
|
||||
|
||||
cTypes.push({
|
||||
cType: types.TARGET_TEMPERATURE_CTYPE,
|
||||
perms: [Characteristic.Perms.WRITE,Characteristic.Perms.READ,Characteristic.Perms.NOTIFY],
|
||||
format: Characteristic.Formats.INT,
|
||||
designedMinValue: 16,
|
||||
designedMaxValue: 38,
|
||||
designedMinStep: 1,
|
||||
initialValue: 20,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Target Temperature",
|
||||
unit: Characteristic.Units.FAHRENHEIT,
|
||||
onUpdate: function(value) {
|
||||
that.setTargetTemperature(value);
|
||||
},
|
||||
onRead: function(callback) {
|
||||
that.getTargetTemperature(callback);
|
||||
}
|
||||
});
|
||||
|
||||
cTypes.push({
|
||||
cType: types.TEMPERATURE_UNITS_CTYPE,
|
||||
perms: [Characteristic.Perms.READ,Characteristic.Perms.NOTIFY],
|
||||
format: Characteristic.Formats.INT,
|
||||
initialValue: 1,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Unit",
|
||||
onUpdate: null,
|
||||
onRead: function(callback) {
|
||||
callback(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (that.typeSupportsOnOff || !hasAType) {
|
||||
cTypes.push({
|
||||
cType: types.POWER_STATE_CTYPE,
|
||||
perms: [Characteristic.Perms.WRITE,Characteristic.Perms.READ,Characteristic.Perms.NOTIFY],
|
||||
format: Characteristic.Formats.BOOL,
|
||||
initialValue: (that.isOn) ? 1 : 0,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Change the power state",
|
||||
designedMaxLength: 1,
|
||||
onUpdate: function(value) {
|
||||
if (value == 0) {
|
||||
that.turnOff();
|
||||
} else {
|
||||
that.turnOn();
|
||||
}
|
||||
},
|
||||
onRead: function(callback) {
|
||||
that.query("isOn", function(isOn) {
|
||||
callback((isOn) ? 1 : 0);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return cTypes;
|
||||
},
|
||||
|
||||
sType: function() {
|
||||
if (this.typeSupportsHVAC) {
|
||||
return types.THERMOSTAT_STYPE;
|
||||
} else if (this.typeSupportsDim) {
|
||||
return types.LIGHTBULB_STYPE;
|
||||
} else if (this.typeSupportsSpeedControl) {
|
||||
return types.FAN_STYPE;
|
||||
} else if (this.typeSupportsOnOff) {
|
||||
return types.SWITCH_STYPE;
|
||||
}
|
||||
|
||||
return types.SWITCH_STYPE;
|
||||
},
|
||||
|
||||
getServices: function() {
|
||||
var that = this;
|
||||
var services = [{
|
||||
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||
characteristics: that.informationCharacteristics(),
|
||||
},
|
||||
{
|
||||
sType: that.sType(),
|
||||
characteristics: that.controlCharacteristics(that)
|
||||
}];
|
||||
|
||||
that.log("Loaded services for " + that.name);
|
||||
return services;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.accessory = IndigoAccessory;
|
||||
module.exports.platform = IndigoPlatform;
|
||||
156
platforms/KNX-sample-config.json
Normal file
156
platforms/KNX-sample-config.json
Normal file
@@ -0,0 +1,156 @@
|
||||
{
|
||||
"bridge": {
|
||||
"name": "Homebridge",
|
||||
"username": "CC:22:3D:E3:CE:30",
|
||||
"port": 51826,
|
||||
"pin": "031-45-154"
|
||||
},
|
||||
"description": "This is an example configuration file for KNX platform shim",
|
||||
"hint": "Always paste into jsonlint.com validation page before starting your homebridge, saves a lot of frustration",
|
||||
"hint2": "Replace all group addresses by current addresses of your installation, these are arbitrary examples!",
|
||||
"hint3": "For valid services and their characteristics have a look at the KNX.md file in folder platforms!",
|
||||
"platforms": [
|
||||
{
|
||||
"platform": "KNX",
|
||||
"name": "KNX",
|
||||
"knxd_ip": "192.168.178.205",
|
||||
"knxd_port": 6720,
|
||||
"accessories": [
|
||||
{
|
||||
"accessory_type": "knxdevice",
|
||||
"description": "Only generic type knxdevice is supported, all previous knx types have been merged into that.",
|
||||
"name": "Living Room North Lamp",
|
||||
"services": [
|
||||
{
|
||||
"type": "Lightbulb",
|
||||
"description": "iOS8 Lightbulb type, supports On (Switch) and Brightness",
|
||||
"name": "Living Room North Lamp",
|
||||
"On": {
|
||||
"Set": "1/1/6",
|
||||
"Listen": [
|
||||
"1/1/63"
|
||||
]
|
||||
},
|
||||
"Brightness": {
|
||||
"Set": "1/1/62",
|
||||
"Listen": [
|
||||
"1/1/64"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"services-description": "Services is an array, you CAN have multiple service types in one accessory, though it is not fully supported in many iOS HK apps, such as EVE and myTouchHome"
|
||||
},
|
||||
{
|
||||
"accessory_type": "knxdevice",
|
||||
"name": "Office Temperature",
|
||||
"description": "iOS8.4.1 TemperatureSensor type, supports CurrentTemperature",
|
||||
"services": [
|
||||
{
|
||||
"type": "TemperatureSensor",
|
||||
"name": "Raumtemperatur",
|
||||
"CurrentTemperature": {
|
||||
"Listen": "3/3/44"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"accessory_type": "knxdevice",
|
||||
"name": "Office Window Lock",
|
||||
"services": [
|
||||
{
|
||||
"type": "LockMechanism",
|
||||
"description": "iOS8 Lock mechanism, Supports LockCurrentState, LockTargetState, append R to the addresses if LOCKED is 1",
|
||||
"name": "Office Window Lock",
|
||||
"LockCurrentState": {
|
||||
"Listen": "5/3/15R"
|
||||
},
|
||||
"LockTargetState": {
|
||||
"Listen": "5/3/16R"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"accessory_type": "knxdevice",
|
||||
"description": "sample device with multiple services. Multiple services of different types are widely supported",
|
||||
"name": "Office",
|
||||
"services": [
|
||||
{
|
||||
"type": "Lightbulb",
|
||||
"name": "Office Lamp",
|
||||
"On": {
|
||||
"Set": "1/3/5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Thermostat",
|
||||
"description": "iOS8 Thermostat type, supports CurrentTemperature, TargetTemperature, CurrentHeatingCoolingState ",
|
||||
"name": "Raumtemperatur",
|
||||
"CurrentTemperature": {
|
||||
"Listen": "3/3/44"
|
||||
},
|
||||
"TargetTemperature": {
|
||||
"Set": "3/3/94"
|
||||
},
|
||||
"CurrentHeatingCoolingState": {
|
||||
"Listen": "3/3/64"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "WindowCovering",
|
||||
"description": "iOS9 Window covering (blinds etc) type, still WIP",
|
||||
"name": "Blinds",
|
||||
"TargetPosition": {
|
||||
"Set": "1/2/3",
|
||||
"Listen": "1/2/4"
|
||||
},
|
||||
"CurrentPosition": {
|
||||
"Set": "1/3/1",
|
||||
"Listen": "1/3/2"
|
||||
},
|
||||
"PositionState": {
|
||||
"Listen": "2/7/1"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"accessory_type": "knxdevice",
|
||||
"description": "sample contact sensor device",
|
||||
"name": "Office Contact",
|
||||
"services": [
|
||||
{
|
||||
"type": "ContactSensor",
|
||||
"name": "Office Door",
|
||||
"ContactSensorState": {
|
||||
"Listen": "5/3/5"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"accessory_type": "knxdevice",
|
||||
"description": "sample garage door opener",
|
||||
"name": "Office Garage",
|
||||
"services": [
|
||||
{
|
||||
"type": "GarageDoorOpener",
|
||||
"name": "Office Garage Opener",
|
||||
"CurrentDoorState": {
|
||||
"Listen": "5/4/5"
|
||||
},
|
||||
"TargetDoorState": {
|
||||
"Listen": "5/4/6"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"accessories": [
|
||||
|
||||
]
|
||||
}
|
||||
205
platforms/KNX.js
Normal file
205
platforms/KNX.js
Normal file
@@ -0,0 +1,205 @@
|
||||
/** Sample platform outline
|
||||
* based on Sonos platform
|
||||
*/
|
||||
'use strict';
|
||||
var types = require("hap-nodejs/accessories/types.js");
|
||||
|
||||
var knxd = require('eibd');
|
||||
|
||||
function KNXPlatform(log, config){
|
||||
this.log = log;
|
||||
this.config = config;
|
||||
|
||||
|
||||
|
||||
// initiate connection to bus for listening ==> done with first shim
|
||||
|
||||
}
|
||||
|
||||
KNXPlatform.prototype = {
|
||||
accessories: function(callback) {
|
||||
this.log("Fetching KNX devices.");
|
||||
var that = this;
|
||||
|
||||
|
||||
// iterate through all devices the platform my offer
|
||||
// for each device, create an accessory
|
||||
|
||||
// read accessories from file !!!!!
|
||||
var foundAccessories = this.config.accessories;
|
||||
|
||||
|
||||
//create array of accessories
|
||||
var myAccessories = [];
|
||||
|
||||
for (var int = 0; int < foundAccessories.length; int++) {
|
||||
this.log("parsing acc " + int + " of " + foundAccessories.length);
|
||||
// instantiate and push to array
|
||||
switch (foundAccessories[int].accessory_type) {
|
||||
case "knxdevice":
|
||||
this.log("push new universal device "+foundAccessories[int].name);
|
||||
// push knxd connection setting to each device from platform
|
||||
foundAccessories[int].knxd_ip = this.config.knxd_ip;
|
||||
foundAccessories[int].knxd_port = this.config.knxd_port;
|
||||
var accConstructor = require('./../accessories/knxdevice.js');
|
||||
var acc = new accConstructor.accessory(this.log,foundAccessories[int]);
|
||||
this.log("created "+acc.name+" universal accessory");
|
||||
myAccessories.push(acc);
|
||||
break;
|
||||
default:
|
||||
// do something else
|
||||
this.log("unkown accessory type found");
|
||||
}
|
||||
|
||||
}
|
||||
// if done, return the array to callback function
|
||||
this.log("returning "+myAccessories.length+" accessories");
|
||||
callback(myAccessories);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* The buscallbacks module is to expose a simple function to listen on the bus and register callbacks for value changes
|
||||
* of registered addresses.
|
||||
*
|
||||
* Usage:
|
||||
* You can start the monitoring process at any time
|
||||
startMonitor({host: name-ip, port: port-num });
|
||||
|
||||
* You can add addresses to the subscriptions using
|
||||
|
||||
registerGA(groupAddress, callback)
|
||||
|
||||
* groupAddress has to be an groupAddress in common knx notation string '1/2/3'
|
||||
* the callback has to be a
|
||||
* var f = function(value) { handle value update;}
|
||||
* so you can do a
|
||||
* registerGA('1/2/3', function(value){
|
||||
* console.log('1/2/3 got a hit with '+value);
|
||||
* });
|
||||
* but of course it is meant to be used programmatically, not literally, otherwise it has no advantage
|
||||
*
|
||||
* You can also use arrays of addresses if your callback is supposed to listen to many addresses:
|
||||
|
||||
registerGA(groupAddresses[], callback)
|
||||
|
||||
* as in
|
||||
* registerGA(['1/2/3','1/0/0'], function(value){
|
||||
* console.log('1/2/3 or 1/0/0 got a hit with '+value);
|
||||
* });
|
||||
* if you are having central addresses like "all lights off" or additional response objects
|
||||
*
|
||||
*
|
||||
* callbacks can have a signature of
|
||||
* function(value, src, dest, type) but do not have to support these parameters (order matters)
|
||||
* src = physical address such as '1.1.20'
|
||||
* dest = groupAddress hit (you subscribed to that address, remember?), as '1/2/3'
|
||||
* type = Data point type, as 'DPT1'
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
|
||||
//array of registered addresses and their callbacks
|
||||
var subscriptions = [];
|
||||
//check variable to avoid running two listeners
|
||||
var running;
|
||||
|
||||
function groupsocketlisten(opts, callback) {
|
||||
var conn = knxd.Connection();
|
||||
conn.socketRemote(opts, function() {
|
||||
conn.openGroupSocket(0, callback);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
var registerSingleGA = function registerSingleGA (groupAddress, callback, reverse) {
|
||||
subscriptions.push({address: groupAddress, callback: callback, reverse:reverse });
|
||||
};
|
||||
|
||||
/*
|
||||
* public busMonitor.startMonitor()
|
||||
* starts listening for telegrams on KNX bus
|
||||
*
|
||||
*/
|
||||
var startMonitor = function startMonitor(opts) { // using { host: name-ip, port: port-num } options object
|
||||
if (!running) {
|
||||
running = true;
|
||||
} else {
|
||||
console.log("<< knxd socket listener already running >>");
|
||||
return null;
|
||||
}
|
||||
console.log(">>> knxd groupsocketlisten starting <<<");
|
||||
groupsocketlisten(opts, function(parser) {
|
||||
//console.log("knxfunctions.read: in callback parser");
|
||||
parser.on('write', function(src, dest, type, val){
|
||||
// search the registered group addresses
|
||||
//console.log('recv: Write from '+src+' to '+dest+': '+val+' ['+type+'], listeners:' + subscriptions.length);
|
||||
for (var i = 0; i < subscriptions.length; i++) {
|
||||
// iterate through all registered addresses
|
||||
if (subscriptions[i].address === dest) {
|
||||
// found one, notify
|
||||
console.log('HIT: Write from '+src+' to '+dest+': '+val+' ['+type+']');
|
||||
subscriptions[i].callback(val, src, dest, type, subscriptions[i].reverse);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
parser.on('response', function(src, dest, type, val) {
|
||||
// search the registered group addresses
|
||||
// console.log('recv: resp from '+src+' to '+dest+': '+val+' ['+type+']');
|
||||
for (var i = 0; i < subscriptions.length; i++) {
|
||||
// iterate through all registered addresses
|
||||
if (subscriptions[i].address === dest) {
|
||||
// found one, notify
|
||||
// console.log('HIT: Response from '+src+' to '+dest+': '+val+' ['+type+']');
|
||||
subscriptions[i].callback(val, src, dest, type, subscriptions[i].reverse);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
//dont care about reads here
|
||||
// parser.on('read', function(src, dest) {
|
||||
// console.log('Read from '+src+' to '+dest);
|
||||
// });
|
||||
//console.log("knxfunctions.read: in callback parser at end");
|
||||
}); // groupsocketlisten parser
|
||||
}; //startMonitor
|
||||
|
||||
|
||||
/*
|
||||
* public registerGA(groupAdresses[], callback(value))
|
||||
* parameters
|
||||
* callback: function(value, src, dest, type) called when a value is sent on the bus
|
||||
* groupAddresses: (Array of) string(s) for group addresses
|
||||
*
|
||||
*
|
||||
*
|
||||
*/
|
||||
var registerGA = function (groupAddresses, callback) {
|
||||
// check if the groupAddresses is an array
|
||||
if (groupAddresses.constructor.toString().indexOf("Array") > -1) {
|
||||
// handle multiple addresses
|
||||
for (var i = 0; i < groupAddresses.length; i++) {
|
||||
if (groupAddresses[i] && groupAddresses[i].match(/(\d*\/\d*\/\d*)/)) { // do not bind empty addresses or invalid addresses
|
||||
// clean the addresses
|
||||
registerSingleGA (groupAddresses[i].match(/(\d*\/\d*\/\d*)/)[0], callback,groupAddresses[i].match(/\d*\/\d*\/\d*(R)/) ? true:false );
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// it's only one
|
||||
if (groupAddresses.match(/(\d*\/\d*\/\d*)/)) {
|
||||
registerSingleGA (groupAddresses.match(/(\d*\/\d*\/\d*)/)[0], callback, groupAddresses.match(/\d*\/\d*\/\d*(R)/) ? true:false);
|
||||
}
|
||||
}
|
||||
// console.log("listeners now: " + subscriptions.length);
|
||||
};
|
||||
|
||||
|
||||
|
||||
module.exports.platform = KNXPlatform;
|
||||
module.exports.registerGA = registerGA;
|
||||
module.exports.startMonitor = startMonitor;
|
||||
213
platforms/KNX.md
Normal file
213
platforms/KNX.md
Normal file
@@ -0,0 +1,213 @@
|
||||
# Syntax of the config.json
|
||||
In the platforms section, you can insert a KNX type platform.
|
||||
You need to configure all devices directly in the config.json.
|
||||
````json
|
||||
"platforms": [
|
||||
{
|
||||
"platform": "KNX",
|
||||
"name": "KNX",
|
||||
"knxd_ip": "192.168.178.205",
|
||||
"knxd_port": 6720,
|
||||
"accessories": [
|
||||
{
|
||||
"accessory_type": "knxdevice",
|
||||
"name": "Living Room North Lamp",
|
||||
"services": [
|
||||
{
|
||||
"type": "Lightbulb",
|
||||
"description": "iOS8 Lightbulb type, supports On (Switch) and Brightness",
|
||||
"name": "Living Room North Lamp",
|
||||
"On": {
|
||||
"Set": "1/1/6",
|
||||
"Listen": ["1/1/63"]
|
||||
},
|
||||
"Brightness": {
|
||||
"Set": "1/1/62",
|
||||
"Listen": ["1/1/64"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
````
|
||||
In the accessories section (the array within the brackets [ ]) you can insert as many objects as you like in the following form
|
||||
````json
|
||||
{
|
||||
"accessory_type": "knxdevice",
|
||||
"name": "Here goes your display name, this will be shown in HomeKit apps",
|
||||
"services": [
|
||||
{
|
||||
}
|
||||
]
|
||||
}
|
||||
````
|
||||
You have to add services in the following syntax:
|
||||
````json
|
||||
{
|
||||
"type": "SERVICENAME",
|
||||
"description": "This is just for you to remember things",
|
||||
"name": "beer tap thermostat",
|
||||
"CHARACTERISTIC1": {
|
||||
"Set": "1/1/6",
|
||||
"Listen": [
|
||||
"1/1/63"
|
||||
]
|
||||
},
|
||||
"CHARACTERISTIC2": {
|
||||
"Set": "1/1/62",
|
||||
"Listen": [
|
||||
"1/1/64"
|
||||
]
|
||||
}
|
||||
}
|
||||
````
|
||||
`CHARACTERISTICx` are properties that are dependent on the service type, so they are listed below.
|
||||
|
||||
Two kinds of addresses are supported: `"Set":"1/2/3"` is a writable group address, to which changes are sent if the service supports changing values. Changes on the bus are listened to, too.
|
||||
`"Listen":["1/2/3","1/2/4","1/2/5"]` is an array of addresses that are listened to additionally. To these addresses never values get written, but the on startup the service will issue *KNX read requests* to ALL addresses listed in `Set:` and in `Listen:`
|
||||
|
||||
|
||||
For two characteristics there are additional minValue and maxValue attributes. These are CurrentTemperature and TargetTemperature, and are used in TemperatureSensor and Thermostat.
|
||||
|
||||
So the charcteristic section may look like:
|
||||
|
||||
````json
|
||||
{
|
||||
"type": "Thermostat",
|
||||
"description": "Sample thermostat",
|
||||
"name": "We need a name for each service, though it usually shows only if multiple services are present in one accessory",
|
||||
"CurrentTemperature": {
|
||||
"Set": "1/1/6",
|
||||
"Listen": [
|
||||
"1/1/63"
|
||||
],
|
||||
"minValue": -18,
|
||||
"maxValue": 30
|
||||
},
|
||||
"TargetTemperature": {
|
||||
"Set": "1/1/62",
|
||||
"Listen": [
|
||||
"1/1/64"
|
||||
],
|
||||
"minValue": -4,
|
||||
"maxValue": 12
|
||||
}
|
||||
}
|
||||
````
|
||||
|
||||
|
||||
## reversal of values for characteristics
|
||||
In general, all DPT1 types can be reversed. If you need a 1 for "contact" of a contact senser, you can append an "R" to the group address.
|
||||
Likewise, all percentages of DPT5 can be reversed, if you need a 100% (=255) for window closed, append an "R" to the group address. Do not forget the listening addresses!
|
||||
````json
|
||||
{
|
||||
"type": "ContactSensor",
|
||||
"description": "Sample ContactSensor with 1 as contact (0 is Apple's default)",
|
||||
"name": "WindowContact1",
|
||||
"ContactSensorState": {
|
||||
"Listen": [
|
||||
"1/1/100R"
|
||||
]
|
||||
}
|
||||
}
|
||||
````
|
||||
# Supported Services and their characteristics
|
||||
## ContactSensor
|
||||
- ContactSensorState: DPT 1.002, 0 as contact
|
||||
- ~~ContactSensorStateContact1: DPT 1.002, 1 as contact~~
|
||||
|
||||
- StatusActive: DPT 1.011, 1 as true
|
||||
- StatusFault: DPT 1.011, 1 as true
|
||||
- StatusTampered: DPT 1.011, 1 as true
|
||||
- StatusLowBattery: DPT 1.011, 1 as true
|
||||
|
||||
## GarageDoorOpener
|
||||
- CurrentDoorState: DPT5 integer value in range 0..4
|
||||
// Characteristic.CurrentDoorState.OPEN = 0;
|
||||
// Characteristic.CurrentDoorState.CLOSED = 1;
|
||||
// Characteristic.CurrentDoorState.OPENING = 2;
|
||||
// Characteristic.CurrentDoorState.CLOSING = 3;
|
||||
// Characteristic.CurrentDoorState.STOPPED = 4;
|
||||
|
||||
- TargetDoorState: DPT5 integer value in range 0..1
|
||||
// Characteristic.TargetDoorState.OPEN = 0;
|
||||
// Characteristic.TargetDoorState.CLOSED = 1;
|
||||
|
||||
- ObstructionDetected: DPT1, 1 as true
|
||||
|
||||
- LockCurrentState: DPT5 integer value in range 0..3
|
||||
// Characteristic.LockCurrentState.UNSECURED = 0;
|
||||
// Characteristic.LockCurrentState.SECURED = 1;
|
||||
// Characteristic.LockCurrentState.JAMMED = 2;
|
||||
// Characteristic.LockCurrentState.UNKNOWN = 3;
|
||||
|
||||
- LockTargetState: DPT5 integer value in range 0..1
|
||||
// Characteristic.LockTargetState.UNSECURED = 0;
|
||||
// Characteristic.LockTargetState.SECURED = 1;
|
||||
|
||||
|
||||
|
||||
## Lightbulb
|
||||
- On: DPT 1.001, 1 as on, 0 as off
|
||||
- Brightness: DPT5.001 percentage, 100% (=255) the brightest
|
||||
|
||||
## LightSensor
|
||||
- CurrentAmbientLightLevel: DPT 9.004, 0 to 100000 Lux
|
||||
|
||||
## LockMechanism (This is poorly mapped!)
|
||||
- LockCurrentState: DPT 1, 1 as secured
|
||||
- ~~LockCurrentStateSecured0: DPT 1, 0 as secured~~
|
||||
- LockTargetState: DPT 1, 1 as secured
|
||||
- ~~LockTargetStateSecured0: DPT 1, 0 as secured~~
|
||||
|
||||
*ToDo here: correction of mappings, HomeKit reqires lock states UNSECURED=0, SECURED=1, JAMMED = 2, UNKNOWN=3*
|
||||
|
||||
## MotionSensor
|
||||
- MotionDetected: DPT 1.002, 1 as motion detected
|
||||
|
||||
- StatusActive: DPT 1.011, 1 as true
|
||||
- StatusFault: DPT 1.011, 1 as true
|
||||
- StatusTampered: DPT 1.011, 1 as true
|
||||
- StatusLowBattery: DPT 1.011, 1 as true
|
||||
|
||||
## Outlet
|
||||
- On: DPT 1.001, 1 as on, 0 as off
|
||||
- OutletInUse: DPT 1.011, 1 as on, 0 as off
|
||||
|
||||
## Switch
|
||||
- On: DPT 1.001, 1 as on, 0 as off
|
||||
|
||||
## TemperatureSensor
|
||||
- CurrentTemperature: DPT9.001 in °C [listen only]
|
||||
|
||||
## Thermostat
|
||||
- CurrentTemperature: DPT9.001 in °C [listen only], -40 to 80°C if not overriden as shown above
|
||||
- TargetTemperature: DPT9.001, values 0..40°C only, all others are ignored
|
||||
- CurrentHeatingCoolingState: DPT20.102 HVAC, because of the incompatible mapping only off and heating (=auto) are shown, [listen only]
|
||||
- TargetHeatingCoolingState: DPT20.102 HVAC, as above
|
||||
|
||||
## Window
|
||||
- CurrentPosition: DPT5.001 percentage
|
||||
- TargetPosition: DPT5.001 percentage
|
||||
- PositionState: DPT5.005 value [listen only: 0 Increasing, 1 Decreasing, 2 Stopped]
|
||||
|
||||
## WindowCovering
|
||||
- CurrentPosition: DPT5 percentage
|
||||
- TargetPosition: DPT5 percentage
|
||||
- PositionState: DPT5 value [listen only: 0 Closing, 1 Opening, 2 Stopped]
|
||||
|
||||
### not yet supported
|
||||
- HoldPosition
|
||||
- TargetHorizontalTiltAngle
|
||||
- TargetVerticalTiltAngle
|
||||
- CurrentHorizontalTiltAngle
|
||||
- CurrentVerticalTiltAngle
|
||||
- ObstructionDetected
|
||||
|
||||
|
||||
|
||||
|
||||
# DISCLAIMER
|
||||
**This is work in progress!**
|
||||
|
||||
302
platforms/LIFx.js
Normal file
302
platforms/LIFx.js
Normal file
@@ -0,0 +1,302 @@
|
||||
'use strict';
|
||||
|
||||
// LiFX Platform Shim for HomeBridge
|
||||
//
|
||||
// Remember to add platform to config.json. Example:
|
||||
// "platforms": [
|
||||
// {
|
||||
// "platform": "LIFx", // required
|
||||
// "name": "LIFx", // required
|
||||
// "access_token": "access token", // required
|
||||
// "use_lan": "true" // optional set to "true" (gets and sets over the lan) or "get" (gets only over the lan)
|
||||
// }
|
||||
// ],
|
||||
//
|
||||
// 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 Service = require("hap-nodejs").Service;
|
||||
var Characteristic = require("hap-nodejs").Characteristic;
|
||||
var lifxRemoteObj = require('lifx-api');
|
||||
var lifx_remote;
|
||||
|
||||
var lifxLanObj;
|
||||
var lifx_lan;
|
||||
var use_lan;
|
||||
|
||||
function LIFxPlatform(log, config){
|
||||
// auth info
|
||||
this.access_token = config["access_token"];
|
||||
|
||||
lifx_remote = new lifxRemoteObj(this.access_token);
|
||||
|
||||
// use remote or lan api ?
|
||||
use_lan = config["use_lan"] || false;
|
||||
|
||||
if (use_lan != false) {
|
||||
lifxLanObj = require('lifx');
|
||||
lifx_lan = lifxLanObj.init();
|
||||
}
|
||||
|
||||
this.log = log;
|
||||
}
|
||||
|
||||
LIFxPlatform.prototype = {
|
||||
accessories: function(callback) {
|
||||
this.log("Fetching LIFx devices.");
|
||||
|
||||
var that = this;
|
||||
var foundAccessories = [];
|
||||
|
||||
lifx_remote.listLights("all", function(body) {
|
||||
var bulbs = JSON.parse(body);
|
||||
|
||||
for(var i = 0; i < bulbs.length; i ++) {
|
||||
var accessory = new LIFxBulbAccessory(that.log, bulbs[i]);
|
||||
foundAccessories.push(accessory);
|
||||
}
|
||||
callback(foundAccessories)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function LIFxBulbAccessory(log, bulb) {
|
||||
// device info
|
||||
this.name = bulb.label;
|
||||
this.model = bulb.product_name;
|
||||
this.deviceId = bulb.id;
|
||||
this.serial = bulb.uuid;
|
||||
this.capabilities = bulb.capabilities;
|
||||
this.log = log;
|
||||
}
|
||||
|
||||
LIFxBulbAccessory.prototype = {
|
||||
getLan: function(type, callback){
|
||||
var that = this;
|
||||
|
||||
if (!lifx_lan.bulbs[this.deviceId]) {
|
||||
callback(new Error("Device not found"), false);
|
||||
return;
|
||||
}
|
||||
|
||||
lifx_lan.requestStatus();
|
||||
lifx_lan.on('bulbstate', function(bulb) {
|
||||
if (callback == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (bulb.addr.toString('hex') == that.deviceId) {
|
||||
switch(type) {
|
||||
case "power":
|
||||
callback(null, bulb.state.power > 0);
|
||||
break;
|
||||
case "brightness":
|
||||
callback(null, Math.round(bulb.state.brightness * 100 / 65535));
|
||||
break;
|
||||
case "hue":
|
||||
callback(null, Math.round(bulb.state.hue * 360 / 65535));
|
||||
break;
|
||||
case "saturation":
|
||||
callback(null, Math.round(bulb.state.saturation * 100 / 65535));
|
||||
break;
|
||||
}
|
||||
|
||||
callback = null
|
||||
}
|
||||
});
|
||||
},
|
||||
getRemote: function(type, callback){
|
||||
var that = this;
|
||||
|
||||
lifx_remote.listLights("id:"+ that.deviceId, function(body) {
|
||||
var bulb = JSON.parse(body);
|
||||
|
||||
if (bulb.connected != true) {
|
||||
callback(new Error("Device not found"), false);
|
||||
return;
|
||||
}
|
||||
|
||||
switch(type) {
|
||||
case "power":
|
||||
callback(null, bulb.power == "on" ? 1 : 0);
|
||||
break;
|
||||
case "brightness":
|
||||
callback(null, Math.round(bulb.brightness * 100));
|
||||
break;
|
||||
case "hue":
|
||||
callback(null, bulb.color.hue);
|
||||
break;
|
||||
case "saturation":
|
||||
callback(null, Math.round(bulb.color.saturation * 100));
|
||||
break;
|
||||
}
|
||||
});
|
||||
},
|
||||
identify: function(callback) {
|
||||
lifx_remote.breatheEffect("id:"+ this.deviceId, 'green', null, 1, 3, false, true, 0.5, function (body) {
|
||||
callback();
|
||||
});
|
||||
},
|
||||
setLanColor: function(type, value, callback){
|
||||
var bulb = lifx_lan.bulbs[this.deviceId];
|
||||
|
||||
if (!bulb) {
|
||||
callback(new Error("Device not found"), false);
|
||||
return;
|
||||
}
|
||||
|
||||
var state = {
|
||||
hue: bulb.state.hue,
|
||||
saturation: bulb.state.saturation,
|
||||
brightness: bulb.state.brightness,
|
||||
kelvin: bulb.state.kelvin
|
||||
};
|
||||
|
||||
var scale = type == "hue" ? 360 : 100;
|
||||
|
||||
state[type] = Math.round(value * 65535 / scale) & 0xffff;
|
||||
lifx_lan.lightsColour(state.hue, state.saturation, state.brightness, state.kelvin, 0, bulb);
|
||||
|
||||
callback(null);
|
||||
},
|
||||
setLanPower: function(state, callback){
|
||||
var bulb = lifx_lan.bulbs[this.deviceId];
|
||||
|
||||
if (!bulb) {
|
||||
callback(new Error("Device not found"), false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (state) {
|
||||
lifx_lan.lightsOn(bulb);
|
||||
}
|
||||
else {
|
||||
lifx_lan.lightsOff(bulb);
|
||||
}
|
||||
|
||||
callback(null);
|
||||
},
|
||||
setRemoteColor: function(type, value, callback){
|
||||
var color;
|
||||
|
||||
switch(type) {
|
||||
case "brightness":
|
||||
color = "brightness:" + (value / 100);
|
||||
break;
|
||||
case "hue":
|
||||
color = "hue:" + value;
|
||||
break;
|
||||
case "saturation":
|
||||
color = "saturation:" + (value / 100);
|
||||
break;
|
||||
}
|
||||
|
||||
lifx_remote.setColor("id:"+ this.deviceId, color, 0, null, function (body) {
|
||||
callback();
|
||||
});
|
||||
},
|
||||
setRemotePower: function(state, callback){
|
||||
var that = this;
|
||||
|
||||
lifx_remote.setPower("id:"+ that.deviceId, (state == 1 ? "on" : "off"), 0, function (body) {
|
||||
callback();
|
||||
});
|
||||
},
|
||||
getServices: function() {
|
||||
var that = this;
|
||||
var services = []
|
||||
var service = new Service.Lightbulb(this.name);
|
||||
|
||||
switch(use_lan) {
|
||||
case true:
|
||||
case "true":
|
||||
// gets and sets over the lan api
|
||||
service
|
||||
.getCharacteristic(Characteristic.On)
|
||||
.on('get', function(callback) { that.getLan("power", callback);})
|
||||
.on('set', function(value, callback) {that.setLanPower(value, callback);});
|
||||
|
||||
service
|
||||
.addCharacteristic(Characteristic.Brightness)
|
||||
.on('get', function(callback) { that.getLan("brightness", callback);})
|
||||
.on('set', function(value, callback) { that.setLanColor("brightness", value, callback);});
|
||||
|
||||
if (this.capabilities.has_color == true) {
|
||||
service
|
||||
.addCharacteristic(Characteristic.Hue)
|
||||
.on('get', function(callback) { that.getLan("hue", callback);})
|
||||
.on('set', function(value, callback) { that.setLanColor("hue", value, callback);});
|
||||
|
||||
service
|
||||
.addCharacteristic(Characteristic.Saturation)
|
||||
.on('get', function(callback) { that.getLan("saturation", callback);})
|
||||
.on('set', function(value, callback) { that.setLanColor("saturation", value, callback);});
|
||||
}
|
||||
break;
|
||||
case "get":
|
||||
// gets over the lan api, sets over the remote api
|
||||
service
|
||||
.getCharacteristic(Characteristic.On)
|
||||
.on('get', function(callback) { that.getLan("power", callback);})
|
||||
.on('set', function(value, callback) {that.setRemotePower(value, callback);});
|
||||
|
||||
service
|
||||
.addCharacteristic(Characteristic.Brightness)
|
||||
.on('get', function(callback) { that.getLan("brightness", callback);})
|
||||
.on('set', function(value, callback) { that.setRemoteColor("brightness", value, callback);});
|
||||
|
||||
if (this.capabilities.has_color == true) {
|
||||
service
|
||||
.addCharacteristic(Characteristic.Hue)
|
||||
.on('get', function(callback) { that.getLan("hue", callback);})
|
||||
.on('set', function(value, callback) { that.setRemoteColor("hue", value, callback);});
|
||||
|
||||
service
|
||||
.addCharacteristic(Characteristic.Saturation)
|
||||
.on('get', function(callback) { that.getLan("saturation", callback);})
|
||||
.on('set', function(value, callback) { that.setRemoteColor("saturation", value, callback);});
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// gets and sets over the remote api
|
||||
service
|
||||
.getCharacteristic(Characteristic.On)
|
||||
.on('get', function(callback) { that.getRemote("power", callback);})
|
||||
.on('set', function(value, callback) {that.setRemotePower(value, callback);});
|
||||
|
||||
service
|
||||
.addCharacteristic(Characteristic.Brightness)
|
||||
.on('get', function(callback) { that.getRemote("brightness", callback);})
|
||||
.on('set', function(value, callback) { that.setRemoteColor("brightness", value, callback);});
|
||||
|
||||
if (this.capabilities.has_color == true) {
|
||||
service
|
||||
.addCharacteristic(Characteristic.Hue)
|
||||
.on('get', function(callback) { that.getRemote("hue", callback);})
|
||||
.on('set', function(value, callback) { that.setRemoteColor("hue", value, callback);});
|
||||
|
||||
service
|
||||
.addCharacteristic(Characteristic.Saturation)
|
||||
.on('get', function(callback) { that.getRemote("saturation", callback);})
|
||||
.on('set', function(value, callback) { that.setRemoteColor("saturation", value, callback);});
|
||||
}
|
||||
}
|
||||
|
||||
services.push(service);
|
||||
|
||||
service = new Service.AccessoryInformation();
|
||||
|
||||
service
|
||||
.setCharacteristic(Characteristic.Manufacturer, "LIFX")
|
||||
.setCharacteristic(Characteristic.Model, this.model)
|
||||
.setCharacteristic(Characteristic.SerialNumber, this.serial);
|
||||
|
||||
services.push(service);
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.accessory = LIFxBulbAccessory;
|
||||
module.exports.platform = LIFxPlatform;
|
||||
267
platforms/LogitechHarmony.js
Normal file
267
platforms/LogitechHarmony.js
Normal file
@@ -0,0 +1,267 @@
|
||||
'use strict';
|
||||
|
||||
// Logitech Harmony Remote Platform Shim for HomeBridge
|
||||
// Based on the Domoticz Platform Shim for HomeBridge by Joep Verhaeg (http://www.joepverhaeg.nl)
|
||||
// Wriiten by John Wells (https://github.com/madmod)
|
||||
//
|
||||
// Remember to add platform to config.json. Example:
|
||||
// "platforms": [
|
||||
// {
|
||||
// "platform": "LogitechHarmony",
|
||||
// "name": "Logitech Harmony"
|
||||
// }
|
||||
// ],
|
||||
//
|
||||
// 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 harmonyDiscover = require('harmonyhubjs-discover');
|
||||
var harmony = require('harmonyhubjs-client');
|
||||
|
||||
var _harmonyHubPort = 61991;
|
||||
|
||||
var Service = require("hap-nodejs").Service;
|
||||
var Characteristic = require("hap-nodejs").Characteristic;
|
||||
var Accessory = require("hap-nodejs").Accessory;
|
||||
var uuid = require("hap-nodejs").uuid;
|
||||
var inherits = require('util').inherits;
|
||||
var queue = require('queue');
|
||||
|
||||
|
||||
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));
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
function LogitechHarmonyPlatform (log, config) {
|
||||
this.log = log;
|
||||
this.ip_address = config['ip_address'];
|
||||
};
|
||||
|
||||
|
||||
LogitechHarmonyPlatform.prototype = {
|
||||
|
||||
accessories: function (callback) {
|
||||
var plat = this;
|
||||
var foundAccessories = [];
|
||||
var activityAccessories = [];
|
||||
var hub = null;
|
||||
var hubIP = null;
|
||||
var hubQueue = queue();
|
||||
hubQueue.concurrency = 1;
|
||||
|
||||
// Get the first hub
|
||||
locateHub(function (err, client, clientIP) {
|
||||
if (err) throw err;
|
||||
|
||||
plat.log("Fetching Logitech Harmony devices and activites...");
|
||||
|
||||
hub = client;
|
||||
hubIP = clientIP;
|
||||
//getDevices(hub);
|
||||
getActivities();
|
||||
});
|
||||
|
||||
// Find one Harmony remote hub (only support one for now)
|
||||
function locateHub(callback) {
|
||||
// Use the ip address in configuration if available
|
||||
if (plat.ip_address) {
|
||||
console.log("Using Logitech Harmony hub ip address from configuration");
|
||||
|
||||
return createClient(plat.ip_address, callback)
|
||||
}
|
||||
|
||||
plat.log("Searching for Logitech Harmony remote hubs...");
|
||||
|
||||
// Discover the harmony hub with bonjour
|
||||
var discover = new harmonyDiscover(_harmonyHubPort);
|
||||
|
||||
// TODO: Support update event with some way to add accessories
|
||||
// TODO: Have some kind of timeout with an error message. Right now this searches forever until it finds one hub.
|
||||
discover.on('online', function (hubInfo) {
|
||||
plat.log("Found Logitech Harmony remote hub: " + hubInfo.ip);
|
||||
|
||||
// Stop looking for hubs once we find the first one
|
||||
// TODO: Support multiple hubs
|
||||
discover.stop();
|
||||
|
||||
createClient(hubInfo.ip, callback);
|
||||
});
|
||||
|
||||
// Start looking for hubs
|
||||
discover.start();
|
||||
}
|
||||
|
||||
// Connect to a Harmony hub
|
||||
function createClient(ipAddress, callback) {
|
||||
plat.log("Connecting to Logitech Harmony remote hub...");
|
||||
harmony(ipAddress)
|
||||
.then(function (client) {
|
||||
plat.log("Connected to Logitech Harmony remote hub");
|
||||
callback(null, client, ipAddress);
|
||||
});
|
||||
}
|
||||
|
||||
// Get Harmony Activities
|
||||
function getActivities() {
|
||||
plat.log("Fetching Logitech Harmony activities...");
|
||||
|
||||
hub.getActivities()
|
||||
.then(function (activities) {
|
||||
plat.log("Found activities: \n" + activities.map(function (a) { return "\t" + a.label; }).join("\n"));
|
||||
|
||||
hub.getCurrentActivity().then(function (currentActivity) {
|
||||
var actAccessories = [];
|
||||
var sArray = sortByKey(activities, "label");
|
||||
sArray.map(function(s) {
|
||||
var accessory = createActivityAccessory(s);
|
||||
if (accessory.id > 0) {
|
||||
accessory.updateActivityState(currentActivity);
|
||||
actAccessories.push(accessory);
|
||||
foundAccessories.push(accessory);
|
||||
}
|
||||
});
|
||||
activityAccessories = actAccessories;
|
||||
keepAliveRefreshLoop();
|
||||
callback(foundAccessories);
|
||||
}).catch(function (err) {
|
||||
plat.log('Unable to get current activity with error', err);
|
||||
throw err;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createActivityAccessory(activity) {
|
||||
var accessory = new LogitechHarmonyActivityAccessory(plat.log, activity, changeCurrentActivity.bind(plat), -1);
|
||||
return accessory;
|
||||
}
|
||||
|
||||
var isChangingActivity = false;
|
||||
function changeCurrentActivity(nextActivity, callback) {
|
||||
if (!nextActivity) {
|
||||
nextActivity = -1;
|
||||
}
|
||||
plat.log('Queue activity to ' + nextActivity);
|
||||
executeOnHub(function(h, cb) {
|
||||
plat.log('Set activity to ' + nextActivity);
|
||||
h.startActivity(nextActivity)
|
||||
.then(function () {
|
||||
cb();
|
||||
isChangingActivity = false;
|
||||
plat.log('Finished setting activity to ' + nextActivity);
|
||||
updateCurrentActivity(nextActivity);
|
||||
if (callback) callback(null, nextActivity);
|
||||
})
|
||||
.catch(function (err) {
|
||||
cb();
|
||||
isChangingActivity = false;
|
||||
plat.log('Failed setting activity to ' + nextActivity + ' with error ' + err);
|
||||
if (callback) callback(err);
|
||||
});
|
||||
}, function(){
|
||||
callback(Error("Set activity failed too many times"));
|
||||
});
|
||||
}
|
||||
|
||||
function updateCurrentActivity(currentActivity) {
|
||||
var actAccessories = activityAccessories;
|
||||
if (actAccessories instanceof Array) {
|
||||
actAccessories.map(function(a) { a.updateActivityState(currentActivity); });
|
||||
}
|
||||
}
|
||||
|
||||
// prevent connection from closing
|
||||
function keepAliveRefreshLoop() {
|
||||
setTimeout(function() {
|
||||
setInterval(function() {
|
||||
executeOnHub(function(h, cb) {
|
||||
plat.log("Refresh Status");
|
||||
h.getCurrentActivity()
|
||||
.then(function(currentActivity){
|
||||
cb();
|
||||
updateCurrentActivity(currentActivity);
|
||||
})
|
||||
.catch(cb);
|
||||
});
|
||||
}, 20000);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function executeOnHub(func, funcMaxTimeout)
|
||||
{
|
||||
if (!func) return;
|
||||
hubQueue.push(function(cb) {
|
||||
var tout = setTimeout(function(){
|
||||
plat.log("Reconnecting to Hub " + hubIP);
|
||||
createClient(hubIP, function(err, newHub){
|
||||
if (err) throw err;
|
||||
hub = newHub;
|
||||
if (funcMaxTimeout) {
|
||||
funcMaxTimeout();
|
||||
}
|
||||
cb();
|
||||
});
|
||||
}, 30000);
|
||||
func(hub, function(){
|
||||
clearTimeout(tout);
|
||||
cb();
|
||||
});
|
||||
});
|
||||
if (!hubQueue.running){
|
||||
hubQueue.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function LogitechHarmonyActivityAccessory (log, details, changeCurrentActivity) {
|
||||
this.log = log;
|
||||
this.id = details.id;
|
||||
this.name = details.label;
|
||||
this.isOn = false;
|
||||
this.changeCurrentActivity = changeCurrentActivity;
|
||||
Accessory.call(this, this.name, uuid.generate(this.id));
|
||||
var self = this;
|
||||
|
||||
this.getService(Service.AccessoryInformation)
|
||||
.setCharacteristic(Characteristic.Manufacturer, "Logitech")
|
||||
.setCharacteristic(Characteristic.Model, "Harmony")
|
||||
// TODO: Add hub unique id to this for people with multiple hubs so that it is really a guid.
|
||||
.setCharacteristic(Characteristic.SerialNumber, this.id);
|
||||
|
||||
this.addService(Service.Switch)
|
||||
.getCharacteristic(Characteristic.On)
|
||||
.on('get', function(callback) {
|
||||
// Refreshed automatically by platform
|
||||
callback(null, self.isOn);
|
||||
})
|
||||
.on('set', this.setPowerState.bind(this));
|
||||
|
||||
}
|
||||
inherits(LogitechHarmonyActivityAccessory, Accessory);
|
||||
LogitechHarmonyActivityAccessory.prototype.parent = Accessory.prototype;
|
||||
LogitechHarmonyActivityAccessory.prototype.getServices = function() {
|
||||
return this.services;
|
||||
};
|
||||
|
||||
LogitechHarmonyActivityAccessory.prototype.updateActivityState = function (currentActivity) {
|
||||
this.isOn = (currentActivity === this.id);
|
||||
// Force get to trigger 'change' if needed
|
||||
this.getService(Service.Switch)
|
||||
.getCharacteristic(Characteristic.On)
|
||||
.getValue();
|
||||
};
|
||||
|
||||
LogitechHarmonyActivityAccessory.prototype.setPowerState = function (state, callback) {
|
||||
this.changeCurrentActivity(state ? this.id : null, callback);
|
||||
};
|
||||
|
||||
module.exports.platform = LogitechHarmonyPlatform;
|
||||
|
||||
242
platforms/MiLight.js
Normal file
242
platforms/MiLight.js
Normal file
@@ -0,0 +1,242 @@
|
||||
/*
|
||||
|
||||
MiLight platform shim for Homebridge
|
||||
Written by Sam Edwards (https://samedwards.ca/)
|
||||
|
||||
Uses the node-milight-promise library (https://github.com/mwittig/node-milight-promise) which features some code from
|
||||
applamp.nl (http://www.applamp.nl/service/applamp-api/) and uses other details from (http://www.limitlessled.com/dev/)
|
||||
|
||||
Configure in config.json as follows:
|
||||
|
||||
"platforms": [
|
||||
{
|
||||
"platform":"MiLight",
|
||||
"name":"MiLight",
|
||||
"ip_address": "255.255.255.255",
|
||||
"port": 8899,
|
||||
"type": "rgbw",
|
||||
"delay": 30,
|
||||
"repeat": 3,
|
||||
"zones":["Kitchen Lamp","Bedroom Lamp","Living Room Lamp","Hallway Lamp"]
|
||||
}
|
||||
]
|
||||
|
||||
Where the parameters are:
|
||||
*platform (required): This must be "MiLight", and refers to the name of the accessory as exported from this file
|
||||
*name (optional): The display name used for logging output by Homebridge. Best to set to "MiLight"
|
||||
*ip_address (optional): The IP address of the WiFi Bridge. Default to the broadcast address of 255.255.255.255 if not specified
|
||||
*port (optional): Port of the WiFi bridge. Defaults to 8899 if not specified
|
||||
*type (optional): One of either "rgbw", "rgb", or "white", depending on the type of bulb being controlled. This applies to all zones. Defaults to rgbw.
|
||||
*delay (optional): Delay between commands sent over UDP. Default 30ms. May cause delays when sending a lot of commands. Try decreasing to improve.
|
||||
*repeat (optional): Number of times to repeat the UDP command for better reliability. Default 3
|
||||
*zones (required): An array of the names of the zones, in order, 1-4. Use null if a zone is skipped. RGB lamps can only have a single zone.
|
||||
|
||||
Tips and Tricks:
|
||||
*Setting the brightness of an rgbw or a white bulb will set it to "night mode", which is dimmer than the lowest brightness setting
|
||||
*White and rgb bulbs don't support absolute brightness setting, so we just send a brightness up/brightness down command depending
|
||||
if we got a percentage above/below 50% respectively
|
||||
*The only exception to the above is that white bulbs support a "maximum brightness" command, so we send that when we get 100%
|
||||
*Implemented warmer/cooler for white lamps in a similar way to brightnes, except this time above/below 180 degrees on the colour wheel
|
||||
*I welcome feedback on a better way to work the brightness/hue for white and rgb bulbs
|
||||
|
||||
Troubleshooting:
|
||||
The node-milight-promise library provides additional debugging output when the MILIGHT_DEBUG environmental variable is set
|
||||
|
||||
TODO:
|
||||
*Possibly build in some sort of state logging and persistance so that we can answswer HomeKit status queries to the best of our ability
|
||||
|
||||
*/
|
||||
|
||||
var Service = require("hap-nodejs").Service;
|
||||
var Characteristic = require("hap-nodejs").Characteristic;
|
||||
var Milight = require('node-milight-promise').MilightController;
|
||||
var commands = require('node-milight-promise').commands;
|
||||
|
||||
module.exports = {
|
||||
accessory: MiLightAccessory,
|
||||
platform: MiLightPlatform
|
||||
}
|
||||
|
||||
function MiLightPlatform(log, config) {
|
||||
this.log = log;
|
||||
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
MiLightPlatform.prototype = {
|
||||
accessories: function(callback) {
|
||||
var zones = [];
|
||||
|
||||
// Various error checking
|
||||
if (this.config.zones) {
|
||||
var zoneLength = this.config.zones.length;
|
||||
} else {
|
||||
this.log("ERROR: Could not read zones from configuration.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.config["type"]) {
|
||||
this.log("INFO: Type not specified, defaulting to rgbw");
|
||||
this.config["type"] = "rgbw";
|
||||
}
|
||||
|
||||
if (zoneLength == 0) {
|
||||
this.log("ERROR: No zones found in configuration.");
|
||||
return;
|
||||
} else if (this.config["type"] == "rgb" && zoneLength > 1) {
|
||||
this.log("WARNING: RGB lamps only have a single zone. Only the first defined zone will be used.");
|
||||
zoneLength = 1;
|
||||
} else if (zoneLength > 4) {
|
||||
this.log("WARNING: Only a maximum of 4 zones are supported per bridge. Only recognizing the first 4 zones.");
|
||||
zoneLength = 4;
|
||||
}
|
||||
|
||||
// Create lamp accessories for all of the defined zones
|
||||
for (var i=0; i < zoneLength; i++) {
|
||||
if (!!this.config.zones[i]) {
|
||||
this.config["name"] = this.config.zones[i];
|
||||
this.config["zone"] = i+1;
|
||||
lamp = new MiLightAccessory(this.log, this.config);
|
||||
zones.push(lamp);
|
||||
}
|
||||
}
|
||||
if (zones.length > 0) {
|
||||
callback(zones);
|
||||
} else {
|
||||
this.log("ERROR: Unable to find any valid zones");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function MiLightAccessory(log, config) {
|
||||
this.log = log;
|
||||
|
||||
// config info
|
||||
this.ip_address = config["ip_address"];
|
||||
this.port = config["port"];
|
||||
this.name = config["name"];
|
||||
this.zone = config["zone"];
|
||||
this.type = config["type"];
|
||||
this.delay = config["delay"];
|
||||
this.repeat = config["repeat"];
|
||||
|
||||
this.light = new Milight({
|
||||
ip: this.ip_address,
|
||||
port: this.port,
|
||||
delayBetweenCommands: this.delay,
|
||||
commandRepeat: this.repeat
|
||||
});
|
||||
|
||||
}
|
||||
MiLightAccessory.prototype = {
|
||||
|
||||
setPowerState: function(powerOn, callback) {
|
||||
if (powerOn) {
|
||||
this.log("["+this.name+"] Setting power state to on");
|
||||
this.light.sendCommands(commands[this.type].on(this.zone));
|
||||
} else {
|
||||
this.log("["+this.name+"] Setting power state to off");
|
||||
this.light.sendCommands(commands[this.type].off(this.zone));
|
||||
}
|
||||
callback();
|
||||
},
|
||||
|
||||
setBrightness: function(level, callback) {
|
||||
if (level == 0) {
|
||||
// If brightness is set to 0, turn off the lamp
|
||||
this.log("["+this.name+"] Setting brightness to 0 (off)");
|
||||
this.light.sendCommands(commands[this.type].off(this.zone));
|
||||
} else if (level <= 2 && (this.type == "rgbw" || this.type == "white")) {
|
||||
// If setting brightness to 2 or lower, instead set night mode for lamps that support it
|
||||
this.log("["+this.name+"] Setting night mode", level);
|
||||
|
||||
this.light.sendCommands(commands[this.type].off(this.zone));
|
||||
// Ensure we're pausing for 100ms between these commands as per the spec
|
||||
this.light.pause(100);
|
||||
this.light.sendCommands(commands[this.type].nightMode(this.zone));
|
||||
|
||||
} else {
|
||||
this.log("["+this.name+"] Setting brightness to %s", level);
|
||||
|
||||
// Send on command to ensure we're addressing the right bulb
|
||||
this.light.sendCommands(commands[this.type].on(this.zone));
|
||||
|
||||
// If this is an rgbw lamp, set the absolute brightness specified
|
||||
if (this.type == "rgbw") {
|
||||
this.light.sendCommands(commands.rgbw.brightness(level));
|
||||
} else {
|
||||
// If this is an rgb or a white lamp, they only support brightness up and down.
|
||||
// Set brightness up when value is >50 and down otherwise. Not sure how well this works real-world.
|
||||
if (level >= 50) {
|
||||
if (this.type == "white" && level == 100) {
|
||||
// But the white lamps do have a "maximum brightness" command
|
||||
this.light.sendCommands(commands.white.maxBright(this.zone));
|
||||
} else {
|
||||
this.light.sendCommands(commands[this.type].brightUp());
|
||||
}
|
||||
} else {
|
||||
this.light.sendCommands(commands[this.type].brightDown());
|
||||
}
|
||||
}
|
||||
}
|
||||
callback();
|
||||
},
|
||||
|
||||
setHue: function(value, callback) {
|
||||
this.log("["+this.name+"] Setting hue to %s", value);
|
||||
|
||||
var hue = Array(value, 0, 0);
|
||||
|
||||
// Send on command to ensure we're addressing the right bulb
|
||||
this.light.sendCommands(commands[this.type].on(this.zone));
|
||||
|
||||
if (this.type == "rgbw") {
|
||||
if (value == 0) {
|
||||
this.light.sendCommands(commands.rgbw.whiteMode(this.zone));
|
||||
} else {
|
||||
this.light.sendCommands(commands.rgbw.hue(commands.rgbw.hsvToMilightColor(hue)));
|
||||
}
|
||||
} else if (this.type == "rgb") {
|
||||
this.light.sendCommands(commands.rgb.hue(commands.rgbw.hsvToMilightColor(hue)));
|
||||
} else if (this.type == "white") {
|
||||
// Again, white lamps don't support setting an absolue colour temp, so trying to do warmer/cooler step at a time based on colour
|
||||
if (value >= 180) {
|
||||
this.light.sendCommands(commands.white.cooler());
|
||||
} else {
|
||||
this.light.sendCommands(commands.white.warmer());
|
||||
}
|
||||
}
|
||||
callback();
|
||||
},
|
||||
|
||||
identify: function(callback) {
|
||||
this.log("["+this.name+"] Identify requested!");
|
||||
callback(); // success
|
||||
},
|
||||
|
||||
getServices: function() {
|
||||
var informationService = new Service.AccessoryInformation();
|
||||
|
||||
informationService
|
||||
.setCharacteristic(Characteristic.Manufacturer, "MiLight")
|
||||
.setCharacteristic(Characteristic.Model, this.type)
|
||||
.setCharacteristic(Characteristic.SerialNumber, "MILIGHT12345");
|
||||
|
||||
var lightbulbService = new Service.Lightbulb();
|
||||
|
||||
lightbulbService
|
||||
.getCharacteristic(Characteristic.On)
|
||||
.on('set', this.setPowerState.bind(this));
|
||||
|
||||
lightbulbService
|
||||
.addCharacteristic(new Characteristic.Brightness())
|
||||
.on('set', this.setBrightness.bind(this));
|
||||
|
||||
lightbulbService
|
||||
.addCharacteristic(new Characteristic.Hue())
|
||||
.on('set', this.setHue.bind(this));
|
||||
|
||||
return [informationService, lightbulbService];
|
||||
}
|
||||
};
|
||||
397
platforms/Nest.js
Normal file
397
platforms/Nest.js
Normal file
@@ -0,0 +1,397 @@
|
||||
var types = require("hap-nodejs/accessories/types.js");
|
||||
var nest = require('unofficial-nest-api');
|
||||
|
||||
function NestPlatform(log, config){
|
||||
|
||||
// auth info
|
||||
this.username = config["username"];
|
||||
this.password = config["password"];
|
||||
|
||||
this.log = log;
|
||||
}
|
||||
|
||||
NestPlatform.prototype = {
|
||||
accessories: function(callback) {
|
||||
this.log("Fetching Nest devices.");
|
||||
|
||||
var that = this;
|
||||
var foundAccessories = [];
|
||||
|
||||
nest.login(this.username, this.password, function (err, data) {
|
||||
if (err) {
|
||||
that.log("There was a problem authenticating with Nest.");
|
||||
}
|
||||
else {
|
||||
nest.fetchStatus(function (data) {
|
||||
for (var deviceId in data.device) {
|
||||
if (data.device.hasOwnProperty(deviceId)) {
|
||||
var device = data.device[deviceId];
|
||||
// it's a thermostat, adjust this to detect other accessories
|
||||
if (data.shared[deviceId].hasOwnProperty('current_temperature'))
|
||||
{
|
||||
var name = data.shared[deviceId].name
|
||||
var accessory = new NestThermostatAccessory(that.log, name, device, deviceId);
|
||||
foundAccessories.push(accessory);
|
||||
}
|
||||
}
|
||||
}
|
||||
callback(foundAccessories)
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function NestThermostatAccessory(log, name, device, deviceId) {
|
||||
// device info
|
||||
if (name) {
|
||||
this.name = name;
|
||||
} else {
|
||||
this.name = "Nest" + device.serial_number;
|
||||
}
|
||||
this.model = device.model_version;
|
||||
this.serial = device.serial_number;
|
||||
this.deviceId = deviceId;
|
||||
this.log = log;
|
||||
}
|
||||
|
||||
NestThermostatAccessory.prototype = {
|
||||
getCurrentHeatingCooling: function(callback){
|
||||
|
||||
var that = this;
|
||||
|
||||
this.log("Checking current heating cooling for: " + this.name);
|
||||
nest.fetchStatus(function (data) {
|
||||
var device = data.device[that.deviceId];
|
||||
|
||||
var currentHeatingCooling = 0;
|
||||
switch(device.current_schedule_mode) {
|
||||
case "OFF":
|
||||
targetHeatingCooling = 0;
|
||||
break;
|
||||
case "HEAT":
|
||||
currentHeatingCooling = 1;
|
||||
break;
|
||||
case "COOL":
|
||||
currentHeatingCooling = 2;
|
||||
break;
|
||||
case "RANGE":
|
||||
currentHeatingCooling = 3;
|
||||
break;
|
||||
default:
|
||||
currentHeatingCooling = 0;
|
||||
}
|
||||
that.log("Current heating for " + this.name + "is: " + currentHeatingCooling);
|
||||
callback(currentHeatingCooling);
|
||||
});
|
||||
|
||||
|
||||
},
|
||||
|
||||
getTargetHeatingCoooling: function(callback){
|
||||
|
||||
var that = this;
|
||||
|
||||
this.log("Checking target heating cooling for: " + this.name);
|
||||
nest.fetchStatus(function (data) {
|
||||
var device = data.device[that.deviceId];
|
||||
|
||||
var targetHeatingCooling = 0;
|
||||
switch(device.target_temperature_type) {
|
||||
case "off":
|
||||
targetHeatingCooling = 0;
|
||||
break;
|
||||
case "heat":
|
||||
targetHeatingCooling = 1;
|
||||
break;
|
||||
case "cool":
|
||||
targetHeatingCooling = 2;
|
||||
break;
|
||||
case "range":
|
||||
targetHeatingCooling = 3;
|
||||
break;
|
||||
default:
|
||||
targetHeatingCooling = 0;
|
||||
}
|
||||
that.log("Current target heating for " + this.name + " is: " + targetHeatingCooling);
|
||||
callback(targetHeatingCooling);
|
||||
});
|
||||
},
|
||||
|
||||
getCurrentTemperature: function(callback){
|
||||
|
||||
var that = this;
|
||||
|
||||
nest.fetchStatus(function (data) {
|
||||
var device = data.shared[that.deviceId];
|
||||
that.log("Current temperature for " + this.name + " is: " + device.current_temperature);
|
||||
callback(device.current_temperature);
|
||||
});
|
||||
|
||||
|
||||
},
|
||||
|
||||
getTargetTemperature: function(callback){
|
||||
|
||||
var that = this;
|
||||
|
||||
nest.fetchStatus(function (data) {
|
||||
var device = data.shared[that.deviceId];
|
||||
that.log("Target temperature for " + this.name + " is: " + device.target_temperature);
|
||||
callback(device.target_temperature);
|
||||
});
|
||||
|
||||
|
||||
},
|
||||
|
||||
getTemperatureUnits: function(callback){
|
||||
|
||||
var that = this;
|
||||
|
||||
nest.fetchStatus(function (data) {
|
||||
var device = data.device[that.deviceId];
|
||||
var temperatureUnits = 0;
|
||||
switch(device.temperature_scale) {
|
||||
case "F":
|
||||
that.log("Tempature unit for " + this.name + " is: " + "Fahrenheit");
|
||||
temperatureUnits = 1;
|
||||
break;
|
||||
case "C":
|
||||
that.log("Tempature unit for " + this.name + " is: " + "Celsius");
|
||||
temperatureUnits = 0;
|
||||
break;
|
||||
default:
|
||||
temperatureUnits = 0;
|
||||
}
|
||||
|
||||
callback(temperatureUnits);
|
||||
});
|
||||
|
||||
|
||||
},
|
||||
|
||||
getCurrentRelativeHumidity: function(callback){
|
||||
|
||||
var that = this;
|
||||
|
||||
nest.fetchStatus(function (data) {
|
||||
var device = data.device[that.deviceId];
|
||||
that.log("Humidity for " + this.name + " is: " + device.current_humidity);
|
||||
callback(device.current_humidity);
|
||||
})
|
||||
|
||||
|
||||
},
|
||||
|
||||
setTargetHeatingCooling: function(targetHeatingCooling){
|
||||
|
||||
var that = this;
|
||||
|
||||
var targetTemperatureType = 'off';
|
||||
switch(targetHeatingCooling) {
|
||||
case 0:
|
||||
targetTemperatureType = 'off';
|
||||
break;
|
||||
case 1:
|
||||
targetTemperatureType = 'heat';
|
||||
break;
|
||||
case 2:
|
||||
targetTemperatureType = 'cool';
|
||||
break;
|
||||
case 3:
|
||||
targetTemperatureType = 'range';
|
||||
break;
|
||||
default:
|
||||
targetTemperatureType = 'off';
|
||||
}
|
||||
|
||||
this.log("Setting target heating cooling for " + this.name + " to: " + targetTemperatureType);
|
||||
nest.setTargetTemperatureType(this.deviceId, targetTemperatureType);
|
||||
|
||||
|
||||
},
|
||||
|
||||
setTargetTemperature: function(targetTemperature){
|
||||
|
||||
var that = this;
|
||||
|
||||
this.log("Setting target temperature for " + this.name + " to: " + targetTemperature);
|
||||
nest.setTemperature(this.deviceId, targetTemperature);
|
||||
|
||||
|
||||
},
|
||||
|
||||
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: "Nest",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Manufacturer",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MODEL_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: this.model,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Model",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.SERIAL_NUMBER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: this.serial,
|
||||
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.THERMOSTAT_STYPE,
|
||||
characteristics: [{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: this.name,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of thermostat",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.CURRENTHEATINGCOOLING_CTYPE,
|
||||
onUpdate: null,
|
||||
onRead: function(callback) {
|
||||
that.getCurrentHeatingCooling(function(currentHeatingCooling){
|
||||
callback(currentHeatingCooling);
|
||||
});
|
||||
},
|
||||
perms: ["pr","ev"],
|
||||
format: "int",
|
||||
initialValue: 0,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Current Mode",
|
||||
designedMaxLength: 1,
|
||||
designedMinValue: 0,
|
||||
designedMaxValue: 2,
|
||||
designedMinStep: 1,
|
||||
},{
|
||||
cType: types.TARGETHEATINGCOOLING_CTYPE,
|
||||
onUpdate: function(value) {
|
||||
that.setTargetHeatingCooling(value);
|
||||
},
|
||||
onRead: function(callback) {
|
||||
that.getTargetHeatingCoooling(function(targetHeatingCooling){
|
||||
callback(targetHeatingCooling);
|
||||
});
|
||||
},
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "int",
|
||||
initialValue: 0,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Target Mode",
|
||||
designedMinValue: 0,
|
||||
designedMaxValue: 3,
|
||||
designedMinStep: 1,
|
||||
},{
|
||||
cType: types.CURRENT_TEMPERATURE_CTYPE,
|
||||
onUpdate: null,
|
||||
onRead: function(callback) {
|
||||
that.getCurrentTemperature(function(currentTemperature){
|
||||
callback(currentTemperature);
|
||||
});
|
||||
},
|
||||
perms: ["pr","ev"],
|
||||
format: "int",
|
||||
initialValue: 20,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Current Temperature",
|
||||
unit: "celsius"
|
||||
},{
|
||||
cType: types.TARGET_TEMPERATURE_CTYPE,
|
||||
onUpdate: function(value) {
|
||||
that.setTargetTemperature(value);
|
||||
},
|
||||
onRead: function(callback) {
|
||||
that.getTargetTemperature(function(targetTemperature){
|
||||
callback(targetTemperature);
|
||||
});
|
||||
},
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "int",
|
||||
initialValue: 20,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Target Temperature",
|
||||
designedMinValue: 16,
|
||||
designedMaxValue: 38,
|
||||
designedMinStep: 1,
|
||||
unit: "celsius"
|
||||
},{
|
||||
cType: types.TEMPERATURE_UNITS_CTYPE,
|
||||
onUpdate: null,
|
||||
onRead: function(callback) {
|
||||
that.getTemperatureUnits(function(temperatureUnits){
|
||||
callback(temperatureUnits);
|
||||
});
|
||||
},
|
||||
perms: ["pr","ev"],
|
||||
format: "int",
|
||||
initialValue: 0,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Unit",
|
||||
},{
|
||||
cType: types.CURRENT_RELATIVE_HUMIDITY_CTYPE,
|
||||
onUpdate: null,
|
||||
onRead: function(callback) {
|
||||
that.getCurrentRelativeHumidity(function(currentRelativeHumidity){
|
||||
callback(currentRelativeHumidity);
|
||||
});
|
||||
},
|
||||
perms: ["pr","ev"],
|
||||
format: "int",
|
||||
initialValue: 0,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Humidity",
|
||||
}]
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.accessory = NestThermostatAccessory;
|
||||
module.exports.platform = NestPlatform;
|
||||
347
platforms/Openhab.js
Normal file
347
platforms/Openhab.js
Normal file
@@ -0,0 +1,347 @@
|
||||
// OpenHAB Platform Shim for HomeBridge
|
||||
// Written by Tommaso Marchionni
|
||||
// Based on many of the other HomeBridge platform modules
|
||||
//
|
||||
// Revisions:
|
||||
//
|
||||
// 17 October 2015 [tommasomarchionni]
|
||||
// - Initial release
|
||||
//
|
||||
// Remember to add platform to config.json. Example:
|
||||
// "platforms": [
|
||||
// {
|
||||
// "platform": "Openhab",
|
||||
// "name": "Openhab",
|
||||
// "server": "127.0.0.1",
|
||||
// "port": "8080",
|
||||
// "sitemap": "demo"
|
||||
// }
|
||||
// ],
|
||||
//
|
||||
// Example of sitemap in OpenHAB:
|
||||
// sitemap homekit label="HomeKit" {
|
||||
// Switch item=Light_1 label="Light 1"
|
||||
// }
|
||||
//
|
||||
// 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");
|
||||
var Service = require("hap-nodejs/lib/Service.js").Service;
|
||||
var Characteristic = require("hap-nodejs").Characteristic;
|
||||
|
||||
function OpenhabPlatform(log, config){
|
||||
this.log = log;
|
||||
this.user = config["user"];
|
||||
this.password = config["password"];
|
||||
this.server = config["server"];
|
||||
this.port = config["port"];
|
||||
|
||||
this.protocol = "http";
|
||||
|
||||
this.sitemap = "demo";
|
||||
if (typeof config["sitemap"] != 'undefined') {
|
||||
this.sitemap = config["sitemap"];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
OpenhabPlatform.prototype = {
|
||||
|
||||
sitemapUrl: function() {
|
||||
var serverString = this.server;
|
||||
//TODO da verificare
|
||||
if (this.user && this.password) {
|
||||
serverString = this.user + ":" + this.password + "@" + serverString;
|
||||
}
|
||||
|
||||
return this.protocol + "://" + serverString + ":" + this.port + "/rest/sitemaps/" + this.sitemap + "?type=json";
|
||||
},
|
||||
|
||||
parseSitemap: function(sitemap) {
|
||||
var widgets = [].concat(sitemap.homepage.widget);
|
||||
var result = [];
|
||||
for (var i = 0; i < widgets.length; i++) {
|
||||
var widget = widgets[i];
|
||||
if (!widget.item) {
|
||||
//TODO to handle frame
|
||||
this.log("WARN: The widget '" + widget.label + "' does not reference an item.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (widget.item.type=="SwitchItem" || widget.item.type=="DimmerItem" || widget.item.type == "RollershutterItem"){
|
||||
accessory = new OpenhabAccessory(this.log,this,widget.widgetId,widget.label,widget.item)
|
||||
this.log("Accessory Found: " + widget.label);
|
||||
result.push(accessory);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
accessories: function(callback) {
|
||||
this.log("Fetching OpenHAB devices.");
|
||||
var that = this;
|
||||
|
||||
url = that.sitemapUrl();
|
||||
this.log("Connecting to " + url);
|
||||
request.get({
|
||||
url: url,
|
||||
json: true
|
||||
}, function(err, response, json) {
|
||||
if (!err && response.statusCode == 200) {
|
||||
callback(that.parseSitemap(json));
|
||||
} else {
|
||||
that.log("There was a problem connecting to OpenHAB.");
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function OpenhabAccessory(log, platform, widgetId, label, detail) {
|
||||
this.log = log;
|
||||
this.platform = platform;
|
||||
this.idx = widgetId;
|
||||
this.name = label;
|
||||
this.label = label;
|
||||
this.type = detail.type;
|
||||
this.deviceURL = detail.link;
|
||||
this.addressStr = "n/a";
|
||||
this.state = detail.state;
|
||||
|
||||
if (this.type == "DimmerItem") {
|
||||
this.typeSupportsOnOff = true;
|
||||
this.typeSupportsDim = true;
|
||||
}
|
||||
|
||||
if (this.type == "SwitchItem") {
|
||||
this.typeSupportsOnOff = true;
|
||||
}
|
||||
|
||||
if (this.type == "RollershutterItem") {
|
||||
this.typeSupportsWindowCovering = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
OpenhabAccessory.prototype = {
|
||||
|
||||
updateStatus: function(command) {
|
||||
var that = this;
|
||||
|
||||
var options = {
|
||||
url: this.deviceURL,
|
||||
method: 'POST',
|
||||
body: "" + command
|
||||
};
|
||||
if (this.auth) {
|
||||
options['auth'] = this.auth;
|
||||
}
|
||||
|
||||
that.log("eseguo post");
|
||||
|
||||
request(options, function(error, response, body) {
|
||||
if (error) {
|
||||
console.trace("Updating Device Status.");
|
||||
that.log(error);
|
||||
return error;
|
||||
}
|
||||
|
||||
that.log("updateStatus of " + that.name + ": " + command);
|
||||
|
||||
});
|
||||
},
|
||||
|
||||
getServiceType: function() {
|
||||
if (this.typeSupportsWindowCovering){
|
||||
return new Service.WindowCovering;
|
||||
} else if (this.typeSupportsDim) {
|
||||
return new Service.Lightbulb;
|
||||
} else if (this.typeSupportsOnOff) {
|
||||
return new Service.Switch;
|
||||
}
|
||||
},
|
||||
|
||||
updateStatus: function(command, callback) {
|
||||
var that = this;
|
||||
|
||||
var options = {
|
||||
url: this.deviceURL,
|
||||
method: 'POST',
|
||||
body: "" + command
|
||||
};
|
||||
if (this.auth) {
|
||||
options['auth'] = this.auth;
|
||||
}
|
||||
|
||||
request(options, function(error, response, body) {
|
||||
if (error) {
|
||||
//console.trace("Updating Device Status.");
|
||||
//that.log(error);
|
||||
//return error;
|
||||
callback(new Error(error));
|
||||
} else {
|
||||
that.log("updateStatus of " + that.name + ": " + command);
|
||||
callback(true);
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
setPowerState: function(powerOn, callback) {
|
||||
var that = this;
|
||||
|
||||
if (this.typeSupportsOnOff) {
|
||||
if (powerOn) {
|
||||
var command = "ON";
|
||||
} else {
|
||||
var command = "OFF";
|
||||
}
|
||||
|
||||
this.log("Setting power state on the '"+this.name+"' to " + command);
|
||||
this.updateStatus(command, function(noError){
|
||||
if (noError) {
|
||||
that.log("Successfully set '"+that.name+"' to " + command);
|
||||
callback();
|
||||
} else {
|
||||
callback(new Error('Can not communicate with OpenHAB.'));
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
}else{
|
||||
callback(new Error(this.name + " not supports ONOFF"));
|
||||
}
|
||||
},
|
||||
|
||||
getStatus: function(callback){
|
||||
var that = this;
|
||||
this.log("Fetching status brightness for: " + this.name);
|
||||
|
||||
var options = {
|
||||
url: this.deviceURL + '/state?type=json',
|
||||
method: 'GET'
|
||||
};
|
||||
|
||||
if (this.auth) {
|
||||
options['auth'] = this.auth;
|
||||
}
|
||||
|
||||
request(options, function(error, response, body) {
|
||||
if (error) {
|
||||
//console.trace("Requesting Device Status.");
|
||||
//that.log(error);
|
||||
//return error;
|
||||
callback(new Error('Can not communicate with Home Assistant.'));
|
||||
} else {
|
||||
that.log("getStatus of " + that.name + ": " + body);
|
||||
callback(null,body);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}.bind(this));
|
||||
|
||||
},
|
||||
|
||||
getCurrentPosition: function(callback){
|
||||
callback(100);
|
||||
},
|
||||
|
||||
getPositionState: function(callback){
|
||||
this.log("Fetching position state for: " + this.name);
|
||||
callback(Characteristic.PositionState.STOPPED);
|
||||
},
|
||||
|
||||
setTargetPosition: function(level, callback) {
|
||||
var that = this;
|
||||
|
||||
this.log("Setting target position on the '"+this.name+"' to " + level);
|
||||
|
||||
this.updateStatus(level, function(noError){
|
||||
if (noError) {
|
||||
that.log("Successfully set position on the '"+that.name+"' to " + level);
|
||||
callback();
|
||||
} else {
|
||||
callback(new Error('Can not communicate with OpenHAB.'));
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
},
|
||||
|
||||
setBrightness: function(level, callback) {
|
||||
var that = this;
|
||||
|
||||
if (this.typeSupportsDim && level >= 0 && level <= 100) {
|
||||
|
||||
this.log("Setting brightness on the '"+this.name+"' to " + level);
|
||||
|
||||
this.updateStatus(level, function(noError){
|
||||
if (noError) {
|
||||
that.log("Successfully set brightness on the '"+that.name+"' to " + level);
|
||||
callback();
|
||||
} else {
|
||||
callback(new Error('Can not communicate with OpenHAB.'));
|
||||
}
|
||||
}.bind(this));
|
||||
}
|
||||
},
|
||||
|
||||
getServices: function() {
|
||||
|
||||
var informationService = new Service.AccessoryInformation();
|
||||
|
||||
informationService
|
||||
.setCharacteristic(Characteristic.Manufacturer, "OpenHAB")
|
||||
.setCharacteristic(Characteristic.Model, this.type)
|
||||
.setCharacteristic(Characteristic.SerialNumber, "1234567890")
|
||||
.setCharacteristic(Characteristic.Name, this.label);
|
||||
|
||||
var otherService = this.getServiceType();
|
||||
|
||||
if (this.typeSupportsOnOff) {
|
||||
otherService
|
||||
.getCharacteristic(Characteristic.On)
|
||||
.on('get', this.getStatus.bind(this))
|
||||
.on('set', this.setPowerState.bind(this));
|
||||
|
||||
}
|
||||
|
||||
if (this.typeSupportsDim) {
|
||||
otherService
|
||||
.addCharacteristic(Characteristic.Brightness)
|
||||
.on('get', this.getStatus.bind(this))
|
||||
.on('set', this.setBrightness.bind(this));
|
||||
}
|
||||
|
||||
if (this.typeSupportsWindowCovering) {
|
||||
var currentPosition = 100;
|
||||
|
||||
otherService
|
||||
.getCharacteristic(Characteristic.CurrentPosition)
|
||||
.on('get', this.getCurrentPosition.bind(this))
|
||||
.setValue(currentPosition);
|
||||
|
||||
otherService
|
||||
.getCharacteristic(Characteristic.PositionState)
|
||||
.on('get', this.getPositionState.bind(this))
|
||||
.setValue(Characteristic.PositionState.STOPPED);
|
||||
|
||||
otherService
|
||||
.getCharacteristic(Characteristic.TargetPosition)
|
||||
.on('get', this.getCurrentPosition.bind(this))
|
||||
.on('set', this.setTargetPosition.bind(this));
|
||||
|
||||
}
|
||||
|
||||
console.log(informationService);
|
||||
|
||||
return [informationService, otherService];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports.accessory = OpenhabAccessory;
|
||||
module.exports.platform = OpenhabPlatform;
|
||||
@@ -33,7 +33,7 @@ var hue = require("node-hue-api"),
|
||||
HueApi = hue.HueApi,
|
||||
lightState = hue.lightState;
|
||||
|
||||
var types = require("HAP-NodeJS/accessories/types.js");
|
||||
var types = require("hap-nodejs/accessories/types.js");
|
||||
|
||||
function PhilipsHuePlatform(log, config) {
|
||||
this.log = log;
|
||||
@@ -167,7 +167,7 @@ PhilipsHueAccessory.prototype = {
|
||||
// Convert 0-65535 to 0-360
|
||||
hueToArcDegrees: function(value) {
|
||||
value = value/65535;
|
||||
value = value*100;
|
||||
value = value*360;
|
||||
value = Math.round(value);
|
||||
return value;
|
||||
},
|
||||
@@ -216,7 +216,13 @@ PhilipsHueAccessory.prototype = {
|
||||
that.log(device.name + ", characteristic: " + characteristic + ", value: " + value + ".");
|
||||
}
|
||||
else {
|
||||
that.log(err);
|
||||
if (err.code == "ECONNRESET") {
|
||||
setTimeout(function() {
|
||||
that.executeChange(api, device, characteristic, value);
|
||||
}, 300);
|
||||
} else {
|
||||
that.log(err);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// SmartThings JSON API SmartApp required
|
||||
// https://github.com/jnewland/SmartThings/blob/master/JSON.groovy
|
||||
//
|
||||
var types = require("HAP-NodeJS/accessories/types.js");
|
||||
var types = require("hap-nodejs/accessories/types.js");
|
||||
var request = require("request");
|
||||
|
||||
function SmartThingsPlatform(log, config){
|
||||
|
||||
188
platforms/Sonos.js
Normal file
188
platforms/Sonos.js
Normal file
@@ -0,0 +1,188 @@
|
||||
var types = require("hap-nodejs/accessories/types.js");
|
||||
var sonos = require('sonos');
|
||||
|
||||
function SonosPlatform(log, config){
|
||||
this.log = log;
|
||||
this.config = config;
|
||||
this.name = config["name"];
|
||||
this.playVolume = config["play_volume"];
|
||||
// timeout for device discovery
|
||||
this.discoveryTimeout = (config.deviceDiscoveryTimeout || 10)*1000; // assume 10sec as a default
|
||||
}
|
||||
|
||||
SonosPlatform.prototype = {
|
||||
accessories: function(callback) {
|
||||
this.log("Fetching Sonos devices.");
|
||||
var that = this;
|
||||
|
||||
// track found devices so we don't add duplicates
|
||||
var roomNamesFound = {};
|
||||
|
||||
// collector array for the devices from callbacks
|
||||
var devicesFound = [];
|
||||
// tell the sonos callbacks if timeout already occured
|
||||
var timeout = false;
|
||||
|
||||
// the timeout event will push the accessories back
|
||||
setTimeout(function(){
|
||||
timeout=true;
|
||||
callback(devicesFound);
|
||||
}, this.discoveryTimeout);
|
||||
|
||||
|
||||
sonos.search(function (device) {
|
||||
that.log("Found device at " + device.host);
|
||||
|
||||
device.deviceDescription(function (err, description) {
|
||||
if (description["zoneType"] != '11' && description["zoneType"] != '8' && description["zoneType"] != '4') { // 8 is the Sonos SUB, 4 is the Sonos Bridge
|
||||
var roomName = description["roomName"];
|
||||
|
||||
if (!roomNamesFound[roomName]) {
|
||||
roomNamesFound[roomName] = true;
|
||||
that.log("Found playable device - " + roomName);
|
||||
if (timeout) {
|
||||
that.log("Ignored: Discovered after timeout (Set deviceDiscoveryTimeout parameter in Sonos section of config.json)");
|
||||
}
|
||||
// device is an instance of sonos.Sonos
|
||||
var accessory = new SonosAccessory(that.log, that.config, device, description);
|
||||
// add it to the collector array
|
||||
devicesFound.push(accessory);
|
||||
}
|
||||
else {
|
||||
that.log("Ignoring playable device with duplicate room name - " + roomName);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function SonosAccessory(log, config, device, description) {
|
||||
this.log = log;
|
||||
this.config = config;
|
||||
this.device = device;
|
||||
this.description = description;
|
||||
|
||||
this.name = this.description["roomName"] + " " + this.config["name"];
|
||||
this.serviceName = this.description["roomName"] + " Speakers";
|
||||
this.playVolume = this.config["play_volume"];
|
||||
}
|
||||
|
||||
SonosAccessory.prototype = {
|
||||
|
||||
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: this.serviceName,
|
||||
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;
|
||||
module.exports.platform = SonosPlatform;
|
||||
265
platforms/Telldus.js
Normal file
265
platforms/Telldus.js
Normal file
@@ -0,0 +1,265 @@
|
||||
var types = require("hap-nodejs/accessories/types.js");
|
||||
var telldus = require('telldus');
|
||||
|
||||
function TelldusPlatform(log, config) {
|
||||
var that = this;
|
||||
that.log = log;
|
||||
}
|
||||
|
||||
TelldusPlatform.prototype = {
|
||||
|
||||
accessories: function(callback) {
|
||||
var that = this;
|
||||
|
||||
that.log("Fetching devices...");
|
||||
|
||||
var devices = telldus.getDevicesSync();
|
||||
|
||||
that.log("Found " + devices.length + " devices...");
|
||||
|
||||
var foundAccessories = [];
|
||||
|
||||
// Clean non device
|
||||
for (var i = 0; i < devices.length; i++) {
|
||||
if (devices[i].type != 'DEVICE') {
|
||||
devices.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < devices.length; i++) {
|
||||
if (devices[i].type === 'DEVICE') {
|
||||
TelldusAccessory.create(that.log, devices[i], function(err, accessory) {
|
||||
if (!!err) that.log("Couldn't load device info");
|
||||
foundAccessories.push(accessory);
|
||||
if (foundAccessories.length >= devices.length) {
|
||||
callback(foundAccessories);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var TelldusAccessory = function TelldusAccessory(log, device) {
|
||||
|
||||
this.log = log;
|
||||
|
||||
var m = device.model.split(':');
|
||||
|
||||
this.dimTimeout = false;
|
||||
|
||||
// Set accessory info
|
||||
this.device = device;
|
||||
this.id = device.id;
|
||||
this.name = device.name;
|
||||
this.manufacturer = "Telldus"; // NOTE: Change this later
|
||||
this.model = device.model;
|
||||
this.status = device.status;
|
||||
switch (device.status.name) {
|
||||
case 'OFF':
|
||||
this.state = 0;
|
||||
this.stateValue = 0;
|
||||
break;
|
||||
case 'ON':
|
||||
this.state = 2;
|
||||
this.stateValue = 1;
|
||||
break;
|
||||
case 'DIM':
|
||||
this.state = 16;
|
||||
this.stateValue = device.status.level;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
TelldusAccessory.create = function (log, device, callback) {
|
||||
|
||||
callback(null, new TelldusAccessory(log, device));
|
||||
|
||||
};
|
||||
|
||||
TelldusAccessory.prototype = {
|
||||
|
||||
dimmerValue: function() {
|
||||
|
||||
if (this.state === 1) {
|
||||
return 100;
|
||||
}
|
||||
|
||||
if (this.state === 16 && this.stateValue != "unde") {
|
||||
return parseInt(this.stateValue * 100 / 255);
|
||||
}
|
||||
|
||||
return 0;
|
||||
},
|
||||
|
||||
informationCharacteristics: function() {
|
||||
var that = this;
|
||||
|
||||
informationCharacteristics = [
|
||||
{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: that.name,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of the accessory",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MANUFACTURER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: that.manufacturer,
|
||||
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: "A1S2NASF88EW",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "SN",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.IDENTIFY_CTYPE,
|
||||
onUpdate: function () {
|
||||
telldus.turnOff(that.id, function(err){
|
||||
if (!!err) that.log("Error: " + err.message);
|
||||
telldus.turnOn(that.id, function(err){
|
||||
if (!!err) that.log("Error: " + err.message);
|
||||
telldus.turnOff(that.id, function(err){
|
||||
if (!!err) that.log("Error: " + err.message);
|
||||
telldus.turnOn(that.id, function(err){
|
||||
if (!!err) that.log("Error: " + err.message);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
perms: ["pw"],
|
||||
format: "bool",
|
||||
initialValue: false,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Identify Accessory",
|
||||
designedMaxLength: 1
|
||||
}
|
||||
];
|
||||
return informationCharacteristics;
|
||||
},
|
||||
|
||||
controlCharacteristics: function() {
|
||||
var that = this;
|
||||
|
||||
cTypes = [{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: that.name,
|
||||
supportEvents: true,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of service",
|
||||
designedMaxLength: 255
|
||||
}]
|
||||
|
||||
cTypes.push({
|
||||
cType: types.POWER_STATE_CTYPE,
|
||||
onUpdate: function(value) {
|
||||
if (value) {
|
||||
telldus.turnOn(that.id, function(err){
|
||||
if (!!err) {
|
||||
that.log("Error: " + err.message)
|
||||
} else {
|
||||
that.log(that.name + " - Updated power state: ON");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
telldus.turnOff(that.id, function(err){
|
||||
if (!!err) {
|
||||
that.log("Error: " + err.message)
|
||||
} else {
|
||||
that.log(that.name + " - Updated power state: OFF");
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "bool",
|
||||
initialValue: (that.state != 2 && (that.state === 16 && that.stateValue != 0)) ? 1 : 0,
|
||||
supportEvents: true,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Change the power state",
|
||||
designedMaxLength: 1
|
||||
})
|
||||
|
||||
if (that.model === "selflearning-dimmer") {
|
||||
cTypes.push({
|
||||
cType: types.BRIGHTNESS_CTYPE,
|
||||
onUpdate: function (value) {
|
||||
if (that.dimTimeout) {
|
||||
clearTimeout(that.dimTimeout);
|
||||
}
|
||||
|
||||
that.dimTimeout = setTimeout(function(){
|
||||
telldus.dim(that.id, (255 * (value / 100)), function(err, result){
|
||||
if (!!err) {
|
||||
that.log("Error: " + err.message);
|
||||
} else {
|
||||
that.log(that.name + " - Updated brightness: " + value);
|
||||
}
|
||||
});
|
||||
that.dimTimeout = false;
|
||||
}, 250);
|
||||
},
|
||||
perms: ["pw", "pr", "ev"],
|
||||
format: "int",
|
||||
initialValue: that.dimmerValue(),
|
||||
supportEvents: true,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Adjust Brightness of Light",
|
||||
designedMinValue: 0,
|
||||
designedMaxValue: 100,
|
||||
designedMinStep: 1,
|
||||
unit: "%"
|
||||
})
|
||||
}
|
||||
|
||||
return cTypes
|
||||
},
|
||||
|
||||
getServices: function() {
|
||||
|
||||
var services = [
|
||||
{
|
||||
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||
characteristics: this.informationCharacteristics()
|
||||
},
|
||||
{
|
||||
sType: types.LIGHTBULB_STYPE,
|
||||
characteristics: this.controlCharacteristics()
|
||||
}
|
||||
];
|
||||
|
||||
return services;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.platform = TelldusPlatform;
|
||||
module.exports.accessory = TelldusAccessory;
|
||||
265
platforms/TelldusLive.js
Normal file
265
platforms/TelldusLive.js
Normal file
@@ -0,0 +1,265 @@
|
||||
var types = require("hap-nodejs/accessories/types.js");
|
||||
var TellduAPI = require("telldus-live");
|
||||
|
||||
function TelldusLivePlatform(log, config) {
|
||||
var that = this;
|
||||
that.log = log;
|
||||
|
||||
that.isLoggedIn = false;
|
||||
|
||||
// Login to Telldus Live!
|
||||
that.cloud = new TellduAPI.TelldusAPI({publicKey: config["public_key"], privateKey: config["private_key"]})
|
||||
.login(config["token"], config["token_secret"], function(err, user) {
|
||||
if (!!err) that.log("Login error: " + err.message);
|
||||
that.log("User logged in: " + user.firstname + " " + user.lastname + ", " + user.email);
|
||||
that.isLoggedIn = true;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
TelldusLivePlatform.prototype = {
|
||||
|
||||
accessories: function(callback) {
|
||||
var that = this;
|
||||
|
||||
that.log("Fetching devices...");
|
||||
|
||||
that.cloud.getDevices(function(err, devices) {
|
||||
|
||||
if (!!err) return that.log('getDevices: ' + err.message);
|
||||
|
||||
var foundAccessories = [];
|
||||
|
||||
// Clean non device
|
||||
for (var i = 0; i < devices.length; i++) {
|
||||
if (devices[i].type != 'device') {
|
||||
devices.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < devices.length; i++) {
|
||||
if (devices[i].type === 'device') {
|
||||
TelldusLiveAccessory.create(that.log, devices[i], that.cloud, function(err, accessory) {
|
||||
if (!!err) that.log("Couldn't load device info");
|
||||
foundAccessories.push(accessory);
|
||||
if (foundAccessories.length >= devices.length) {
|
||||
callback(foundAccessories);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
var TelldusLiveAccessory = function TelldusLiveAccessory(log, cloud, device) {
|
||||
|
||||
this.log = log;
|
||||
this.cloud = cloud;
|
||||
|
||||
var m = device.model ? device.model.split(':') : ['unknown', 'unknown'] ;
|
||||
|
||||
// Set accessory info
|
||||
this.device = device;
|
||||
this.id = device.id;
|
||||
this.name = device.name;
|
||||
this.manufacturer = m[1];
|
||||
this.model = m[0];
|
||||
this.state = device.state;
|
||||
this.stateValue = device.stateValue;
|
||||
this.status = device.status;
|
||||
};
|
||||
|
||||
TelldusLiveAccessory.create = function (log, device, cloud, callback) {
|
||||
|
||||
cloud.getDeviceInfo(device, function(err, device) {
|
||||
|
||||
if (!!err) that.log("Couldn't load device info");
|
||||
|
||||
callback(err, new TelldusLiveAccessory(log, cloud, device));
|
||||
});
|
||||
};
|
||||
|
||||
TelldusLiveAccessory.prototype = {
|
||||
|
||||
dimmerValue: function() {
|
||||
|
||||
if (this.state === 1) {
|
||||
return 100;
|
||||
}
|
||||
|
||||
if (this.state === 16 && this.stateValue != "unde") {
|
||||
return parseInt(this.stateValue * 100 / 255);
|
||||
}
|
||||
|
||||
return 0;
|
||||
},
|
||||
|
||||
informationCharacteristics: function() {
|
||||
var that = this;
|
||||
|
||||
informationCharacteristics = [
|
||||
{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: that.name,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of the accessory",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MANUFACTURER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: that.manufacturer,
|
||||
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: "A1S2NASF88EW",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "SN",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.IDENTIFY_CTYPE,
|
||||
onUpdate: function () {
|
||||
that.cloud.onOffDevice(that.device, true, function(err, result) {
|
||||
if (!!err) that.log("Error: " + err.message);
|
||||
that.cloud.onOffDevice(that.device, false, function(err, result) {
|
||||
if (!!err) that.log("Error: " + err.message);
|
||||
that.cloud.onOffDevice(that.device, true, function(err, result) {
|
||||
if (!!err) that.log("Error: " + err.message);
|
||||
that.cloud.onOffDevice(that.device, false, function(err, result) {
|
||||
if (!!err) that.log("Error: " + err.message);
|
||||
that.cloud.onOffDevice(that.device, true, function(err, result) {
|
||||
if (!!err) that.log("Error: " + err.message);
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
perms: ["pw"],
|
||||
format: "bool",
|
||||
initialValue: false,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Identify Accessory",
|
||||
designedMaxLength: 1
|
||||
}
|
||||
];
|
||||
return informationCharacteristics;
|
||||
},
|
||||
|
||||
controlCharacteristics: function() {
|
||||
var that = this;
|
||||
|
||||
cTypes = [{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: that.name,
|
||||
supportEvents: true,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of service",
|
||||
designedMaxLength: 255
|
||||
}]
|
||||
|
||||
cTypes.push({
|
||||
cType: types.POWER_STATE_CTYPE,
|
||||
onUpdate: function(value) {
|
||||
if (value == 1) {
|
||||
that.cloud.onOffDevice(that.device, value, function(err, result) {
|
||||
if (!!err) {
|
||||
that.log("Error: " + err.message)
|
||||
} else {
|
||||
that.log(that.name + " - Updated power state: " + (value === true ? 'ON' : 'OFF'));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
that.cloud.onOffDevice(that.device, value, function(err, result) {
|
||||
if (!!err) {
|
||||
that.log("Error: " + err.message)
|
||||
} else {
|
||||
that.log(that.name + " - Updated power state: " + (value === true ? 'ON' : 'OFF'));
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "bool",
|
||||
initialValue: (that.state != 2 && (that.state === 16 && that.stateValue != "0")) ? 1 : 0,
|
||||
supportEvents: true,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Change the power state",
|
||||
designedMaxLength: 1
|
||||
})
|
||||
|
||||
if (that.model === "selflearning-dimmer") {
|
||||
cTypes.push({
|
||||
cType: types.BRIGHTNESS_CTYPE,
|
||||
onUpdate: function (value) {
|
||||
that.cloud.dimDevice(that.device, (255 * (value / 100)), function (err, result) {
|
||||
if (!!err) {
|
||||
that.log("Error: " + err.message);
|
||||
} else {
|
||||
that.log(that.name + " - Updated brightness: " + value);
|
||||
}
|
||||
});
|
||||
},
|
||||
perms: ["pw", "pr", "ev"],
|
||||
format: "int",
|
||||
initialValue: that.dimmerValue(),
|
||||
supportEvents: true,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Adjust Brightness of Light",
|
||||
designedMinValue: 0,
|
||||
designedMaxValue: 100,
|
||||
designedMinStep: 1,
|
||||
unit: "%"
|
||||
})
|
||||
}
|
||||
|
||||
return cTypes
|
||||
},
|
||||
|
||||
getServices: function() {
|
||||
|
||||
var services = [
|
||||
{
|
||||
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||
characteristics: this.informationCharacteristics()
|
||||
},
|
||||
{
|
||||
sType: types.LIGHTBULB_STYPE,
|
||||
characteristics: this.controlCharacteristics()
|
||||
}
|
||||
];
|
||||
|
||||
return services;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.platform = TelldusLivePlatform;
|
||||
module.exports.accessory = TelldusLiveAccessory;
|
||||
@@ -1,4 +1,4 @@
|
||||
var types = require("HAP-NodeJS/accessories/types.js");
|
||||
var types = require("hap-nodejs/accessories/types.js");
|
||||
var wink = require('wink-js');
|
||||
|
||||
var model = {
|
||||
|
||||
238
platforms/YamahaAVR.js
Normal file
238
platforms/YamahaAVR.js
Normal file
@@ -0,0 +1,238 @@
|
||||
var types = require("hap-nodejs/accessories/types.js");
|
||||
var inherits = require('util').inherits;
|
||||
var debug = require('debug')('YamahaAVR');
|
||||
var Service = require("hap-nodejs").Service;
|
||||
var Characteristic = require("hap-nodejs").Characteristic;
|
||||
var Yamaha = require('yamaha-nodejs');
|
||||
var Q = require('q');
|
||||
var mdns = require('mdns');
|
||||
//workaround for raspberry pi
|
||||
var sequence = [
|
||||
mdns.rst.DNSServiceResolve(),
|
||||
'DNSServiceGetAddrInfo' in mdns.dns_sd ? mdns.rst.DNSServiceGetAddrInfo() : mdns.rst.getaddrinfo({families:[4]}),
|
||||
mdns.rst.makeAddressesUnique()
|
||||
];
|
||||
|
||||
function YamahaAVRPlatform(log, config){
|
||||
this.log = log;
|
||||
this.config = config;
|
||||
this.playVolume = config["play_volume"];
|
||||
this.minVolume = config["min_volume"] || -50.0;
|
||||
this.maxVolume = config["max_volume"] || -20.0;
|
||||
this.gapVolume = this.maxVolume - this.minVolume;
|
||||
this.setMainInputTo = config["setMainInputTo"];
|
||||
this.expectedDevices = config["expected_devices"] || 100;
|
||||
this.discoveryTimeout = config["discovery_timeout"] || 30;
|
||||
this.manualAddresses = config["manual_addresses"] || {};
|
||||
this.browser = mdns.createBrowser(mdns.tcp('http'), {resolverSequence: sequence});
|
||||
}
|
||||
|
||||
// Custom Characteristics and service...
|
||||
|
||||
YamahaAVRPlatform.AudioVolume = function() {
|
||||
Characteristic.call(this, 'Audio Volume', '00001001-0000-1000-8000-135D67EC4377');
|
||||
this.setProps({
|
||||
format: Characteristic.Formats.UINT8,
|
||||
unit: Characteristic.Units.PERCENTAGE,
|
||||
maxValue: 100,
|
||||
minValue: 0,
|
||||
minStep: 1,
|
||||
perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY]
|
||||
});
|
||||
this.value = this.getDefaultValue();
|
||||
};
|
||||
inherits(YamahaAVRPlatform.AudioVolume, Characteristic);
|
||||
|
||||
YamahaAVRPlatform.Muting = function() {
|
||||
Characteristic.call(this, 'Muting', '00001002-0000-1000-8000-135D67EC4377');
|
||||
this.setProps({
|
||||
format: Characteristic.Formats.UINT8,
|
||||
perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY]
|
||||
});
|
||||
this.value = this.getDefaultValue();
|
||||
};
|
||||
inherits(YamahaAVRPlatform.Muting, Characteristic);
|
||||
|
||||
YamahaAVRPlatform.AudioDeviceService = function(displayName, subtype) {
|
||||
Service.call(this, displayName, '00000001-0000-1000-8000-135D67EC4377', subtype);
|
||||
|
||||
// Required Characteristics
|
||||
this.addCharacteristic(YamahaAVRPlatform.AudioVolume);
|
||||
|
||||
// Optional Characteristics
|
||||
this.addOptionalCharacteristic(YamahaAVRPlatform.Muting);
|
||||
};
|
||||
inherits(YamahaAVRPlatform.AudioDeviceService, Service);
|
||||
|
||||
|
||||
YamahaAVRPlatform.prototype = {
|
||||
accessories: function(callback) {
|
||||
this.log("Getting Yamaha AVR devices.");
|
||||
var that = this;
|
||||
|
||||
var browser = this.browser;
|
||||
browser.stop();
|
||||
browser.removeAllListeners('serviceUp'); // cleanup listeners
|
||||
var accessories = [];
|
||||
var timer, timeElapsed = 0, checkCyclePeriod = 5000;
|
||||
|
||||
// Hmm... seems we need to prevent double-listing via manual and Bonjour...
|
||||
var sysIds = {};
|
||||
|
||||
var setupFromService = function(service){
|
||||
var name = service.name;
|
||||
//console.log('Found HTTP service "' + name + '"');
|
||||
// We can't tell just from mdns if this is an AVR...
|
||||
if (service.port != 80) return; // yamaha-nodejs assumes this, so finding one on another port wouldn't do any good anyway.
|
||||
var yamaha = new Yamaha(service.host);
|
||||
yamaha.getSystemConfig().then(
|
||||
function(sysConfig){
|
||||
var sysModel = sysConfig.YAMAHA_AV.System[0].Config[0].Model_Name[0];
|
||||
var sysId = sysConfig.YAMAHA_AV.System[0].Config[0].System_ID[0];
|
||||
if(sysIds[sysId]){
|
||||
this.log("WARN: Got multiple systems with ID " + sysId + "! Omitting duplicate!");
|
||||
return;
|
||||
}
|
||||
sysIds[sysId] = true;
|
||||
this.log("Found Yamaha " + sysModel + " - " + sysId + ", \"" + name + "\"");
|
||||
var accessory = new YamahaAVRAccessory(this.log, this.config, name, yamaha, sysConfig);
|
||||
accessories.push(accessory);
|
||||
if(accessories.length >= this.expectedDevices)
|
||||
timeoutFunction(); // We're done, call the timeout function now.
|
||||
}.bind(this)
|
||||
);
|
||||
}.bind(this);
|
||||
|
||||
// process manually specified devices...
|
||||
for(var key in this.manualAddresses){
|
||||
if(!this.manualAddresses.hasOwnProperty(key)) continue;
|
||||
setupFromService({
|
||||
name: key,
|
||||
host: this.manualAddresses[key],
|
||||
port: 80
|
||||
});
|
||||
}
|
||||
|
||||
browser.on('serviceUp', setupFromService);
|
||||
browser.start();
|
||||
|
||||
// The callback can only be called once...so we'll have to find as many as we can
|
||||
// in a fixed time and then call them in.
|
||||
var timeoutFunction = function(){
|
||||
if(accessories.length >= that.expectedDevices){
|
||||
clearTimeout(timer);
|
||||
} else {
|
||||
timeElapsed += checkCyclePeriod;
|
||||
if(timeElapsed > that.discoveryTimeout * 1000){
|
||||
that.log("Waited " + that.discoveryTimeout + " seconds, stopping discovery.");
|
||||
} else {
|
||||
timer = setTimeout(timeoutFunction, checkCyclePeriod);
|
||||
return;
|
||||
}
|
||||
}
|
||||
browser.stop();
|
||||
browser.removeAllListeners('serviceUp');
|
||||
that.log("Discovery finished, found " + accessories.length + " Yamaha AVR devices.");
|
||||
callback(accessories);
|
||||
};
|
||||
timer = setTimeout(timeoutFunction, checkCyclePeriod);
|
||||
}
|
||||
};
|
||||
|
||||
function YamahaAVRAccessory(log, config, name, yamaha, sysConfig) {
|
||||
this.log = log;
|
||||
this.config = config;
|
||||
this.yamaha = yamaha;
|
||||
this.sysConfig = sysConfig;
|
||||
|
||||
this.nameSuffix = config["name_suffix"] || " Speakers";
|
||||
this.name = name;
|
||||
this.serviceName = name + this.nameSuffix;
|
||||
this.setMainInputTo = config["setMainInputTo"];
|
||||
this.playVolume = this.config["play_volume"];
|
||||
this.minVolume = config["min_volume"] || -50.0;
|
||||
this.maxVolume = config["max_volume"] || -20.0;
|
||||
this.gapVolume = this.maxVolume - this.minVolume;
|
||||
}
|
||||
|
||||
YamahaAVRAccessory.prototype = {
|
||||
|
||||
setPlaying: function(playing) {
|
||||
var that = this;
|
||||
var yamaha = this.yamaha;
|
||||
|
||||
if (playing) {
|
||||
|
||||
return yamaha.powerOn().then(function(){
|
||||
if (that.playVolume) return yamaha.setVolumeTo(that.playVolume*10);
|
||||
else return Q();
|
||||
}).then(function(){
|
||||
if (that.setMainInputTo) return yamaha.setMainInputTo(that.setMainInputTo);
|
||||
else return Q();
|
||||
}).then(function(){
|
||||
if (that.setMainInputTo == "AirPlay") return yamaha.SendXMLToReceiver(
|
||||
'<YAMAHA_AV cmd="PUT"><AirPlay><Play_Control><Playback>Play</Playback></Play_Control></AirPlay></YAMAHA_AV>'
|
||||
);
|
||||
else return Q();
|
||||
});
|
||||
}
|
||||
else {
|
||||
return yamaha.powerOff();
|
||||
}
|
||||
},
|
||||
|
||||
getServices: function() {
|
||||
var that = this;
|
||||
var informationService = new Service.AccessoryInformation();
|
||||
var yamaha = this.yamaha;
|
||||
|
||||
informationService
|
||||
.setCharacteristic(Characteristic.Name, this.name)
|
||||
.setCharacteristic(Characteristic.Manufacturer, "Yamaha")
|
||||
.setCharacteristic(Characteristic.Model, this.sysConfig.YAMAHA_AV.System[0].Config[0].Model_Name[0])
|
||||
.setCharacteristic(Characteristic.SerialNumber, this.sysConfig.YAMAHA_AV.System[0].Config[0].System_ID[0]);
|
||||
|
||||
var switchService = new Service.Switch("Power State");
|
||||
switchService.getCharacteristic(Characteristic.On)
|
||||
.on('get', function(callback, context){
|
||||
yamaha.isOn().then(function(result){
|
||||
callback(false, result);
|
||||
}.bind(this));
|
||||
}.bind(this))
|
||||
.on('set', function(powerOn, callback){
|
||||
this.setPlaying(powerOn).then(function(){
|
||||
callback(false, powerOn);
|
||||
}, function(error){
|
||||
callback(error, !powerOn); //TODO: Actually determine and send real new status.
|
||||
});
|
||||
}.bind(this));
|
||||
|
||||
var audioDeviceService = new YamahaAVRPlatform.AudioDeviceService("Audio Functions");
|
||||
audioDeviceService.getCharacteristic(YamahaAVRPlatform.AudioVolume)
|
||||
.on('get', function(callback, context){
|
||||
yamaha.getBasicInfo().done(function(basicInfo){
|
||||
var v = basicInfo.getVolume()/10.0;
|
||||
var p = 100 * ((v - that.minVolume) / that.gapVolume);
|
||||
p = p < 0 ? 0 : p > 100 ? 100 : Math.round(p);
|
||||
debug("Got volume percent of " + p + "%");
|
||||
callback(false, p);
|
||||
});
|
||||
})
|
||||
.on('set', function(p, callback){
|
||||
var v = ((p / 100) * that.gapVolume) + that.minVolume;
|
||||
v = Math.round(v*10.0);
|
||||
debug("Setting volume to " + v);
|
||||
yamaha.setVolumeTo(v).then(function(){
|
||||
callback(false, p);
|
||||
});
|
||||
})
|
||||
.getValue(null, null); // force an asynchronous get
|
||||
|
||||
|
||||
return [informationService, switchService, audioDeviceService];
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.accessory = YamahaAVRAccessory;
|
||||
module.exports.platform = YamahaAVRPlatform;
|
||||
1012
platforms/ZWayServer.js
Normal file
1012
platforms/ZWayServer.js
Normal file
File diff suppressed because it is too large
Load Diff
763
platforms/isy-js.js
Normal file
763
platforms/isy-js.js
Normal file
@@ -0,0 +1,763 @@
|
||||
/*
|
||||
ISY-JS
|
||||
|
||||
ISY-99 REST / WebSockets based HomeBridge shim.
|
||||
|
||||
Supports the following Insteon devices: Lights (dimmable and non-dimmable), Fans, Outlets, Door/Window Sensors, MorningLinc locks, Inline Lincs and I/O Lincs.
|
||||
Also supports ZWave based locks. If elkEnabled is set to true then this will also expose your Elk Alarm Panel and all of your Elk Sensors.
|
||||
|
||||
Turns out that HomeBridge platforms can only return a maximum of 100 devices. So if you end up exposing more then 100 devices through HomeBridge the HomeKit
|
||||
software will fail adding the HomeBridge to your HomeKit network. To address this issue this platform provides an option to screen out devices based on
|
||||
criteria specified in the config.
|
||||
|
||||
Configuration sample:
|
||||
|
||||
"platforms": [
|
||||
{
|
||||
"platform": "isy-js",
|
||||
"name": "isy-js",
|
||||
"host": "10.0.1.12",
|
||||
"username": "admin",
|
||||
"password": "password",
|
||||
"elkEnabled": true,
|
||||
"ignoreDevices": [
|
||||
{ "nameContains": "ApplianceLinc", "lastAddressDigit": "", "address": ""},
|
||||
{ "nameContains": "Bedroom.Side Gate", "lastAddressDigit": "", "address": ""},
|
||||
{ "nameContains": "Remote", "lastAddressDigit": "", "address": "" },
|
||||
{ "nameContains": "Keypad", "lastAddressDigit": "2", "address": "" },
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
Fields:
|
||||
"platform" - Must be set to isy-js
|
||||
"name" - Can be set to whatever you want
|
||||
"host" - IP address of the ISY
|
||||
"username" - Your ISY username
|
||||
"password" - Your ISY password
|
||||
"elkEnabled" - true if there is an elk alarm panel connected to your ISY
|
||||
"ignoreDevices" - Array of objects specifying criteria for screening out devices from the network. nameContains is the only required criteria. If the other criteria
|
||||
are blank all devices will match those criteria (providing they match the name criteria).
|
||||
"nameContains" - Specifies a substring to check against the names of the ISY devices. Required field for the criteria.
|
||||
"lastAddressDigit" - Specifies a single digit in the ISY address of a device which should be used to match the device. Example use of this is for composite
|
||||
devices like keypads so you can screen out the non-main buttons.
|
||||
"address" - ISY address to match.
|
||||
|
||||
Examples:
|
||||
|
||||
{ "nameContains": "Keypad", "lastAddressDigit": "2", "address": "" } - Ignore all devices which have the word Keypad in their name and whose last address digit is 2.
|
||||
{ "nameContains": "Remote", "lastAddressDigit": "", "address": "" } - Ignore all devices which have the word Remote in their name
|
||||
{ "nameContains": "", "lastAddressDigit": "", "address": "15 5 3 2"} - Ignore the device with an ISY address of 15 5 3 2.
|
||||
|
||||
TODOS: Implement identify functions (beep perhaps?) and more device types.
|
||||
*/
|
||||
|
||||
|
||||
var types = require("hap-nodejs/accessories/types.js");
|
||||
var isy = require('isy-js');
|
||||
var Service = require("hap-nodejs").Service;
|
||||
var Characteristic = require("hap-nodejs").Characteristic;
|
||||
var inherits = require('util').inherits;
|
||||
|
||||
// Global device map. Needed to map incoming notifications to the corresponding HomeKit device for update.
|
||||
var deviceMap = {};
|
||||
|
||||
// This function responds to changes in devices from the isy-js library. Uses the global device map to update
|
||||
// the state.
|
||||
// TODO: Move this to a member function of the ISYPlatform object so we don't need a global map.
|
||||
function ISYChangeHandler(isy,device) {
|
||||
var deviceToUpdate = deviceMap[device.address];
|
||||
if(deviceToUpdate != null) {
|
||||
deviceToUpdate.handleExternalChange();
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to have ISYJSDEBUG control if debug output appears
|
||||
function ISYJSDebugMessage(isy,message) {
|
||||
if(process.env.ISYJSDEBUG != undefined) {
|
||||
isy.log(message);
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// PLATFORM
|
||||
|
||||
// Construct the ISY platform. log = Logger, config = homebridge cofnig
|
||||
function ISYPlatform(log,config) {
|
||||
this.log = log;
|
||||
this.config = config;
|
||||
this.host = config.host;
|
||||
this.username = config.username;
|
||||
this.password = config.password;
|
||||
this.elkEnabled = config.elkEnabled;
|
||||
this.isy = new isy.ISY(this.host, this.username,this.password, config.elkEnabled, ISYChangeHandler);
|
||||
}
|
||||
|
||||
// Checks the device against the configuration to see if it should be ignored.
|
||||
ISYPlatform.prototype.shouldIgnore = function(device) {
|
||||
var deviceAddress = device.address;
|
||||
var deviceName = device.name;
|
||||
for(var index = 0; index < this.config.ignoreDevices.length; index++) {
|
||||
var rule = this.config.ignoreDevices[index];
|
||||
if(rule.nameContains != "") {
|
||||
if(deviceName.indexOf(rule.nameContains) == -1) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if(rule.lastAddressDigit != "") {
|
||||
if(deviceAddress.indexOf(rule.lastAddressDigit,deviceAddress.length-2) == -1) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if(rule.address != "") {
|
||||
if(deviceAddress != rule.address) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
ISYJSDebugMessage(this,"Ignoring device: "+deviceName+" ["+deviceAddress+"] because of rule ["+rule.nameContains+"] ["+rule.lastAddressDigit+"] ["+rule.address+"]");
|
||||
return true;
|
||||
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calls the isy-js library, retrieves the list of devices, and maps them to appropriate ISYXXXXAccessory devices.
|
||||
ISYPlatform.prototype.accessories = function(callback) {
|
||||
var that = this;
|
||||
this.isy.initialize(function() {
|
||||
var results = [];
|
||||
var deviceList = that.isy.getDeviceList();
|
||||
for(var index = 0; index < deviceList.length; index++) {
|
||||
var device = deviceList[index];
|
||||
var homeKitDevice = null;
|
||||
if(!that.shouldIgnore(device)) {
|
||||
|
||||
if(device.deviceType == that.isy.DEVICE_TYPE_LIGHT || device.deviceType == that.isy.DEVICE_TYPE_DIMMABLE_LIGHT) {
|
||||
homeKitDevice = new ISYLightAccessory(that.log,device);
|
||||
} else if(device.deviceType == that.isy.DEVICE_TYPE_LOCK || device.deviceType == that.isy.DEVICE_TYPE_SECURE_LOCK) {
|
||||
homeKitDevice = new ISYLockAccessory(that.log,device);
|
||||
} else if(device.deviceType == that.isy.DEVICE_TYPE_OUTLET) {
|
||||
homeKitDevice = new ISYOutletAccessory(that.log,device);
|
||||
} else if(device.deviceType == that.isy.DEVICE_TYPE_FAN) {
|
||||
homeKitDevice = new ISYFanAccessory(that.log,device);
|
||||
} else if(device.deviceType == that.isy.DEVICE_TYPE_DOOR_WINDOW_SENSOR) {
|
||||
homeKitDevice = new ISYDoorWindowSensorAccessory(that.log,device);
|
||||
} else if(device.deviceType == that.isy.DEVICE_TYPE_ALARM_DOOR_WINDOW_SENSOR) {
|
||||
homeKitDevice = new ISYDoorWindowSensorAccessory(that.log,device);
|
||||
} else if(device.deviceType == that.isy.DEVICE_TYPE_ALARM_PANEL) {
|
||||
homeKitDevice = new ISYElkAlarmPanelAccessory(that.log,device);
|
||||
}
|
||||
if(homeKitDevice != null) {
|
||||
// Make sure the device is address to the global map
|
||||
deviceMap[device.address] = homeKitDevice;
|
||||
results.push(homeKitDevice);
|
||||
}
|
||||
}
|
||||
}
|
||||
if(that.isy.elkEnabled) {
|
||||
var panelDevice = that.isy.getElkAlarmPanel();
|
||||
var panelDeviceHK = new ISYElkAlarmPanelAccessory(that.log,panelDevice);
|
||||
deviceMap[panelDevice.address] = panelDeviceHK;
|
||||
results.push(panelDeviceHK);
|
||||
}
|
||||
ISYJSDebugMessage(that,"Filtered device has: "+results.length+" devices");
|
||||
callback(results);
|
||||
});
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// BASE FOR ALL DEVICES
|
||||
|
||||
// Provides common constructor tasks
|
||||
function ISYAccessoryBaseSetup(accessory,log,device) {
|
||||
accessory.log = log;
|
||||
accessory.device = device;
|
||||
accessory.address = device.address;
|
||||
accessory.name = device.name;
|
||||
accessory.uuid_base = device.isy.address+":"+device.address;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// FANS - ISYFanAccessory
|
||||
// Implemetnts the fan service for an isy fan device.
|
||||
|
||||
// Constructs a fan accessory object. device is the isy-js device object and log is the logger.
|
||||
function ISYFanAccessory(log,device) {
|
||||
ISYAccessoryBaseSetup(this,log,device);
|
||||
}
|
||||
|
||||
ISYFanAccessory.prototype.identify = function(callback) {
|
||||
// Do the identify action
|
||||
callback();
|
||||
}
|
||||
|
||||
// Translates the fan speed as an isy-js string into the corresponding homekit constant level.
|
||||
// Homekit doesn't have steps for the fan speed and needs to have a value from 0 to 100. We
|
||||
// split the range into 4 steps and map them to the 4 isy-js levels.
|
||||
ISYFanAccessory.prototype.translateFanSpeedToHK = function(fanSpeed) {
|
||||
if(fanSpeed == this.device.FAN_LEVEL_OFF) {
|
||||
return 0;
|
||||
} else if(fanSpeed == this.device.FAN_LEVEL_LOW) {
|
||||
return 32;
|
||||
} else if(fanSpeed == this.device.FAN_LEVEL_MEDIUM) {
|
||||
return 67;
|
||||
} else if(fanSpeed == this.device.FAN_LEVEL_HIGH) {
|
||||
return 100;
|
||||
} else {
|
||||
ISYJSDebugMessage(this,"!!!! ERROR: Unknown fan speed: "+fanSpeed);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Translates the fan level from homebridge into the isy-js level. Maps from the 0-100
|
||||
// to the four isy-js fan speed levels.
|
||||
ISYFanAccessory.prototype.translateHKToFanSpeed = function(fanStateHK) {
|
||||
if(fanStateHK == 0) {
|
||||
return this.device.FAN_LEVEL_OFF;
|
||||
} else if(fanStateHK > 0 && fanStateHK <=32) {
|
||||
return this.device.FAN_LEVEL_LOW;
|
||||
} else if(fanStateHK >= 33 && fanStateHK <= 67) {
|
||||
return this.device.FAN_LEVEL_MEDIUM;
|
||||
} else if(fanStateHK > 67) {
|
||||
return this.device.FAN_LEVEL_HIGH;
|
||||
} else {
|
||||
ISYJSDebugMessage(this,"ERROR: Unknown fan state!");
|
||||
return this.device.FAN_LEVEL_OFF;
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the current state of the fan from the isy-js level to the 0-100 level of HK.
|
||||
ISYFanAccessory.prototype.getFanRotationSpeed = function(callback) {
|
||||
callback(null,this.translateFanSpeedToHK(this.device.getCurrentFanState()));
|
||||
}
|
||||
|
||||
// Sets the current state of the fan from the 0-100 level of HK to the isy-js level.
|
||||
ISYFanAccessory.prototype.setFanRotationSpeed = function(fanStateHK,callback) {
|
||||
var newFanState = this.translateHKToFanSpeed(fanStateHK);
|
||||
ISYJSDebugMessage(this,"Sending command to set fan state to: "+newFanState);
|
||||
if(newFanState != this.device.getCurrentFanState()) {
|
||||
this.device.sendFanCommand(newFanState, function(result) {
|
||||
callback();
|
||||
});
|
||||
} else {
|
||||
ISYJSDebugMessage(this,"Fan command does not change actual speed");
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
// Returns true if the fan is on
|
||||
ISYFanAccessory.prototype.getIsFanOn = function() {
|
||||
return (this.device.getCurrentFanState() != "Off");
|
||||
}
|
||||
|
||||
// Returns the state of the fan to the homebridge system for the On characteristic
|
||||
ISYFanAccessory.prototype.getFanOnState = function(callback) {
|
||||
callback(null,this.getIsFanOn());
|
||||
}
|
||||
|
||||
// Sets the fan state based on the value of the On characteristic. Default to Medium for on.
|
||||
ISYFanAccessory.prototype.setFanOnState = function(onState,callback) {
|
||||
if(onState != this.getIsFanOn()) {
|
||||
if(onState) {
|
||||
this.setFanRotationSpeed(this.translateFanSpeedToHK(this.device.FAN_LEVEL_MEDIUM), callback);
|
||||
} else {
|
||||
this.setFanRotationSpeed(this.translateFanSpeedToHK(this.device.FAN_LEVEL_OFF), callback);
|
||||
}
|
||||
} else {
|
||||
ISYJSDebugMessage(this,"Fan command does not change actual state");
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
// Mirrors change in the state of the underlying isj-js device object.
|
||||
ISYFanAccessory.prototype.handleExternalChange = function() {
|
||||
this.fanService
|
||||
.setCharacteristic(Characteristic.On, this.getIsFanOn());
|
||||
|
||||
this.fanService
|
||||
.setCharacteristic(Characteristic.RotationSpeed, this.translateFanSpeedToHK(this.device.getCurrentFanState()));
|
||||
}
|
||||
|
||||
// Returns the services supported by the fan device.
|
||||
ISYFanAccessory.prototype.getServices = function() {
|
||||
var informationService = new Service.AccessoryInformation();
|
||||
|
||||
informationService
|
||||
.setCharacteristic(Characteristic.Manufacturer, "SmartHome")
|
||||
.setCharacteristic(Characteristic.Model, this.device.deviceFriendlyName)
|
||||
.setCharacteristic(Characteristic.SerialNumber, this.device.address);
|
||||
|
||||
var fanService = new Service.Fan();
|
||||
|
||||
this.fanService = fanService;
|
||||
this.informationService = informationService;
|
||||
|
||||
fanService
|
||||
.getCharacteristic(Characteristic.On)
|
||||
.on('set', this.setFanOnState.bind(this));
|
||||
|
||||
fanService
|
||||
.getCharacteristic(Characteristic.On)
|
||||
.on('get', this.getFanOnState.bind(this));
|
||||
|
||||
fanService
|
||||
.addCharacteristic(new Characteristic.RotationSpeed())
|
||||
.on('get', this.getFanRotationSpeed.bind(this));
|
||||
|
||||
fanService
|
||||
.getCharacteristic(Characteristic.RotationSpeed)
|
||||
.on('set', this.setFanRotationSpeed.bind(this));
|
||||
|
||||
return [informationService, fanService];
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// OUTLETS - ISYOutletAccessory
|
||||
// Implements the Outlet service for ISY devices.
|
||||
|
||||
// Constructs an outlet. log = HomeBridge logger, device = isy-js device to wrap
|
||||
function ISYOutletAccessory(log,device) {
|
||||
ISYAccessoryBaseSetup(this,log,device);
|
||||
}
|
||||
|
||||
// Handles the identify command
|
||||
ISYOutletAccessory.prototype.identify = function(callback) {
|
||||
// Do the identify action
|
||||
callback();
|
||||
}
|
||||
|
||||
// Handles a request to set the outlet state. Ignores redundant sets based on current states.
|
||||
ISYOutletAccessory.prototype.setOutletState = function(outletState,callback) {
|
||||
ISYJSDebugMessage(this,"Sending command to set outlet state to: "+outletState);
|
||||
if(outletState != this.device.getCurrentOutletState()) {
|
||||
this.device.sendOutletCommand(outletState, function(result) {
|
||||
callback();
|
||||
});
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
// Handles a request to get the current outlet state based on underlying isy-js device object.
|
||||
ISYOutletAccessory.prototype.getOutletState = function(callback) {
|
||||
callback(null,this.device.getCurrentOutletState());
|
||||
}
|
||||
|
||||
// Handles a request to get the current in use state of the outlet. We set this to true always as
|
||||
// there is no way to deterine this through the isy.
|
||||
ISYOutletAccessory.prototype.getOutletInUseState = function(callback) {
|
||||
callback(null, true);
|
||||
}
|
||||
|
||||
// Mirrors change in the state of the underlying isj-js device object.
|
||||
ISYOutletAccessory.prototype.handleExternalChange = function() {
|
||||
this.outletService
|
||||
.setCharacteristic(Characteristic.On, this.device.getCurrentOutletState());
|
||||
}
|
||||
|
||||
// Returns the set of services supported by this object.
|
||||
ISYOutletAccessory.prototype.getServices = function() {
|
||||
var informationService = new Service.AccessoryInformation();
|
||||
|
||||
informationService
|
||||
.setCharacteristic(Characteristic.Manufacturer, "SmartHome")
|
||||
.setCharacteristic(Characteristic.Model, this.device.deviceFriendlyName)
|
||||
.setCharacteristic(Characteristic.SerialNumber, this.device.address);
|
||||
|
||||
var outletService = new Service.Outlet();
|
||||
|
||||
this.outletService = outletService;
|
||||
this.informationService = informationService;
|
||||
|
||||
outletService
|
||||
.getCharacteristic(Characteristic.On)
|
||||
.on('set', this.setOutletState.bind(this));
|
||||
|
||||
outletService
|
||||
.getCharacteristic(Characteristic.On)
|
||||
.on('get', this.getOutletState.bind(this));
|
||||
|
||||
outletService
|
||||
.getCharacteristic(Characteristic.OutletInUse)
|
||||
.on('get', this.getOutletInUseState.bind(this));
|
||||
|
||||
return [informationService, outletService];
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// LOCKS - ISYLockAccessory
|
||||
// Implements the lock service for isy-js devices.
|
||||
|
||||
// Constructs a lock accessory. log = homebridge logger, device = isy-js device object being wrapped
|
||||
function ISYLockAccessory(log,device) {
|
||||
ISYAccessoryBaseSetup(this,log,device);
|
||||
}
|
||||
|
||||
// Handles an identify request
|
||||
ISYLockAccessory.prototype.identify = function(callback) {
|
||||
callback();
|
||||
}
|
||||
|
||||
// Handles a set to the target lock state. Will ignore redundant commands.
|
||||
ISYLockAccessory.prototype.setTargetLockState = function(lockState,callback) {
|
||||
ISYJSDebugMessage(this,"Sending command to set lock state to: "+lockState);
|
||||
if(lockState != this.getDeviceCurrentStateAsHK()) {
|
||||
var targetLockValue = (lockState == 0) ? false : true;
|
||||
this.device.sendLockCommand(targetLockValue, function(result) {
|
||||
callback();
|
||||
});
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
// Translates underlying lock state into the corresponding homekit state
|
||||
ISYLockAccessory.prototype.getDeviceCurrentStateAsHK = function() {
|
||||
return (this.device.getCurrentLockState() ? 1 : 0);
|
||||
}
|
||||
|
||||
// Handles request to get the current lock state for homekit
|
||||
ISYLockAccessory.prototype.getLockCurrentState = function(callback) {
|
||||
callback(null, this.getDeviceCurrentStateAsHK());
|
||||
}
|
||||
|
||||
// Handles request to get the target lock state for homekit
|
||||
ISYLockAccessory.prototype.getTargetLockState = function(callback) {
|
||||
this.getLockCurrentState(callback);
|
||||
}
|
||||
|
||||
// Mirrors change in the state of the underlying isj-js device object.
|
||||
ISYLockAccessory.prototype.handleExternalChange = function() {
|
||||
this.lockService
|
||||
.setCharacteristic(Characteristic.LockTargetState, this.getDeviceCurrentStateAsHK());
|
||||
this.lockService
|
||||
.setCharacteristic(Characteristic.LockCurrentState, this.getDeviceCurrentStateAsHK());
|
||||
}
|
||||
|
||||
// Returns the set of services supported by this object.
|
||||
ISYLockAccessory.prototype.getServices = function() {
|
||||
var informationService = new Service.AccessoryInformation();
|
||||
|
||||
informationService
|
||||
.setCharacteristic(Characteristic.Manufacturer, "SmartHome")
|
||||
.setCharacteristic(Characteristic.Model, this.device.deviceFriendlyName)
|
||||
.setCharacteristic(Characteristic.SerialNumber, this.device.address);
|
||||
|
||||
var lockMechanismService = new Service.LockMechanism();
|
||||
|
||||
this.lockService = lockMechanismService;
|
||||
this.informationService = informationService;
|
||||
|
||||
lockMechanismService
|
||||
.getCharacteristic(Characteristic.LockTargetState)
|
||||
.on('set', this.setTargetLockState.bind(this));
|
||||
|
||||
lockMechanismService
|
||||
.getCharacteristic(Characteristic.LockTargetState)
|
||||
.on('get', this.getTargetLockState.bind(this));
|
||||
|
||||
lockMechanismService
|
||||
.getCharacteristic(Characteristic.LockCurrentState)
|
||||
.on('get', this.getLockCurrentState.bind(this));
|
||||
|
||||
return [informationService, lockMechanismService];
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// LIGHTS
|
||||
// Implements the Light service for homekit based on an underlying isy-js device. Is dimmable or not depending
|
||||
// on if the underlying device is dimmable.
|
||||
|
||||
// Constructs the light accessory. log = homebridge logger, device = isy-js device object being wrapped
|
||||
function ISYLightAccessory(log,device) {
|
||||
ISYAccessoryBaseSetup(this,log,device);
|
||||
this.dimmable = (this.device.deviceType == "DimmableLight");
|
||||
}
|
||||
|
||||
// Handles the identify command
|
||||
ISYLightAccessory.prototype.identify = function(callback) {
|
||||
this.device.sendLightCommand(true, function(result) {
|
||||
this.device.sendLightCommand(false, function(result) {
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Handles request to set the current powerstate from homekit. Will ignore redundant commands.
|
||||
ISYLightAccessory.prototype.setPowerState = function(powerOn,callback) {
|
||||
ISYJSDebugMessage(this,"Setting powerstate to %s", powerOn);
|
||||
if(powerOn != this.device.getCurrentLightState()) {
|
||||
ISYJSDebugMessage(this,"Changing powerstate to "+powerOn);
|
||||
this.device.sendLightCommand(powerOn, function(result) {
|
||||
callback();
|
||||
});
|
||||
} else {
|
||||
ISYJSDebugMessage(this,"Ignoring redundant setPowerState");
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
// Mirrors change in the state of the underlying isj-js device object.
|
||||
ISYLightAccessory.prototype.handleExternalChange = function() {
|
||||
ISYJSDebugMessage(this,"Handling external change for light");
|
||||
this.lightService
|
||||
.setCharacteristic(Characteristic.On, this.device.getCurrentLightState());
|
||||
if(this.device.deviceType == this.device.isy.DEVICE_TYPE_DIMMABLE_LIGHT) {
|
||||
this.lightService
|
||||
.setCharacteristic(Characteristic.Brightness, this.device.getCurrentLightDimState() );
|
||||
}
|
||||
}
|
||||
|
||||
// Handles request to get the current on state
|
||||
ISYLightAccessory.prototype.getPowerState = function(callback) {
|
||||
callback(null,this.device.getCurrentLightState());
|
||||
}
|
||||
|
||||
// Handles request to set the brightness level of dimmable lights. Ignore redundant commands.
|
||||
ISYLightAccessory.prototype.setBrightness = function(level,callback) {
|
||||
ISYJSDebugMessage(this,"Setting brightness to %s", level);
|
||||
if(level != this.device.getCurrentLightDimState()) {
|
||||
ISYJSDebugMessage(this,"Changing Brightness to "+level);
|
||||
this.device.sendLightDimCommand(level, function(result) {
|
||||
callback();
|
||||
});
|
||||
} else {
|
||||
ISYJSDebugMessage(this,"Ignoring redundant setBrightness");
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
// Handles a request to get the current brightness level for dimmable lights.
|
||||
ISYLightAccessory.prototype.getBrightness = function(callback) {
|
||||
callback(null,this.device.getCurrentLightDimState());
|
||||
}
|
||||
|
||||
// Returns the set of services supported by this object.
|
||||
ISYLightAccessory.prototype.getServices = function() {
|
||||
var informationService = new Service.AccessoryInformation();
|
||||
|
||||
informationService
|
||||
.setCharacteristic(Characteristic.Manufacturer, "SmartHome")
|
||||
.setCharacteristic(Characteristic.Model, this.device.deviceFriendlyName)
|
||||
.setCharacteristic(Characteristic.SerialNumber, this.device.address);
|
||||
|
||||
var lightBulbService = new Service.Lightbulb();
|
||||
|
||||
this.informationService = informationService;
|
||||
this.lightService = lightBulbService;
|
||||
|
||||
lightBulbService
|
||||
.getCharacteristic(Characteristic.On)
|
||||
.on('set', this.setPowerState.bind(this));
|
||||
|
||||
lightBulbService
|
||||
.getCharacteristic(Characteristic.On)
|
||||
.on('get', this.getPowerState.bind(this));
|
||||
|
||||
if(this.dimmable) {
|
||||
lightBulbService
|
||||
.addCharacteristic(new Characteristic.Brightness())
|
||||
.on('get', this.getBrightness.bind(this));
|
||||
|
||||
lightBulbService
|
||||
.getCharacteristic(Characteristic.Brightness)
|
||||
.on('set', this.setBrightness.bind(this));
|
||||
}
|
||||
|
||||
return [informationService, lightBulbService];
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// CONTACT SENSOR - ISYDoorWindowSensorAccessory
|
||||
// Implements the ContactSensor service.
|
||||
|
||||
// Constructs a Door Window Sensor (contact sensor) accessory. log = HomeBridge logger, device = wrapped isy-js device.
|
||||
function ISYDoorWindowSensorAccessory(log,device) {
|
||||
ISYAccessoryBaseSetup(this,log,device);
|
||||
this.doorWindowState = false;
|
||||
}
|
||||
|
||||
// Handles the identify command.
|
||||
ISYDoorWindowSensorAccessory.prototype.identify = function(callback) {
|
||||
// Do the identify action
|
||||
callback();
|
||||
}
|
||||
|
||||
// Translates the state of the underlying device object into the corresponding homekit compatible state
|
||||
ISYDoorWindowSensorAccessory.prototype.translateCurrentDoorWindowState = function() {
|
||||
return (this.device.getCurrentDoorWindowState()) ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : Characteristic.ContactSensorState.CONTACT_DETECTED;
|
||||
}
|
||||
|
||||
// Handles the request to get he current door window state.
|
||||
ISYDoorWindowSensorAccessory.prototype.getCurrentDoorWindowState = function(callback) {
|
||||
callback(null,this.translateCurrentDoorWindowState());
|
||||
}
|
||||
|
||||
// Mirrors change in the state of the underlying isj-js device object.
|
||||
ISYDoorWindowSensorAccessory.prototype.handleExternalChange = function() {
|
||||
this.sensorService
|
||||
.setCharacteristic(Characteristic.ContactSensorState, this.translateCurrentDoorWindowState());
|
||||
}
|
||||
|
||||
// Returns the set of services supported by this object.
|
||||
ISYDoorWindowSensorAccessory.prototype.getServices = function() {
|
||||
var informationService = new Service.AccessoryInformation();
|
||||
|
||||
informationService
|
||||
.setCharacteristic(Characteristic.Manufacturer, "SmartHome")
|
||||
.setCharacteristic(Characteristic.Model, this.device.deviceFriendlyName)
|
||||
.setCharacteristic(Characteristic.SerialNumber, this.device.address);
|
||||
|
||||
var sensorService = new Service.ContactSensor();
|
||||
|
||||
this.sensorService = sensorService;
|
||||
this.informationService = informationService;
|
||||
|
||||
sensorService
|
||||
.getCharacteristic(Characteristic.ContactSensorState)
|
||||
.on('get', this.getCurrentDoorWindowState.bind(this));
|
||||
|
||||
return [informationService, sensorService];
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// ELK SENSOR PANEL - ISYElkAlarmPanelAccessory
|
||||
// Implements the SecuritySystem service for an elk security panel connected to the isy system
|
||||
|
||||
// Constructs the alarm panel accessory. log = HomeBridge logger, device = underlying isy-js device being wrapped
|
||||
function ISYElkAlarmPanelAccessory(log,device) {
|
||||
ISYAccessoryBaseSetup(this,log,device);
|
||||
}
|
||||
|
||||
// Handles the identify command
|
||||
ISYElkAlarmPanelAccessory.prototype.identify = function(callback) {
|
||||
callback();
|
||||
}
|
||||
|
||||
// Handles the request to set the alarm target state
|
||||
ISYElkAlarmPanelAccessory.prototype.setAlarmTargetState = function(targetStateHK,callback) {
|
||||
ISYJSDebugMessage(this,"Sending command to set alarm panel state to: "+targetStateHK);
|
||||
var targetState = this.translateHKToAlarmTargetState(targetStateHK);
|
||||
ISYJSDebugMessage(this,"Would send the target state of: "+targetState);
|
||||
if(this.device.getAlarmMode() != targetState) {
|
||||
this.device.sendSetAlarmModeCommand(targetState, function(result) {
|
||||
callback();
|
||||
});
|
||||
} else {
|
||||
ISYJSDebugMessage(this,"Redundant command, already in that state.");
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
// Translates from the current state of the elk alarm system into a homekit compatible state. The elk panel has a lot more
|
||||
// possible states then can be directly represented by homekit so we map them. If the alarm is going off then it is tripped.
|
||||
// If it is arming or armed it is considered armed. Stay maps to the state state, away to the away state, night to the night
|
||||
// state.
|
||||
ISYElkAlarmPanelAccessory.prototype.translateAlarmCurrentStateToHK = function() {
|
||||
var tripState = this.device.getAlarmTripState();
|
||||
var sourceAlarmState = this.device.getAlarmState();
|
||||
var sourceAlarmMode = this.device.getAlarmMode();
|
||||
|
||||
if(tripState >= this.device.ALARM_TRIP_STATE_TRIPPED) {
|
||||
return Characteristic.SecuritySystemCurrentState.ALARM_TRIGGERED;
|
||||
} else if(sourceAlarmState == this.device.ALARM_STATE_NOT_READY_TO_ARM ||
|
||||
sourceAlarmState == this.device.ALARM_STATE_READY_TO_ARM ||
|
||||
sourceAlarmState == this.device.ALARM_STATE_READY_TO_ARM_VIOLATION) {
|
||||
return Characteristic.SecuritySystemCurrentState.DISARMED;
|
||||
} else {
|
||||
if(sourceAlarmMode == this.device.ALARM_MODE_STAY || sourceAlarmMode == this.device.ALARM_MODE_STAY_INSTANT ) {
|
||||
return Characteristic.SecuritySystemCurrentState.STAY_ARM;
|
||||
} else if(sourceAlarmMode == this.device.ALARM_MODE_AWAY || sourceAlarmMode == this.device.ALARM_MODE_VACATION) {
|
||||
return Characteristic.SecuritySystemCurrentState.AWAY_ARM;
|
||||
} else if(sourceAlarmMode == this.device.ALARM_MODE_NIGHT || sourceAlarmMode == this.device.ALARM_MODE_NIGHT_INSTANT) {
|
||||
return Characteristic.SecuritySystemCurrentState.NIGHT_ARM;
|
||||
} else {
|
||||
ISYJSDebugMessage(this,"Setting to disarmed because sourceAlarmMode is "+sourceAlarmMode);
|
||||
return Characteristic.SecuritySystemCurrentState.DISARMED;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Translates the current target state of hthe underlying alarm into the appropriate homekit value
|
||||
ISYElkAlarmPanelAccessory.prototype.translateAlarmTargetStateToHK = function() {
|
||||
var sourceAlarmState = this.device.getAlarmMode();
|
||||
if(sourceAlarmState == this.device.ALARM_MODE_STAY || sourceAlarmState == this.device.ALARM_MODE_STAY_INSTANT ) {
|
||||
return Characteristic.SecuritySystemTargetState.STAY_ARM;
|
||||
} else if(sourceAlarmState == this.device.ALARM_MODE_AWAY || sourceAlarmState == this.device.ALARM_MODE_VACATION) {
|
||||
return Characteristic.SecuritySystemTargetState.AWAY_ARM;
|
||||
} else if(sourceAlarmState == this.device.ALARM_MODE_NIGHT || sourceAlarmState == this.device.ALARM_MODE_NIGHT_INSTANT) {
|
||||
return Characteristic.SecuritySystemTargetState.NIGHT_ARM;
|
||||
} else {
|
||||
return Characteristic.SecuritySystemTargetState.DISARM;
|
||||
}
|
||||
}
|
||||
|
||||
// Translates the homekit version of the alarm target state into the appropriate elk alarm panel state
|
||||
ISYElkAlarmPanelAccessory.prototype.translateHKToAlarmTargetState = function(state) {
|
||||
if(state == Characteristic.SecuritySystemTargetState.STAY_ARM) {
|
||||
return this.device.ALARM_MODE_STAY;
|
||||
} else if(state == Characteristic.SecuritySystemTargetState.AWAY_ARM) {
|
||||
return this.device.ALARM_MODE_AWAY;
|
||||
} else if(state == Characteristic.SecuritySystemTargetState.NIGHT_ARM) {
|
||||
return this.device.ALARM_MODE_NIGHT;
|
||||
} else {
|
||||
return this.device.ALARM_MODE_DISARMED;
|
||||
}
|
||||
}
|
||||
|
||||
// Handles request to get the target alarm state
|
||||
ISYElkAlarmPanelAccessory.prototype.getAlarmTargetState = function(callback) {
|
||||
callback(null,this.translateAlarmTargetStateToHK());
|
||||
}
|
||||
|
||||
// Handles request to get the current alarm state
|
||||
ISYElkAlarmPanelAccessory.prototype.getAlarmCurrentState = function(callback) {
|
||||
callback(null,this.translateAlarmCurrentStateToHK());
|
||||
}
|
||||
|
||||
// Mirrors change in the state of the underlying isj-js device object.
|
||||
ISYElkAlarmPanelAccessory.prototype.handleExternalChange = function() {
|
||||
ISYJSDebugMessage(this,"Source device. Currenty state locally -"+this.device.getAlarmStatusAsText());
|
||||
ISYJSDebugMessage(this,"Got alarm change notification. Setting HK target state to: "+this.translateAlarmTargetStateToHK()+" Setting HK Current state to: "+this.translateAlarmCurrentStateToHK());
|
||||
this.alarmPanelService
|
||||
.setCharacteristic(Characteristic.SecuritySystemTargetState, this.translateAlarmTargetStateToHK());
|
||||
this.alarmPanelService
|
||||
.setCharacteristic(Characteristic.SecuritySystemCurrentState, this.translateAlarmCurrentStateToHK());
|
||||
}
|
||||
|
||||
// Returns the set of services supported by this object.
|
||||
ISYElkAlarmPanelAccessory.prototype.getServices = function() {
|
||||
var informationService = new Service.AccessoryInformation();
|
||||
|
||||
informationService
|
||||
.setCharacteristic(Characteristic.Manufacturer, "SmartHome")
|
||||
.setCharacteristic(Characteristic.Model, this.device.deviceFriendlyName)
|
||||
.setCharacteristic(Characteristic.SerialNumber, this.device.address);
|
||||
|
||||
var alarmPanelService = new Service.SecuritySystem();
|
||||
|
||||
this.alarmPanelService = alarmPanelService;
|
||||
this.informationService = informationService;
|
||||
|
||||
alarmPanelService
|
||||
.getCharacteristic(Characteristic.SecuritySystemTargetState)
|
||||
.on('set', this.setAlarmTargetState.bind(this));
|
||||
|
||||
alarmPanelService
|
||||
.getCharacteristic(Characteristic.SecuritySystemTargetState)
|
||||
.on('get', this.getAlarmTargetState.bind(this));
|
||||
|
||||
alarmPanelService
|
||||
.getCharacteristic(Characteristic.SecuritySystemCurrentState)
|
||||
.on('get', this.getAlarmCurrentState.bind(this));
|
||||
|
||||
return [informationService, alarmPanelService];
|
||||
}
|
||||
|
||||
module.exports.platform = ISYPlatform;
|
||||
module.exports.accessory = ISYFanAccessory;
|
||||
module.exports.accessory = ISYLightAccessory;
|
||||
module.exports.accessory = ISYLockAccessory;
|
||||
module.exports.accessory = ISYOutletAccessory;
|
||||
module.exports.accessory = ISYDoorWindowSensorAccessory;
|
||||
module.exports.accessory = ISYElkAlarmPanelAccessory;
|
||||
33
script/bootstrap
Executable file
33
script/bootstrap
Executable file
@@ -0,0 +1,33 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
if ! test $(which forever)
|
||||
then
|
||||
echo
|
||||
echo "!!!!"
|
||||
echo "You don't have forever installed. You need to install it first."
|
||||
echo
|
||||
echo "Just install it with this command: "
|
||||
echo 'sudo npm install forever -g'
|
||||
echo
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p log
|
||||
|
||||
echo "Installing packages..."
|
||||
if [[ "$OSTYPE" == "linux*" ]]; then
|
||||
echo "This might take a while on a Raspberry Pi..."
|
||||
fi
|
||||
npm install > /dev/null 2>&1
|
||||
|
||||
if [ ! -f config.json ]
|
||||
then
|
||||
echo
|
||||
echo "==> Creating your config. Please edit config.json."
|
||||
echo
|
||||
cp config-sample.json config.json
|
||||
fi
|
||||
|
||||
echo "Finished setting up Homebridge! run it with script/server or install it with script/install."
|
||||
44
script/install
Executable file
44
script/install
Executable file
@@ -0,0 +1,44 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
echo "Installing Homebridge..."
|
||||
|
||||
APP_PATH=`pwd`
|
||||
FOREVER_PATH=`which forever || true`
|
||||
USER_NAME=`whoami`
|
||||
|
||||
|
||||
if [[ "$OSTYPE" == "linux*" ]]; then
|
||||
## install for linux
|
||||
|
||||
# copy SysVinit script to the correct path.
|
||||
sudo cp startup/homebridge /etc/init.d/homebridge
|
||||
# set exec permissions on script.
|
||||
sudo chmod +x /etc/init.d/homebridge
|
||||
|
||||
# Set the current path for where the app lives.
|
||||
sudo sed -i '' -e "s#%PATH%#$APP_PATH#g" /etc/init.d/homebridge
|
||||
# Set the path for where the forever lives.
|
||||
sudo sed -i '' -e "s#%FOREVER_PATH%#$FOREVER_PATH#g" /etc/init.d/homebridge
|
||||
|
||||
# Start it.
|
||||
sudo /etc/init.d/homebridge start
|
||||
|
||||
# tell it to launch at boot
|
||||
sudo update-rc.d homebridge defaults
|
||||
|
||||
elif [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
## install for OS X
|
||||
|
||||
# copy template plist to the correct path.
|
||||
cp startup/org.homebridge.plist ~/Library/LaunchAgents/org.homebridge.plist
|
||||
|
||||
# Set the current user for the script to run as.
|
||||
sed -i '' -e "s#%USER%#$USER_NAME#g" ~/Library/LaunchAgents/org.homebridge.plist
|
||||
# Set the current path for where the app lives.
|
||||
sed -i '' -e "s#%PATH%#$APP_PATH#g" ~/Library/LaunchAgents/org.homebridge.plist
|
||||
|
||||
# Tell launchd to start it.
|
||||
launchctl load -w -F ~/Library/LaunchAgents/org.homebridge.plist
|
||||
fi
|
||||
4
script/restart
Executable file
4
script/restart
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
|
||||
script/uninstall
|
||||
script/install
|
||||
12
script/server
Executable file
12
script/server
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
|
||||
FOREVER_PATH=`which forever`
|
||||
|
||||
test -z "$NODE_ENV" &&
|
||||
export NODE_ENV='development'
|
||||
|
||||
if [ "$NODE_ENV" = "development" ]; then
|
||||
$FOREVER_PATH -f startup/forever/development.json
|
||||
else
|
||||
$FOREVER_PATH start startup/forever/production.json
|
||||
fi
|
||||
22
script/uninstall
Executable file
22
script/uninstall
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "Uninstalling Homebridge..."
|
||||
# make forever stop the app.
|
||||
forever stop homebridge > /dev/null 2>&1
|
||||
|
||||
if [[ "$OSTYPE" == "linux*" ]]; then
|
||||
## uninstall for linux
|
||||
|
||||
# stop the service.
|
||||
sudo /etc/init.d/homebridge stop
|
||||
|
||||
# remove the service from launching at boot.
|
||||
sudo update-rc.d -f homebridge remove
|
||||
elif [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
## uninstall for OS X
|
||||
|
||||
# tell launchd to stop the process.
|
||||
launchctl unload ~/Library/LaunchAgents/org.homebridge.plist
|
||||
# remove the launchd script.
|
||||
rm ~/Library/LaunchAgents/org.homebridge.plist
|
||||
fi
|
||||
8
script/upgrade
Executable file
8
script/upgrade
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
echo "Updating from GitHub..."
|
||||
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
git pull origin $BRANCH
|
||||
|
||||
npm update
|
||||
|
||||
script/restart
|
||||
4
startup/forever/development.json
Normal file
4
startup/forever/development.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"append": true,
|
||||
"script": "app.js"
|
||||
}
|
||||
7
startup/forever/production.json
Normal file
7
startup/forever/production.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"uid": "homebridge",
|
||||
"append": true,
|
||||
"script": "app.js",
|
||||
"outFile": "log/logs.log",
|
||||
"errFile": "log/error.log"
|
||||
}
|
||||
88
startup/homebridge
Normal file
88
startup/homebridge
Normal file
@@ -0,0 +1,88 @@
|
||||
### BEGIN INIT INFO
|
||||
# If you wish the Daemon to be lauched at boot / stopped at shutdown :
|
||||
#
|
||||
# On Debian-based distributions:
|
||||
# INSTALL : update-rc.d scriptname defaults
|
||||
# (UNINSTALL : update-rc.d -f scriptname remove)
|
||||
#
|
||||
# On RedHat-based distributions (CentOS, OpenSUSE...):
|
||||
# INSTALL : chkconfig --level 35 scriptname on
|
||||
# (UNINSTALL : chkconfig --level 35 scriptname off)
|
||||
#
|
||||
# chkconfig: 2345 90 60
|
||||
# Provides: %PATH%/app.js
|
||||
# Required-Start: $remote_fs $syslog
|
||||
# Required-Stop: $remote_fs $syslog
|
||||
# Default-Start: 2 3 4 5
|
||||
# Default-Stop: 0 1 6
|
||||
# Short-Description: forever running %PATH%/app.js
|
||||
# Description: %PATH%/app.js
|
||||
### END INIT INFO
|
||||
#
|
||||
# initd a node app
|
||||
# Based on a script posted by https://gist.github.com/jinze at https://gist.github.com/3748766
|
||||
#
|
||||
|
||||
if [ -e /lib/lsb/init-functions ]; then
|
||||
# LSB source function library.
|
||||
. /lib/lsb/init-functions
|
||||
fi;
|
||||
|
||||
pidFile="/var/run/homebridge.pid"
|
||||
logFile="%PATH%/log/logs.log"
|
||||
|
||||
command="node"
|
||||
nodeApp="%PATH%/app.js"
|
||||
foreverApp="%FOREVER_PATH%"
|
||||
|
||||
start() {
|
||||
echo "Starting $nodeApp"
|
||||
|
||||
# Notice that we change the PATH because on reboot
|
||||
# the PATH does not include the path to node.
|
||||
# Launching forever with a full path
|
||||
# does not work unless we set the PATH.
|
||||
PATH=/usr/local/bin:$PATH
|
||||
export NODE_ENV=production
|
||||
#PORT=80
|
||||
$foreverApp start --pidFile $pidFile -l $logFile -a -d -c "$command" $nodeApp
|
||||
RETVAL=$?
|
||||
}
|
||||
|
||||
restart() {
|
||||
echo -n "Restarting $nodeApp"
|
||||
$foreverApp restart $nodeApp
|
||||
RETVAL=$?
|
||||
}
|
||||
|
||||
stop() {
|
||||
echo -n "Shutting down $nodeApp"
|
||||
$foreverApp stop $nodeApp
|
||||
RETVAL=$?
|
||||
}
|
||||
|
||||
status() {
|
||||
echo -n "Status $nodeApp"
|
||||
$foreverApp list
|
||||
RETVAL=$?
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
start
|
||||
;;
|
||||
stop)
|
||||
stop
|
||||
;;
|
||||
status)
|
||||
status
|
||||
;;
|
||||
restart)
|
||||
restart
|
||||
;;
|
||||
*)
|
||||
echo "Usage: {start|stop|status|restart}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
exit $RETVAL
|
||||
41
startup/org.homebridge.plist
Normal file
41
startup/org.homebridge.plist
Normal file
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>org.homebridge</string>
|
||||
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>PATH</key>
|
||||
<string>/usr/local/bin/:/usr/bin:$PATH</string>
|
||||
<key>NODE_ENV</key>
|
||||
<string>production</string>
|
||||
</dict>
|
||||
|
||||
<key>Program</key>
|
||||
<string>script/server</string>
|
||||
|
||||
<key>AbandonProcessGroup</key>
|
||||
<false/>
|
||||
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
|
||||
<key>KeepAlive</key>
|
||||
<dict>
|
||||
<key>SuccessfulExit</key>
|
||||
<false/>
|
||||
</dict>
|
||||
|
||||
<key>WorkingDirectory</key>
|
||||
<string>%PATH%</string>
|
||||
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/Users/%USER%/Library/Logs/homebridge.log</string>
|
||||
|
||||
<key>StandardOutPath</key>
|
||||
<string>/Users/%USER%/Library/Logs/homebridge.log</string>
|
||||
|
||||
</dict>
|
||||
</plist>
|
||||
Reference in New Issue
Block a user