145 Commits

Author SHA1 Message Date
Nick Farina
7a561f8754 Move slackin deployment to mitigate NodeJS security issue 2017-07-14 15:06:21 -07:00
Khaos Tian
d9d58855cd 0.4.22 2017-07-02 14:46:27 -07:00
Khaos Tian
3acded3ea2 Bump hap-nodejs version.
This version addressed connectivity problem with node 8.0
2017-07-02 14:46:23 -07:00
Khaos Tian
8a35d75cb5 0.4.21 2017-07-01 22:02:04 -06:00
Khaos Tian
c70cb8be07 Bump hap-nodejs version 2017-07-01 22:01:56 -06:00
Olivier Guerriat
7406f6c9f3 Added "limitations" to the README (#1338) 2017-06-10 12:09:09 -07:00
Brent Unkrich
cbae68afdd updated formatting (#1284)
took out extra spaces and fixed capitalization.
2017-05-11 19:23:08 -07:00
Khaos Tian
495143e1a1 Update hap-nodejs version 2017-05-01 23:30:05 -07:00
Khaos Tian
7585cf3f83 0.4.19
Bump hap-nodejs version to add support for programmable switch label.
2017-04-16 17:35:44 -07:00
Khaos Tian
d521755a49 0.4.18
Add support for updated programmable service profile.
2017-04-16 13:32:00 -07:00
Khaos Tian
6f4ce80aea Bump hap-nodejs version. 2017-04-07 22:06:17 -07:00
Stephan Esch
efda0fac11 Added serverVersion to API; bumped API version to 2.2 (#1064)
* Added serverVersion to API; bumped API version to 2.1.1

* Fixed version number to 2.2
2017-01-13 09:48:56 -08:00
Khaos Tian
600760884d Address a metadata issue that blocks camera support. 2016-12-13 15:50:19 +08:00
Khaos Tian
3a4830ee57 Update hap-nodejs to support new services in iOS 10.2 2016-12-13 11:05:30 +08:00
Khaos Tian
582e00a6ef 0.4.12 2016-12-10 14:23:53 -08:00
Khaos Tian
debba05d2f Merge pull request #967 from andig/patch-1
Upgrade hap-nodejs
2016-12-02 01:20:34 -08:00
andig
027a693c0d Update package.json 2016-12-02 10:19:58 +01:00
andig
b5e1fc52a8 Upgrade hap-nodejs 2016-12-02 10:18:49 +01:00
Khaos Tian
f17fe59590 Fix an warning in nodejs v7 2016-11-29 18:42:17 -08:00
Khaos Tian
69e3ed5ee4 Merge pull request #491 from straccio/master
Added pluginPath based on npm
2016-11-28 13:21:27 -08:00
Khaos Tian
3354842e81 Merge pull request #917 from crzcrz/master
Use chalk instead of hardcoded ANSI codes
2016-11-28 13:17:35 -08:00
straccio
e5e2a400ec Merge remote-tracking branch 'nfarina/master'
# Conflicts:
#	lib/plugin.js
2016-11-28 13:36:00 +01:00
Khaos Tian
4884087041 0.4.10 2016-11-27 15:24:43 -08:00
Khaos Tian
e1867b2bc0 Improve error handling. 2016-11-27 15:24:27 -08:00
Khaos Tian
4d0f9d86f6 Merge pull request #941 from djMax/master
Allow externally specified config
2016-11-21 12:04:57 -08:00
Max Metral
ecda18029f Allow externally specified config 2016-11-21 14:58:39 -05:00
straccio
7acac442a8 Merge remote-tracking branch 'nfarina/master' 2016-11-21 09:12:23 +01:00
Khaos Tian
efc570e5a9 Update example plugin
Add comment for service name.
2016-11-18 00:33:36 -08:00
straccio
7955049337 Merge remote-tracking branch 'nfarina/master' 2016-11-14 11:24:01 +01:00
Stanislav Kljuhhin
8c6cb53dcb Use chalk instead of hardcoded ANSI codes
Improves the readability of the source code and prevents the raw ANSI codes from appearing in the log, if ran as a systemd service.
2016-11-13 16:29:10 +01:00
Khaos Tian
b6cfe3ba7c 0.4.9 2016-11-09 18:11:57 -08:00
Khaos Tian
f836d4a42c Bump hap-nodejs version to fix #791 2016-11-09 18:11:41 -08:00
straccio
f893322887 Merge remote-tracking branch 'nfarina/master' 2016-11-04 10:05:11 +01:00
Khaos Tian
63ab1025e9 Update hap-nodejs to allow homebridge work with nodejs v4.x 2016-11-03 12:20:46 -07:00
straccio
9a25a363d4 Merge remote-tracking branch 'nfarina/master' 2016-11-03 17:05:53 +01:00
Khaos Tian
dc43d0b7c4 Update hap-nodejs
This version requires node v5.10.0 or later.
2016-11-01 18:33:59 -07:00
straccio
1513e5398f Merge remote-tracking branch 'nfarina/master' 2016-10-11 15:52:58 +02:00
Khaos Tian
7c3543ba61 Merge pull request #839 from rxseger/collision-name
Fix camera name in collision error message
2016-10-09 17:14:29 -07:00
rxseger
5adb5f3282 Fix camera name in collision error message 2016-10-09 23:36:53 +00:00
straccio
ffe343c65f Merge remote-tracking branch 'nfarina/master' 2016-10-03 09:24:28 +02:00
Khaos Tian
fedd341970 0.4.6 2016-10-01 16:48:05 -07:00
Khaos Tian
c7c9aa0150 Bump hap-nodejs version. 2016-10-01 16:48:01 -07:00
straccio
a2baa93801 Merge remote-tracking branch 'nfarina/master' 2016-09-29 09:00:45 +02:00
Khaos Tian
e3e08414f6 0.4.5 2016-09-28 17:11:01 -07:00
Khaos Tian
ea9df45d2d Add back the ability to designate the port homebridge is running. 2016-09-28 17:10:52 -07:00
Khaos Tian
7425f8beca 0.4.4 2016-09-28 12:40:35 -07:00
straccio
8192fc2672 Merge remote-tracking branch 'nfarina/master' 2016-09-28 08:19:30 +02:00
Khaos Tian
dbb7b89cf9 0.4.3 2016-09-27 17:01:08 -07:00
Khaos Tian
4f000485db Bump hap-nodejs again... 2016-09-27 17:01:00 -07:00
Khaos Tian
c0884f484e 0.4.2 2016-09-27 12:36:29 -07:00
Khaos Tian
363c997058 Bump hap-nodejs version 2016-09-27 12:36:23 -07:00
straccio
332385d605 Merge remote-tracking branch 'nfarina/master' 2016-09-27 08:38:18 +02:00
Khaos Tian
0ccd80c8e7 0.4.1 2016-09-26 20:32:06 -07:00
Khaos Tian
ef8fe5ced2 Update hap-nodejs version to fix side load problem 2016-09-26 20:31:55 -07:00
Khaos Tian
4a531ede5c Merge pull request #805 from nfarina/ip-cam
IP Camera support for homebridge
2016-09-26 14:34:35 -07:00
Khaos Tian
ff35ece65c 0.4.0 2016-09-26 14:22:22 -07:00
Khaos Tian
66ea6e640d Minor Tweak 2016-09-26 14:15:12 -07:00
Khaos Tian
ecd06d7239 Bump api version 2016-09-26 13:19:50 -07:00
Khaos Tian
c89ff97ac5 init design 2016-09-26 13:01:29 -07:00
Khaos Tian
ceec4c74fd Merge pull request #803 from nfarina/use-hap-0.4
Use hap-nodejs 0.4.0
2016-09-26 12:02:32 -07:00
Jon Maddox
925c1630c4 bump hap-nodejs to 0.4.0 2016-09-26 10:46:21 -04:00
straccio
41c53f8f10 Merge remote-tracking branch 'nfarina/master' 2016-09-20 09:17:15 +02:00
Khaos Tian
4eabc4ad52 Merge pull request #593 from PatchworkBoy/patch-1
Ref #582
2016-09-18 21:37:49 -07:00
Khaos Tian
c0859a29d3 Merge pull request #764 from alessiodionisi/new-bridge-configs
New bridge configs
2016-09-18 21:37:18 -07:00
Khaos Tian
c15707e875 Merge pull request #778 from nrobinson2000/patch-1
Some changes to the README
2016-09-18 21:36:42 -07:00
Nathan Robinson
8c476b45a0 Update README.md 2016-09-18 21:13:38 -04:00
Nick Farina
f49229d73c Update README to reflect the new iOS 10 Home app. 2016-09-15 21:05:39 -07:00
Khaos Tian
fbccc031f4 Merge pull request #770 from kcjonson/patch-1
Update README.md
2016-09-15 14:57:11 -07:00
Kevin Jonson
d70fa741d8 Update README.md 2016-09-15 14:07:09 -07:00
Alessio Dionisi
4740bf1fc5 New bridge configs
"bridge": {
    "manufacturer": "Manufacturer here",
    "serialNumber": "Serial number here",
    "model": "Model here"
}
2016-09-13 13:54:37 +02:00
Khaos Tian
da57b29972 Merge pull request #758 from cflurin/master
Warning config.json not found
2016-09-06 08:28:36 -07:00
cflurin
5944365bc6 Warning config.json not found 2016-09-06 14:49:53 +02:00
Khaos Tian
a8908fd9b8 Merge pull request #714 from EdJoPaTo/platformLog
optimized logging of the SamplePlatform example
2016-07-30 18:41:01 -07:00
Khaos Tian
8ef7e62094 Merge pull request #715 from EdJoPaTo/multiAccessories
fixed SamplePlatform crashes
2016-07-30 18:40:35 -07:00
Edgar To
15c8eaaf29 use of the own log method instead of the general console.log(); added accessory.displayName to the log output if possible/ useful 2016-07-24 22:43:21 +02:00
Edgar To
e6648375c7 different add times will create different UUIDs -> no crash with the second add call 2016-07-24 22:38:05 +02:00
straccio
4251b15291 Merge remote-tracking branch 'nfarina/master' 2016-05-31 08:52:59 +02:00
Khaos Tian
a52bc9e437 Merge pull request #642 from alexbain/patch-1
Add note about --unsafe-perm
2016-05-22 18:35:12 -07:00
Alex Bain
b78c081cd4 Add note about --unsafe-perm
I received the following error when trying to install homebridge on a RaspberryPi. Assuming I'm not the first or the last to see this error, I thought adding a note to README about the --unsafe-perm flag would aid others.
2016-05-22 18:01:32 -07:00
straccio
3f2cd08383 Merge remote-tracking branch 'nfarina/master' 2016-05-02 16:44:42 +02:00
Khaos Tian
87050a2267 update hap-nodejs dependency for node v6 2016-04-29 10:23:15 -07:00
straccio
c8cb0731ff Merge remote-tracking branch 'nfarina/master' 2016-04-11 09:45:54 +02:00
Khaos Tian
35dfaabc69 update hap-nodejs dependency 2016-04-08 23:51:03 -07:00
Khaos Tian
77ce39e157 0.3.3 2016-04-08 23:50:34 -07:00
Khaos Tian
0af8a43dc9 Expose platform accessory category to hap-nodejs
Fix an error in plugin config look up
2016-04-08 23:50:18 -07:00
Marci
f203a2ac6f Ref #582
Ref#582 Add ‘--unsafe-perm’ flag to npm install command to get round all the .gyp warns etc.
2016-03-31 15:50:50 +01:00
straccio
39af2ebbef Merge remote-tracking branch 'nfarina/master'
# Conflicts:
#	lib/logger.js
2016-03-21 08:43:36 +01:00
Khaos Tian
620c8473b8 #572 2016-03-20 22:12:47 -07:00
Khaos Tian
b2f476f833 0.3.2 2016-03-20 21:53:01 -07:00
Khaos Tian
c6d2f889fc Update hap-nodejs version to solve #575 2016-03-20 21:52:56 -07:00
straccio
f73783787d Merge remote-tracking branch 'nfarina/master' 2016-03-11 08:18:56 +01:00
Khaos Tian
2ea2052769 0.3.1 2016-03-10 20:19:47 -08:00
Khaos Tian
64e8c83d9c Update hap-nodejs 2016-03-10 20:19:42 -08:00
straccio
b94c3caa3b Changed color for logging timestamp to white. 2016-03-08 15:59:44 +01:00
straccio
1a710badef Merge remote-tracking branch 'nfarina/master' 2016-03-08 08:45:04 +01:00
straccio
73fdec5928 Revert to upstream/master, no need to skip cached accessories 2016-03-08 08:44:52 +01:00
Nick Farina
13333999f3 Update README.md 2016-03-07 14:43:16 -08:00
Nick Farina
87c48d7267 Update README.md 2016-03-07 14:42:28 -08:00
Nick Farina
9b42fafdaf Add README example for plugin development 2016-03-07 11:49:13 -08:00
straccio
911f088df9 A way to skip cached accessories from being loaded in HAP 2016-03-07 17:32:18 +01:00
straccio
6fade3c3cc Merge remote-tracking branch 'nfarina/master' 2016-03-07 17:28:41 +01:00
Khaos Tian
842ec105be Merge pull request #554 from Danimal4326/master
Prepend date/time to logger messages
2016-03-03 10:29:29 -08:00
Danimal4326
df8508a38f Prepend date/time to logger messages 2016-03-03 12:24:54 -06:00
straccio
191c75c281 Merge remote-tracking branch 'upstream/master' 2016-03-02 08:58:03 +01:00
straccio
1fb58be2b9 ignore idea 2016-03-02 08:47:15 +01:00
Khaos Tian
9d7c1de9dd Update readme to reflect new API system 2016-03-01 18:41:29 -08:00
Khaos Tian
195255bf0d 0.3.0 2016-03-01 18:34:15 -08:00
Khaos Tian
6b182fc4e7 Merge pull request #497 from nfarina/plugin-2
Plugin 2.0
2016-03-01 18:33:27 -08:00
Khaos Tian
c7b2500518 Prepare for merge 2016-03-01 18:28:41 -08:00
Khaos Tian
1f1030766a Update sample plugin 2016-03-01 18:26:40 -08:00
Khaos Tian
8cb22efb83 Add a example to use "identify" event 2016-03-01 00:25:05 -08:00
straccio
ca66cc3499 Merge branch 'master' of https://github.com/straccio/homebridge 2016-02-29 09:06:32 +01:00
straccio
6ae2a19d37 missed ";" 2016-02-29 09:05:42 +01:00
straccio
ffe4232c3b Added pluginPath based on npm 2016-02-29 09:05:42 +01:00
Khaos Tian
f6df85695d Forward identity only if plugin cares about event 2016-02-28 18:19:46 -08:00
Khaos Tian
32e776203f Forward "identify" event 2016-02-28 18:17:46 -08:00
Khaos Tian
c3c2f8815d Merge branch 'master' into plugin-2 2016-02-22 16:52:02 -08:00
Khaos Tian
6500912f54 0.2.19 2016-02-22 16:50:52 -08:00
Khaos Tian
2e2c8eb207 Update hap-nodejs dependency for iOS 9.3 2016-02-22 16:50:33 -08:00
straccio
fa9561d98a missed ";" 2016-02-22 10:01:25 +01:00
straccio
16a29f302d Merge remote-tracking branch 'upstream/master' 2016-02-22 08:53:35 +01:00
Khaos Tian
012005ddc7 Save cached accessories to persist storage when shutting down homebridge. 2016-02-18 14:50:55 -08:00
Khaos Tian
27ffd6e944 Merge branch 'master' into plugin-2 2016-02-18 14:49:05 -08:00
Khaos Tian
0b28387cb1 Merge pull request #532 from torarnv/handle-shutdown-signals
Handle SIGINT and SIGTERM to enable clean shutdown of Homebridge
2016-02-18 14:47:41 -08:00
Khaos Tian
815ea7abea Track setup session termination 2016-02-17 17:45:47 -08:00
Tor Arne Vestbø
cf80e4f2da Handle SIGINT and SIGTERM to enable clean shutdown of Homebridge
For now we terminate the process, but in the future we may tell the
server to stop, which may possibly include some teardown logic.

Handling these signals also make it easier to put Homebridge inside
a docker container, as docker uses SIGTERM to tell a container process
to stop, and passes SIGINT when attached to the container and receiving
a Ctrl+C.
2016-02-18 00:36:56 +01:00
Khaos Tian
40266af8b2 Add the ability to remove services/characteristics 2016-02-17 13:18:25 -08:00
Nick Farina
57beabf0b4 Add comment 2016-02-17 10:43:54 -08:00
Khaos Tian
d3c77a4cda Merge branch 'master' into plugin-2 2016-02-17 10:41:32 -08:00
Khaos Tian
8e360491cf Update hap-nodejs to 0.2.3 2016-02-15 19:22:43 -08:00
Khaos Tian
e546440575 Update hap-nodejs to 0.2.2 2016-02-13 22:44:22 -08:00
Khaos Tian
902fdded65 Address the problem that callback get invoked with wrong signature 😅 2016-02-13 22:21:54 -08:00
Khaos Tian
8de375a4b0 Fix the issue with transaction ID
Update hap-nodejs to fix #497-183825263
2016-02-13 21:57:11 -08:00
Khaos Tian
c02e212b4c bump hap-nodejs version 2016-02-08 14:51:34 -08:00
Khaos Tian
7436be9b44 Add example to update reachability 2016-02-08 14:50:50 -08:00
Khaos Tian
2ad7932fbc Merge pull request #515 from snowdd1/patch-1
Update platformAccessory.js
2016-02-08 14:49:13 -08:00
Raoul
7dd8e12791 Update platformAccessory.js
Some more copy-and-paste errors in the same line.
Was wondering why my platform was failing, but never looked to that simple lines of code :-(
2016-02-08 23:06:32 +01:00
Raoul
c93b0b0df1 Update platformAccessory.js
Small type, big result.
2016-02-08 22:18:54 +01:00
Khaos Tian
b49fd2d6a5 Merge pull request #513 from snowdd1/patch-1
new getServiceByUUIDAndSubtype(UUID, subtype) function
2016-02-07 15:47:46 -08:00
Raoul
9c8812da70 new getServiceByUUIDAndSubtype(UUID, subtype) function
Some platforms may have accessories that contain more than one service of a given type, such as multiple lightbulbs.
2016-02-08 00:01:44 +01:00
Nick Farina
9e6bf028ba Fix license field for Node 2016-02-04 13:34:53 -08:00
Khaos Tian
aebd152ff9 Reverse .gitignore 2016-01-30 21:55:37 -08:00
Khaos Tian
5b9c5192fe add SamplePlatform 2016-01-30 21:55:12 -08:00
Khaos Tian
e1334c5196 Init Plugin 2.0 2016-01-30 18:36:55 -08:00
straccio
40fc7acbed Added pluginPath based on npm 2016-01-27 07:40:26 +01:00
18 changed files with 1255 additions and 188 deletions

4
.gitignore vendored
View File

@@ -9,4 +9,6 @@ npm-debug.log
# Ignore any extra plugins in the example directory that aren't in Git already # Ignore any extra plugins in the example directory that aren't in Git already
# (this is a sandbox for the user) # (this is a sandbox for the user)
example-plugins example-plugins
.idea

View File

@@ -1,5 +1,5 @@
[![Slack Status](https://homebridge-slackin.herokuapp.com/badge.svg)](https://homebridge-slackin.herokuapp.com) [![Slack Status](https://homebridge-slackin.herokuapp.com/badge.svg)](https://slackin-xiwztisllv.now.sh)
# Homebridge # Homebridge
@@ -29,7 +29,11 @@ You can also chat with us in our nascent [Slack instance](http://homebridge-slac
Homebridge is published through [NPM](https://www.npmjs.com/package/homebridge) and should be installed "globally" by typing: Homebridge is published through [NPM](https://www.npmjs.com/package/homebridge) and should be installed "globally" by typing:
sudo npm install -g homebridge sudo npm install -g --unsafe-perm homebridge
You may need to use the `--unsafe-perm` flag if you receive an error similar to this:
gyp WARN EACCES user "root" does not have permission to access the dev dir "/root/.node-gyp/5.5.0"
Now you should be able to run Homebridge: Now you should be able to run Homebridge:
@@ -45,9 +49,9 @@ Once you've installed a Plugin or two, you can run Homebridge again:
However, Homebridge won't do anything until you've created a `config.json` file containing your accessories and/or platforms. You can start by copying and modifying the included `config-sample.json` file which includes declarations for some example accessories and platforms. Each Plugin will have its own expected configuration; the documentation for Plugins should give you some real-world examples for that plugin. However, Homebridge won't do anything until you've created a `config.json` file containing your accessories and/or platforms. You can start by copying and modifying the included `config-sample.json` file which includes declarations for some example accessories and platforms. Each Plugin will have its own expected configuration; the documentation for Plugins should give you some real-world examples for that plugin.
**NOTE**: Your `config.json` file MUST live in your home directory inside `.homebridge`. The full error message will contain the exact path where your config is expected to be found. **NOTE**: Your `config.json` file MUST be inside of `.homebridge`, which is inside of your home folder. On macOS and Linux, the full path for your `config.json` would be `~/.homebridge/config.json`. Any error messages will contain the exact path where your config is expected to be found.
**REALLY IMPORTANT**: You must use a "plain text" editor to create or modify `config.json`. Do NOT use apps like TextEdit on Mac or Wordpad on Windows; these apps will corrupt the formatting of the file in hard-to-debug ways. I suggest using the free [Atom text editor](http://atom.io). **REALLY IMPORTANT**: You must use a "plain text" editor to create or modify `config.json`. Do NOT use apps like TextEdit on Mac or Wordpad on Windows. Apps like these will corrupt the formatting of the file in hard-to-debug ways, making improper `"` signs is an example. I suggest using the free [Atom text editor](http://atom.io).
Once you've added your config file, you should be able to run Homebridge again: Once you've added your config file, you should be able to run Homebridge again:
@@ -79,15 +83,11 @@ You can explore all available plugins at the NPM website by [searching for the k
# Adding Homebridge to iOS # Adding Homebridge 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. HomeKit itself is actually not an app; it's a "database" similar to HealthKit and PassKit. Where HealthKit has the companion _Health_ app and PassKit has _Passbook_, HomeKit has the _Home_ app, introduced with iOS 10.
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. If you are a member of the iOS developer program, you might also find Apple's [HomeKit Catalog](https://developer.apple.com/library/ios/samplecode/HomeKitCatalog/Introduction/Intro.html) app to be useful, as it provides straightforward and comprehensive management of all HomeKit database "objects".
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. Using the Home app (or most other HomeKit apps), you should be able to add the single accessory "Homebridge", assuming that you're still running Homebridge and you're on the same Wifi network. Adding this accessory will automatically add all accessories and platforms defined in `config.json`.
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).
Once you've gotten a HomeKit app running on your iOS device, it should "discover" the single accessory "Homebridge", assuming that you're still running Homebridge and you're on the same Wifi network. Adding this accessory will automatically add all accessories and platforms defined in `config.json`.
When you attempt to add Homebridge, it will ask for a "PIN code". The default code is `031-45-154` (but this can be changed, see `config-sample.json`). 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`).
@@ -101,18 +101,32 @@ One final thing to remember is that Siri will almost always prefer its default p
We don't have a lot of documentation right now for creating plugins, but there are many existing plugins you can study. We don't have a lot of documentation right now for creating plugins, but there are many existing plugins you can study.
The best place to start is the included [Example Plugins](https://github.com/nfarina/homebridge/tree/master/example-plugins). Right now this contains a single plugin that registers a fake door lock Accessory. This will show you how to use the Homebridge Plugin API. The best place to start is the included [Example Plugins](https://github.com/nfarina/homebridge/tree/master/example-plugins). Right now this contains a single plugin that registers a platform that offers fake light accessories. This will show you how to use the Homebridge Plugin API.
For more example on how to construct HomeKit Services and Characteristics, see the many Accessories in the [Legacy Plugins](https://github.com/nfarina/homebridge-legacy-plugins/tree/master/accessories) repository. For more example on how to construct HomeKit Services and Characteristics, see the many Accessories in the [Legacy Plugins](https://github.com/nfarina/homebridge-legacy-plugins/tree/master/accessories) repository.
You can also view the [full list of supported HomeKit Services and Characteristics in the HAP-NodeJS protocol repository](https://github.com/KhaosT/HAP-NodeJS/blob/master/lib/gen/HomeKitTypes.js). You can also view the [full list of supported HomeKit Services and Characteristics in the HAP-NodeJS protocol repository](https://github.com/KhaosT/HAP-NodeJS/blob/master/lib/gen/HomeKitTypes.js).
There isn't currently an example for how to publish a Platform (which allows the user to bridge many discovered devices at once, like a house full of smart lightbulbs), but the process is almost identical to registering an Accessory. Simply modify the example `index.js` in [homebridge-lockitron](https://github.com/nfarina/homebridge/tree/master/example-plugins/homebridge-lockitron) to say something like: And you can find an example plugin that publishes an individual accessory at [here](https://github.com/nfarina/homebridge/tree/6500912f54a70ff479e63e2b72760ab589fa558a/example-plugins/homebridge-lockitron).
homebridge.registerPlatform("homebridge-myplugin", "MyPlatform", MyPlatform);
See more examples on how to create Platform classes in the [Legacy Plugins](https://github.com/nfarina/homebridge-legacy-plugins/tree/master/platforms) repository. See more examples on how to create Platform classes in the [Legacy Plugins](https://github.com/nfarina/homebridge-legacy-plugins/tree/master/platforms) repository.
# Plugin Development
When writing your plugin, you'll want Homebridge to load it from your development directory instead of publishing it to `npm` each time. You can tell Homebridge to look for your plugin at a specific location using the command-line parameter `-P`. For example, if you are in the Homebridge directory (as checked out from Github), you might type:
```sh
DEBUG=* ./bin/homebridge -D -P ../my-great-plugin/
```
This will start up Homebridge and load your in-development plugin from a nearby directory. Note that you can also direct Homebridge to load your configuration from somewhere besides the default `~/.homebridge`, for example:
```sh
DEBUG=* ./bin/homebridge -D -U ~/.homebridge-dev -P ../my-great-plugin/
```
This is very useful when you are already using your development machine to host a "real" Homebridge instance (with all your accessories) that you don't want to disturb.
# Common Issues # Common Issues
### My iOS App Can't Find Homebridge ### My iOS App Can't Find Homebridge
@@ -136,6 +150,11 @@ The following errors are experienced when starting Homebridge and can be safely
*** WARNING *** For more information see http://0pointerde/avahi-compat?s=libdns_sd&e=nodejs&f=DNSServiceRegister *** WARNING *** For more information see http://0pointerde/avahi-compat?s=libdns_sd&e=nodejs&f=DNSServiceRegister
``` ```
### Limitations
* One installation of Homebridge can only expose 100 accessories due to a HomeKit limit. You can however run multiple Homebridge instances by pointing them to different config and persistence paths (see issue #827).
* Once an accessory has been added to the Home app, changing its name via Homebridge won't be automatically reflected in iOS. You must change it via the Home app as well.
# Why Homebridge? # Why Homebridge?
Technically, the device manufacturers should be the ones implementing the HomeKit API. And I'm sure they will - eventually. When they do, this project will be obsolete, and I hope that happens soon. In the meantime, Homebridge is a fun way to get a taste of the future, for those who just can't bear to wait until "real" HomeKit devices are on the market. Technically, the device manufacturers should be the ones implementing the HomeKit API. And I'm sure they will - eventually. When they do, this project will be obsolete, and I hope that happens soon. In the meantime, Homebridge is a fun way to get a taste of the future, for those who just can't bear to wait until "real" HomeKit devices are on the market.

View File

@@ -1,3 +0,0 @@
This is an example plugin for homebridge. It is a fully-working implementation of a Lockitron door lock accessory.
Remember to run `npm install` in this directory in order to install the dependencies needed by this plugin. If a user is installing your plugin from npm, this will be done automatically for them.

View File

@@ -1,82 +0,0 @@
var request = require("request");
var Service, Characteristic;
module.exports = function(homebridge) {
Service = homebridge.hap.Service;
Characteristic = homebridge.hap.Characteristic;
homebridge.registerAccessory("homebridge-lockitron", "Lockitron", LockitronAccessory);
}
function LockitronAccessory(log, config) {
this.log = log;
this.name = config["name"];
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...");
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";
this.log("Set state to %s", lockitronState);
request.put({
url: "https://api.lockitron.com/v2/locks/"+this.lockID,
qs: { access_token: this.accessToken, state: lockitronState }
}, 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));
}
LockitronAccessory.prototype.getServices = function() {
return [this.service];
}

View File

@@ -1,23 +0,0 @@
{
"name": "homebridge-lockitron",
"version": "0.0.1",
"description": "Lockitron plugin for homebridge: https://github.com/nfarina/homebridge",
"license": "ISC",
"keywords": [
"homebridge-plugin"
],
"repository": {
"type": "git",
"url": "git://github.com/example/homebridge-lockitron.git"
},
"bugs": {
"url": "http://github.com/example/homebridge-lockitron/issues"
},
"engines": {
"node": ">=0.12.0",
"homebridge": ">=0.2.0"
},
"dependencies": {
"request": "^2.65.0"
}
}

View File

@@ -0,0 +1,219 @@
var http = require('http');
var Accessory, Service, Characteristic, UUIDGen;
module.exports = function(homebridge) {
console.log("homebridge API version: " + homebridge.version);
// Accessory must be created from PlatformAccessory Constructor
Accessory = homebridge.platformAccessory;
// Service and Characteristic are from hap-nodejs
Service = homebridge.hap.Service;
Characteristic = homebridge.hap.Characteristic;
UUIDGen = homebridge.hap.uuid;
// For platform plugin to be considered as dynamic platform plugin,
// registerPlatform(pluginName, platformName, constructor, dynamic), dynamic must be true
homebridge.registerPlatform("homebridge-samplePlatform", "SamplePlatform", SamplePlatform, true);
}
// Platform constructor
// config may be null
// api may be null if launched from old homebridge version
function SamplePlatform(log, config, api) {
log("SamplePlatform Init");
var platform = this;
this.log = log;
this.config = config;
this.accessories = [];
this.requestServer = http.createServer(function(request, response) {
if (request.url === "/add") {
this.addAccessory(new Date().toISOString());
response.writeHead(204);
response.end();
}
if (request.url == "/reachability") {
this.updateAccessoriesReachability();
response.writeHead(204);
response.end();
}
if (request.url == "/remove") {
this.removeAccessory();
response.writeHead(204);
response.end();
}
}.bind(this));
this.requestServer.listen(18081, function() {
platform.log("Server Listening...");
});
if (api) {
// Save the API object as plugin needs to register new accessory via this object.
this.api = api;
// Listen to event "didFinishLaunching", this means homebridge already finished loading cached accessories
// Platform Plugin should only register new accessory that doesn't exist in homebridge after this event.
// Or start discover new accessories
this.api.on('didFinishLaunching', function() {
platform.log("DidFinishLaunching");
}.bind(this));
}
}
// Function invoked when homebridge tries to restore cached accessory
// Developer can configure accessory at here (like setup event handler)
// Update current value
SamplePlatform.prototype.configureAccessory = function(accessory) {
this.log(accessory.displayName, "Configure Accessory");
var platform = this;
// set the accessory to reachable if plugin can currently process the accessory
// otherwise set to false and update the reachability later by invoking
// accessory.updateReachability()
accessory.reachable = true;
accessory.on('identify', function(paired, callback) {
platform.log(accessory.displayName, "Identify!!!");
callback();
});
if (accessory.getService(Service.Lightbulb)) {
accessory.getService(Service.Lightbulb)
.getCharacteristic(Characteristic.On)
.on('set', function(value, callback) {
platform.log(accessory.displayName, "Light -> " + value);
callback();
});
}
this.accessories.push(accessory);
}
//Handler will be invoked when user try to config your plugin
//Callback can be cached and invoke when nessary
SamplePlatform.prototype.configurationRequestHandler = function(context, request, callback) {
this.log("Context: ", JSON.stringify(context));
this.log("Request: ", JSON.stringify(request));
// Check the request response
if (request && request.response && request.response.inputs && request.response.inputs.name) {
this.addAccessory(request.response.inputs.name);
// Invoke callback with config will let homebridge save the new config into config.json
// Callback = function(response, type, replace, config)
// set "type" to platform if the plugin is trying to modify platforms section
// set "replace" to true will let homebridge replace existing config in config.json
// "config" is the data platform trying to save
callback(null, "platform", true, {"platform":"SamplePlatform", "otherConfig":"SomeData"});
return;
}
// - UI Type: Input
// Can be used to request input from user
// User response can be retrieved from request.response.inputs next time
// when configurationRequestHandler being invoked
var respDict = {
"type": "Interface",
"interface": "input",
"title": "Add Accessory",
"items": [
{
"id": "name",
"title": "Name",
"placeholder": "Fancy Light"
}//,
// {
// "id": "pw",
// "title": "Password",
// "secure": true
// }
]
}
// - UI Type: List
// Can be used to ask user to select something from the list
// User response can be retrieved from request.response.selections next time
// when configurationRequestHandler being invoked
// var respDict = {
// "type": "Interface",
// "interface": "list",
// "title": "Select Something",
// "allowMultipleSelection": true,
// "items": [
// "A","B","C"
// ]
// }
// - UI Type: Instruction
// Can be used to ask user to do something (other than text input)
// Hero image is base64 encoded image data. Not really sure the maximum length HomeKit allows.
// var respDict = {
// "type": "Interface",
// "interface": "instruction",
// "title": "Almost There",
// "detail": "Please press the button on the bridge to finish the setup.",
// "heroImage": "base64 image data",
// "showActivityIndicator": true,
// "showNextButton": true,
// "buttonText": "Login in browser",
// "actionURL": "https://google.com"
// }
// Plugin can set context to allow it track setup process
context.ts = "Hello";
//invoke callback to update setup UI
callback(respDict);
}
// Sample function to show how developer can add accessory dynamically from outside event
SamplePlatform.prototype.addAccessory = function(accessoryName) {
this.log("Add Accessory");
var platform = this;
var uuid;
uuid = UUIDGen.generate(accessoryName);
var newAccessory = new Accessory(accessoryName, uuid);
newAccessory.on('identify', function(paired, callback) {
platform.log(accessory.displayName, "Identify!!!");
callback();
});
// Plugin can save context on accessory
// To help restore accessory in configureAccessory()
// newAccessory.context.something = "Something"
// Make sure you provided a name for service otherwise it may not visible in some HomeKit apps.
newAccessory.addService(Service.Lightbulb, "Test Light")
.getCharacteristic(Characteristic.On)
.on('set', function(value, callback) {
platform.log(accessory.displayName, "Light -> " + value);
callback();
});
this.accessories.push(newAccessory);
this.api.registerPlatformAccessories("homebridge-samplePlatform", "SamplePlatform", [newAccessory]);
}
SamplePlatform.prototype.updateAccessoriesReachability = function() {
this.log("Update Reachability");
for (var index in this.accessories) {
var accessory = this.accessories[index];
accessory.updateReachability(false);
}
}
// Sample function to show how developer can remove accessory dynamically from outside event
SamplePlatform.prototype.removeAccessory = function() {
this.log("Remove Accessory");
this.api.unregisterPlatformAccessories("homebridge-samplePlatform", "SamplePlatform", this.accessories);
this.accessories = [];
}

View File

@@ -0,0 +1,20 @@
{
"name": "homebridge-samplePlatform",
"version": "0.0.1",
"description": "Sample Platform plugin for homebridge: https://github.com/nfarina/homebridge",
"license": "ISC",
"keywords": [
"homebridge-plugin"
],
"repository": {
"type": "git",
"url": "git://github.com/example/homebridge.git"
},
"bugs": {
"url": "http://github.com/example/homebridge/issues"
},
"engines": {
"node": ">=0.12.0",
"homebridge": ">=0.2.0"
}
}

View File

@@ -1,7 +1,11 @@
var inherits = require('util').inherits;
var EventEmitter = require('events').EventEmitter;
var hap = require("hap-nodejs"); var hap = require("hap-nodejs");
var hapLegacyTypes = require("hap-nodejs/accessories/types.js"); var hapLegacyTypes = require("hap-nodejs/accessories/types.js");
var log = require("./logger")._system; var log = require("./logger")._system;
var User = require("./user").User; var User = require("./user").User;
var PlatformAccessory = require("./platformAccessory").PlatformAccessory;
var serverVersion = require("./version");
// The official homebridge API is the object we feed the plugin's exported initializer function. // The official homebridge API is the object we feed the plugin's exported initializer function.
@@ -12,7 +16,16 @@ module.exports = {
function API() { function API() {
this._accessories = {}; // this._accessories[pluginName.accessoryName] = accessory constructor this._accessories = {}; // this._accessories[pluginName.accessoryName] = accessory constructor
this._platforms = {}; // this._platforms[pluginName.platformName] = platform constructor this._platforms = {}; // this._platforms[pluginName.platformName] = platform constructor
this._configurableAccessories = {};
this._dynamicPlatforms = {}; // this._dynamicPlatforms[pluginName.platformName] = platform constructor
// expose the homebridge API version
this.version = 2.2;
// expose the homebridge server version
this.serverVersion = serverVersion;
// expose the User class methods to plugins to get paths. Example: homebridge.user.storagePath() // expose the User class methods to plugins to get paths. Example: homebridge.user.storagePath()
this.user = User; this.user = User;
@@ -24,8 +37,12 @@ function API() {
// we also need to "bolt on" the legacy "types" constants for older accessories/platforms // we also need to "bolt on" the legacy "types" constants for older accessories/platforms
// still using the "object literal" style JSON. // still using the "object literal" style JSON.
this.hapLegacyTypes = hapLegacyTypes; this.hapLegacyTypes = hapLegacyTypes;
this.platformAccessory = PlatformAccessory;
} }
inherits(API, EventEmitter);
API.prototype.accessory = function(name) { API.prototype.accessory = function(name) {
// if you passed the "short form" name like "Lockitron" instead of "homebridge-lockitron.Lockitron", // if you passed the "short form" name like "Lockitron" instead of "homebridge-lockitron.Lockitron",
@@ -56,7 +73,7 @@ API.prototype.accessory = function(name) {
} }
} }
API.prototype.registerAccessory = function(pluginName, accessoryName, constructor) { API.prototype.registerAccessory = function(pluginName, accessoryName, constructor, configurationRequestHandler) {
var fullName = pluginName + "." + accessoryName; var fullName = pluginName + "." + accessoryName;
if (this._accessories[fullName]) if (this._accessories[fullName])
@@ -65,6 +82,23 @@ API.prototype.registerAccessory = function(pluginName, accessoryName, constructo
log.info("Registering accessory '%s'", fullName); log.info("Registering accessory '%s'", fullName);
this._accessories[fullName] = constructor; this._accessories[fullName] = constructor;
// The plugin supports configuration
if (configurationRequestHandler) {
this._configurableAccessories[fullName] = configurationRequestHandler;
}
}
API.prototype.publishCameraAccessories = function(pluginName, accessories) {
for (var index in accessories) {
var accessory = accessories[index];
if (!(accessory instanceof PlatformAccessory)) {
throw new Error(pluginName + " attempt to register an accessory that isn\'t PlatformAccessory!");
}
accessory._associatedPlugin = pluginName;
}
this.emit('publishCameraAccessories', accessories);
} }
API.prototype.platform = function(name) { API.prototype.platform = function(name) {
@@ -97,7 +131,7 @@ API.prototype.platform = function(name) {
} }
} }
API.prototype.registerPlatform = function(pluginName, platformName, constructor) { API.prototype.registerPlatform = function(pluginName, platformName, constructor, dynamic) {
var fullName = pluginName + "." + platformName; var fullName = pluginName + "." + platformName;
if (this._platforms[fullName]) if (this._platforms[fullName])
@@ -106,4 +140,35 @@ API.prototype.registerPlatform = function(pluginName, platformName, constructor)
log.info("Registering platform '%s'", fullName); log.info("Registering platform '%s'", fullName);
this._platforms[fullName] = constructor; this._platforms[fullName] = constructor;
if (dynamic) {
this._dynamicPlatforms[fullName] = constructor;
}
}
API.prototype.registerPlatformAccessories = function(pluginName, platformName, accessories) {
for (var index in accessories) {
var accessory = accessories[index];
if (!(accessory instanceof PlatformAccessory)) {
throw new Error(pluginName + " - " + platformName + " attempt to register an accessory that isn\'t PlatformAccessory!");
}
accessory._associatedPlugin = pluginName;
accessory._associatedPlatform = platformName;
}
this.emit('registerPlatformAccessories', accessories);
}
API.prototype.updatePlatformAccessories = function(accessories) {
this.emit('updatePlatformAccessories', accessories);
}
API.prototype.unregisterPlatformAccessories = function(pluginName, platformName, accessories) {
for (var index in accessories) {
var accessory = accessories[index];
if (!(accessory instanceof PlatformAccessory)) {
throw new Error(pluginName + " - " + platformName + " attempt to unregister an accessory that isn\'t PlatformAccessory!");
}
}
this.emit('unregisterPlatformAccessories', accessories);
} }

96
lib/bridgeSetupManager.js Normal file
View File

@@ -0,0 +1,96 @@
var inherits = require('util').inherits;
var EventEmitter = require('events').EventEmitter;
var Service = require("hap-nodejs").Service;
var Characteristic = require("hap-nodejs").Characteristic;
var SetupSession = require("./bridgeSetupSession").SetupSession;
'use strict';
module.exports = {
BridgeSetupManager: BridgeSetupManager
}
function BridgeSetupManager() {
this.session;
this.service = new Service(null, "49FB9D4D-0FEA-4BF1-8FA6-E7B18AB86DCE");
this.stateCharacteristic = new Characteristic("State", "77474A2F-FA98-485E-97BE-4762458774D8", {
format: Characteristic.Formats.UINT8,
minValue: 0,
maxValue: 1,
minStep: 1,
perms: [Characteristic.Perms.READ, Characteristic.Perms.NOTIFY]
});
this.stateCharacteristic.value = 0;
this.service.addCharacteristic(this.stateCharacteristic);
this.versionCharacteristic = new Characteristic("Version", "FD9FE4CC-D06F-4FFE-96C6-595D464E1026", {
format: Characteristic.Formats.STRING,
perms: [Characteristic.Perms.READ, Characteristic.Perms.NOTIFY]
});
this.versionCharacteristic.value = "1.0";
this.service.addCharacteristic(this.versionCharacteristic);
this.controlPointCharacteristic = new Characteristic("Control Point", "5819A4C2-E1B0-4C9D-B761-3EB1AFF43073", {
format: Characteristic.Formats.DATA,
perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY]
})
this.controlPointCharacteristic.on('get', function(callback, context) {
this.handleReadRequest(callback, context);
}.bind(this));
this.controlPointCharacteristic.on('set', function(newValue, callback, context) {
this.handleWriteRequest(newValue, callback, context);
}.bind(this));
this.controlPointCharacteristic.value = null;
this.service.addCharacteristic(this.controlPointCharacteristic);
}
inherits(BridgeSetupManager, EventEmitter);
BridgeSetupManager.prototype.handleReadRequest = function(callback, context) {
if (!context) {
return;
}
if (!this.session) {
callback(null, null);
} else {
this.session.handleReadRequest(callback);
}
}
BridgeSetupManager.prototype.handleWriteRequest = function(value, callback, context) {
if (!context) {
callback();
return;
}
var data = new Buffer(value, 'base64');
var request = JSON.parse(data.toString());
callback();
if (!this.session || this.session.sessionUUID !== request.sid) {
if (this.session) {
this.session.removeAllListeners();
this.session.validSession = false;
}
this.session = new SetupSession(this.stateCharacteristic, this.controlPointCharacteristic);
this.session.configurablePlatformPlugins = this.configurablePlatformPlugins;
this.session.on('newConfig', function(type, name, replace, config) {
this.emit('newConfig', type, name, replace, config);
}.bind(this));
this.session.on('requestCurrentConfig', function(callback) {
this.emit('requestCurrentConfig', callback);
}.bind(this));
this.session.on('end', function() {
this.session = null;
}.bind(this));
}
this.session.handleWriteRequest(request);
}

191
lib/bridgeSetupSession.js Normal file

File diff suppressed because one or more lines are too long

View File

@@ -17,11 +17,25 @@ module.exports = function() {
.option('-P, --plugin-path [path]', 'look for plugins installed at [path] as well as the default locations ([path] can also point to a single plugin)', function(p) { Plugin.addPluginPath(p); }) .option('-P, --plugin-path [path]', 'look for plugins installed at [path] as well as the default locations ([path] can also point to a single plugin)', function(p) { Plugin.addPluginPath(p); })
.option('-U, --user-storage-path [path]', 'look for homebridge user files at [path] instead of the default location (~/.homebridge)', function(p) { User.setStoragePath(p); }) .option('-U, --user-storage-path [path]', 'look for homebridge user files at [path] instead of the default location (~/.homebridge)', function(p) { User.setStoragePath(p); })
.option('-D, --debug', 'turn on debug level logging', function() { require('./logger').setDebugEnabled(true) }) .option('-D, --debug', 'turn on debug level logging', function() { require('./logger').setDebugEnabled(true) })
.option('-I, --insecure', 'allow insecure access to homebridge', function() { insecureAccess = true; }) .option('-I, --insecure', 'allow unauthenticated requests (for easier hacking)', function() { insecureAccess = true })
.parse(process.argv); .parse(process.argv);
// Initialize HAP-NodeJS with a custom persist directory // Initialize HAP-NodeJS with a custom persist directory
hap.init(User.persistPath()); hap.init(User.persistPath());
new Server(insecureAccess).run(); var server = new Server(insecureAccess);
var signals = { 'SIGINT': 2, 'SIGTERM': 15 };
Object.keys(signals).forEach(function (signal) {
process.on(signal, function () {
log.info("Got %s, shutting down Homebridge...", signal);
// Save cached accessories to persist storage.
server._updateCachedAccessories();
process.exit(128 + signals[signal]);
});
});
server.run();
} }

View File

@@ -31,7 +31,7 @@ Logger.prototype.debug = function(msg) {
if (DEBUG_ENABLED) if (DEBUG_ENABLED)
this.log.apply(this, ['debug'].concat(Array.prototype.slice.call(arguments))); this.log.apply(this, ['debug'].concat(Array.prototype.slice.call(arguments)));
} }
Logger.prototype.info = function(msg) { Logger.prototype.info = function(msg) {
this.log.apply(this, ['info'].concat(Array.prototype.slice.call(arguments))); this.log.apply(this, ['info'].concat(Array.prototype.slice.call(arguments)));
} }
@@ -43,31 +43,35 @@ Logger.prototype.warn = function(msg) {
Logger.prototype.error = function(msg) { Logger.prototype.error = function(msg) {
this.log.apply(this, ['error'].concat(Array.prototype.slice.call(arguments))); this.log.apply(this, ['error'].concat(Array.prototype.slice.call(arguments)));
} }
Logger.prototype.log = function(level, msg) { Logger.prototype.log = function(level, msg) {
msg = util.format.apply(util, Array.prototype.slice.call(arguments, 1)); msg = util.format.apply(util, Array.prototype.slice.call(arguments, 1));
func = console.log; func = console.log;
if (level == 'debug') { if (level == 'debug') {
msg = chalk.gray(msg); msg = chalk.gray(msg);
} }
else if (level == 'warn') { else if (level == 'warn') {
msg = chalk.yellow(msg); msg = chalk.yellow(msg);
func = console.error; func = console.error;
} }
else if (level == 'error') { else if (level == 'error') {
msg = chalk.bold.red(msg); msg = chalk.bold.red(msg);
func = console.error; func = console.error;
} }
// prepend prefix if applicable // prepend prefix if applicable
if (this.prefix) if (this.prefix)
msg = chalk.cyan("[" + this.prefix + "]") + " " + msg; msg = chalk.cyan("[" + this.prefix + "]") + " " + msg;
// prepend timestamp
var date = new Date();
msg = chalk.white("[" + date.toLocaleString() + "]") + " " + msg;
func(msg); func(msg);
} }
Logger.withPrefix = function(prefix) { Logger.withPrefix = function(prefix) {
if (!loggerCache[prefix]) { if (!loggerCache[prefix]) {
@@ -83,6 +87,6 @@ Logger.withPrefix = function(prefix) {
log.prefix = logger.prefix; log.prefix = logger.prefix;
loggerCache[prefix] = log; loggerCache[prefix] = log;
} }
return loggerCache[prefix]; return loggerCache[prefix];
} }

228
lib/platformAccessory.js Normal file
View File

@@ -0,0 +1,228 @@
var uuid = require("hap-nodejs").uuid;
var Accessory = require("hap-nodejs").Accessory;
var Service = require("hap-nodejs").Service;
var Characteristic = require("hap-nodejs").Characteristic;
var inherits = require('util').inherits;
var EventEmitter = require('events').EventEmitter;
'use strict';
module.exports = {
PlatformAccessory: PlatformAccessory
}
function PlatformAccessory(displayName, UUID, category) {
if (!displayName) throw new Error("Accessories must be created with a non-empty displayName.");
if (!UUID) throw new Error("Accessories must be created with a valid UUID.");
if (!uuid.isValid(UUID)) throw new Error("UUID '" + UUID + "' is not a valid UUID. Try using the provided 'generateUUID' function to create a valid UUID from any arbitrary string, like a serial number.");
this.displayName = displayName;
this.UUID = UUID;
this.category = category || Accessory.Categories.OTHER;
this.services = [];
this.reachable = false;
this.context = {};
this._associatedPlugin;
this._associatedPlatform;
this._associatedHAPAccessory;
this
.addService(Service.AccessoryInformation)
.setCharacteristic(Characteristic.Name, displayName)
.setCharacteristic(Characteristic.Manufacturer, "Default-Manufacturer")
.setCharacteristic(Characteristic.Model, "Default-Model")
.setCharacteristic(Characteristic.SerialNumber, "Default-SerialNumber");
}
inherits(PlatformAccessory, EventEmitter);
PlatformAccessory.prototype.addService = function(service) {
// service might be a constructor like `Service.AccessoryInformation` instead of an instance
// of Service. Coerce if necessary.
if (typeof service === 'function')
service = new (Function.prototype.bind.apply(service, arguments));
// check for UUID+subtype conflict
for (var index in this.services) {
var existing = this.services[index];
if (existing.UUID === service.UUID) {
// OK we have two Services with the same UUID. Check that each defines a `subtype` property and that each is unique.
if (!service.subtype)
throw new Error("Cannot add a Service with the same UUID '" + existing.UUID + "' as another Service in this Accessory without also defining a unique 'subtype' property.");
if (service.subtype.toString() === existing.subtype.toString())
throw new Error("Cannot add a Service with the same UUID '" + existing.UUID + "' and subtype '" + existing.subtype + "' as another Service in this Accessory.");
}
}
this.services.push(service);
if (this._associatedHAPAccessory) {
this._associatedHAPAccessory.addService(service);
}
return service;
}
PlatformAccessory.prototype.removeService = function(service) {
var targetServiceIndex;
for (var index in this.services) {
var existingService = this.services[index];
if (existingService === service) {
targetServiceIndex = index;
break;
}
}
if (targetServiceIndex) {
this.services.splice(targetServiceIndex, 1);
service.removeAllListeners();
if (this._associatedHAPAccessory) {
this._associatedHAPAccessory.removeService(service);
}
}
}
/**
* searchs for a Service in the services collection and returns the first Service object that matches.
* If multiple services of the same type are present in one accessory, use getServiceByUUIDAndSubType instead.
* @param {ServiceConstructor|string} name
* @returns Service
*/
PlatformAccessory.prototype.getService = function(name) {
for (var index in this.services) {
var service = this.services[index];
if (typeof name === 'string' && (service.displayName === name || service.name === name))
return service;
else if (typeof name === 'function' && ((service instanceof name) || (name.UUID === service.UUID)))
return service;
}
}
/**
* searchs for a Service in the services collection and returns the first Service object that matches.
* If multiple services of the same type are present in one accessory, use getServiceByUUIDAndSubType instead.
* @param {string} UUID Can be an UUID, a service.displayName, or a constructor of a Service
* @param {string} subtype A subtype string to match
* @returns Service
*/
PlatformAccessory.prototype.getServiceByUUIDAndSubType = function(UUID, subtype) {
for (var index in this.services) {
var service = this.services[index];
if (typeof UUID === 'string' && (service.displayName === UUID || service.name === UUID) && service.subtype === subtype )
return service;
else if (typeof UUID === 'function' && ((service instanceof UUID) || (UUID.UUID === service.UUID)) && service.subtype === subtype)
return service;
}
}
PlatformAccessory.prototype.updateReachability = function(reachable) {
this.reachable = reachable;
if (this._associatedHAPAccessory) {
this._associatedHAPAccessory.updateReachability(reachable);
}
}
PlatformAccessory.prototype.configureCameraSource = function(cameraSource) {
this.cameraSource = cameraSource;
for (var index in cameraSource.services) {
var service = cameraSource.services[index];
this.addService(service);
}
}
PlatformAccessory.prototype._prepareAssociatedHAPAccessory = function () {
this._associatedHAPAccessory = new Accessory(this.displayName, this.UUID);
if (this.cameraSource) {
this._associatedHAPAccessory.configureCameraSource(this.cameraSource);
}
this._associatedHAPAccessory._sideloadServices(this.services);
this._associatedHAPAccessory.category = this.category;
this._associatedHAPAccessory.reachable = this.reachable;
this._associatedHAPAccessory.on('identify', function(paired, callback) {
if (this.listeners('identify').length > 0) {
// allow implementors to identify this Accessory in whatever way is appropriate, and pass along
// the standard callback for completion.
this.emit('identify', paired, callback);
} else {
callback();
}
}.bind(this));
}
PlatformAccessory.prototype._dictionaryPresentation = function() {
var accessory = {};
accessory.plugin = this._associatedPlugin;
accessory.platform = this._associatedPlatform;
accessory.displayName = this.displayName;
accessory.UUID = this.UUID;
accessory.category = this.category;
accessory.context = this.context;
var services = [];
for (var index in this.services) {
var service = this.services[index];
var servicePresentation = {};
servicePresentation.displayName = service.displayName;
servicePresentation.UUID = service.UUID;
servicePresentation.subtype = service.subtype;
var characteristics = [];
for (var cIndex in service.characteristics) {
var characteristic = service.characteristics[cIndex];
var characteristicPresentation = {};
characteristicPresentation.displayName = characteristic.displayName;
characteristicPresentation.UUID = characteristic.UUID;
characteristicPresentation.props = characteristic.props;
characteristicPresentation.value = characteristic.value;
characteristicPresentation.eventOnlyCharacteristic = characteristic.eventOnlyCharacteristic;
characteristics.push(characteristicPresentation);
}
servicePresentation.characteristics = characteristics;
services.push(servicePresentation);
}
accessory.services = services;
return accessory;
}
PlatformAccessory.prototype._configFromData = function(data) {
this._associatedPlugin = data.plugin;
this._associatedPlatform = data.platform;
this.displayName = data.displayName;
this.UUID = data.UUID;
this.category = data.category;
this.context = data.context;
this.reachable = false;
var services = [];
for (var index in data.services) {
var service = data.services[index];
var hapService = new Service(service.displayName, service.UUID, service.subtype);
var characteristics = [];
for (var cIndex in service.characteristics) {
var characteristic = service.characteristics[cIndex];
var hapCharacteristic = new Characteristic(characteristic.displayName, characteristic.UUID, characteristic.props);
hapCharacteristic.eventOnlyCharacteristic = characteristic.eventOnlyCharacteristic;
hapCharacteristic.value = characteristic.value;
characteristics.push(hapCharacteristic);
}
hapService._sideloadCharacteristics(characteristics);
services.push(hapService);
}
this.services = services;
}

View File

@@ -13,7 +13,7 @@ module.exports = {
/** /**
* Homebridge Plugin. * Homebridge Plugin.
* *
* Allows for discovering and loading installed Homebridge plugins. * Allows for discovering and loading installed Homebridge plugins.
*/ */
@@ -28,39 +28,39 @@ Plugin.prototype.name = function() {
Plugin.prototype.load = function(options) { Plugin.prototype.load = function(options) {
options = options || {}; options = options || {};
// does this plugin exist at all? // does this plugin exist at all?
if (!fs.existsSync(this.pluginPath)) { if (!fs.existsSync(this.pluginPath)) {
throw new Error("Plugin " + this.pluginPath + " was not found. Make sure the module '" + this.pluginPath + "' is installed."); throw new Error("Plugin " + this.pluginPath + " was not found. Make sure the module '" + this.pluginPath + "' is installed.");
} }
// attempt to load package.json // attempt to load package.json
var pjson = Plugin.loadPackageJSON(this.pluginPath); var pjson = Plugin.loadPackageJSON(this.pluginPath);
// very temporary fix for first wave of plugins // very temporary fix for first wave of plugins
if (pjson.peerDepdendencies && (!pjson.engines || !pjson.engines.homebridge)) { if (pjson.peerDepdendencies && (!pjson.engines || !pjson.engines.homebridge)) {
var engines = pjson.engines || {} var engines = pjson.engines || {}
engines.homebridge = pjson.peerDepdendencies.homebridge; engines.homebridge = pjson.peerDepdendencies.homebridge;
pjson.engines = engines; pjson.engines = engines;
} }
// pluck out the HomeBridge version requirement // pluck out the HomeBridge version requirement
if (!pjson.engines || !pjson.engines.homebridge) { if (!pjson.engines || !pjson.engines.homebridge) {
throw new Error("Plugin " + this.pluginPath + " does not contain the 'homebridge' package in 'engines'."); throw new Error("Plugin " + this.pluginPath + " does not contain the 'homebridge' package in 'engines'.");
} }
var versionRequired = pjson.engines.homebridge; var versionRequired = pjson.engines.homebridge;
// make sure the version is satisfied by the currently running version of HomeBridge // make sure the version is satisfied by the currently running version of HomeBridge
if (!semver.satisfies(version, versionRequired)) { if (!semver.satisfies(version, versionRequired)) {
throw new Error("Plugin " + this.pluginPath + " requires a HomeBridge version of " + versionRequired + " which does not satisfy the current HomeBridge version of " + version + ". You may need to upgrade your installation of HomeBridge."); throw new Error("Plugin " + this.pluginPath + " requires a HomeBridge version of " + versionRequired + " which does not satisfy the current HomeBridge version of " + version + ". You may need to upgrade your installation of HomeBridge.");
} }
// figure out the main module - index.js unless otherwise specified // figure out the main module - index.js unless otherwise specified
var main = pjson.main || "./index.js"; var main = pjson.main || "./index.js";
var mainPath = path.join(this.pluginPath, main); var mainPath = path.join(this.pluginPath, main);
// try to require() it and grab the exported initialization hook // try to require() it and grab the exported initialization hook
this.initializer = require(mainPath); this.initializer = require(mainPath);
} }
@@ -69,11 +69,11 @@ Plugin.loadPackageJSON = function(pluginPath) {
// check for a package.json // check for a package.json
var pjsonPath = path.join(pluginPath, "package.json"); var pjsonPath = path.join(pluginPath, "package.json");
var pjson = null; var pjson = null;
if (!fs.existsSync(pjsonPath)) { if (!fs.existsSync(pjsonPath)) {
throw new Error("Plugin " + pluginPath + " does not contain a package.json."); throw new Error("Plugin " + pluginPath + " does not contain a package.json.");
} }
try { try {
// attempt to parse package.json // attempt to parse package.json
pjson = JSON.parse(fs.readFileSync(pjsonPath)); pjson = JSON.parse(fs.readFileSync(pjsonPath));
@@ -81,7 +81,7 @@ Plugin.loadPackageJSON = function(pluginPath) {
catch (err) { catch (err) {
throw new Error("Plugin " + pluginPath + " contains an invalid package.json. Error: " + err); throw new Error("Plugin " + pluginPath + " contains an invalid package.json. Error: " + err);
} }
// make sure the name is prefixed with 'homebridge-' // make sure the name is prefixed with 'homebridge-'
if (!pjson.name || pjson.name.indexOf('homebridge-') != 0) { if (!pjson.name || pjson.name.indexOf('homebridge-') != 0) {
throw new Error("Plugin " + pluginPath + " does not have a package name that begins with 'homebridge-'."); throw new Error("Plugin " + pluginPath + " does not have a package name that begins with 'homebridge-'.");
@@ -91,7 +91,7 @@ Plugin.loadPackageJSON = function(pluginPath) {
if (!pjson.keywords || pjson.keywords.indexOf("homebridge-plugin") == -1) { if (!pjson.keywords || pjson.keywords.indexOf("homebridge-plugin") == -1) {
throw new Error("Plugin " + pluginPath + " package.json does not contain the keyword 'homebridge-plugin'."); throw new Error("Plugin " + pluginPath + " package.json does not contain the keyword 'homebridge-plugin'.");
} }
return pjson; return pjson;
} }
@@ -119,9 +119,10 @@ Plugin.getDefaultPaths = function() {
} else { } else {
paths.push('/usr/local/lib/node_modules'); paths.push('/usr/local/lib/node_modules');
paths.push('/usr/lib/node_modules'); paths.push('/usr/lib/node_modules');
const exec = require('child_process').execSync;
paths.push(exec('/bin/echo -n "$(npm -g prefix)/lib/node_modules"').toString('utf8'));
} }
} }
return paths; return paths;
} }
@@ -138,17 +139,17 @@ Plugin.installed = function() {
var plugins = []; var plugins = [];
var pluginsByName = {}; // don't add duplicate plugins var pluginsByName = {}; // don't add duplicate plugins
var searchedPaths = {}; // don't search the same paths twice var searchedPaths = {}; // don't search the same paths twice
// search for plugins among all known paths, in order // search for plugins among all known paths, in order
for (var index in Plugin.paths) { for (var index in Plugin.paths) {
var requirePath = Plugin.paths[index]; var requirePath = Plugin.paths[index];
// did we already search this path? // did we already search this path?
if (searchedPaths[requirePath]) if (searchedPaths[requirePath])
continue; continue;
searchedPaths[requirePath] = true; searchedPaths[requirePath] = true;
// just because this path is in require.main.paths doesn't mean it necessarily exists! // just because this path is in require.main.paths doesn't mean it necessarily exists!
if (!fs.existsSync(requirePath)) if (!fs.existsSync(requirePath))
continue; continue;
@@ -158,17 +159,19 @@ Plugin.installed = function() {
// does this path point inside a single plugin and not a directory containing plugins? // does this path point inside a single plugin and not a directory containing plugins?
if (fs.existsSync(path.join(requirePath, "package.json"))) if (fs.existsSync(path.join(requirePath, "package.json")))
names = [""]; names = [""];
// read through each directory in this node_modules folder // read through each directory in this node_modules folder
for (var index2 in names) { for (var index2 in names) {
var name = names[index2]; var name = names[index2];
// reconstruct full path // reconstruct full path
var pluginPath = path.join(requirePath, name); var pluginPath = path.join(requirePath, name);
try {
// we only care about directories // we only care about directories
if (!fs.statSync(pluginPath).isDirectory()) continue; if (!fs.statSync(pluginPath).isDirectory()) continue;
} catch (e) {
continue;
}
// does this module contain a package.json? // does this module contain a package.json?
var pjson; var pjson;
try { try {
@@ -180,14 +183,14 @@ Plugin.installed = function() {
if (!name || name.indexOf('homebridge-') == 0) { if (!name || name.indexOf('homebridge-') == 0) {
log.warn(err.message); log.warn(err.message);
} }
// skip this module // skip this module
continue; continue;
} }
// get actual name if this path points inside a single plugin // get actual name if this path points inside a single plugin
if (!name) name = pjson.name; if (!name) name = pjson.name;
// add it to the return list // add it to the return list
if (!pluginsByName[name]) { if (!pluginsByName[name]) {
pluginsByName[name] = pluginPath; pluginsByName[name] = pluginPath;
@@ -198,6 +201,6 @@ Plugin.installed = function() {
} }
} }
} }
return plugins; return plugins;
} }

View File

@@ -1,6 +1,7 @@
var path = require('path'); var path = require('path');
var fs = require('fs'); var fs = require('fs');
var uuid = require("hap-nodejs").uuid; var uuid = require("hap-nodejs").uuid;
var accessoryStorage = require('node-persist').create();
var Bridge = require("hap-nodejs").Bridge; var Bridge = require("hap-nodejs").Bridge;
var Accessory = require("hap-nodejs").Accessory; var Accessory = require("hap-nodejs").Accessory;
var Service = require("hap-nodejs").Service; var Service = require("hap-nodejs").Service;
@@ -10,8 +11,12 @@ var once = require("hap-nodejs/lib/util/once").once;
var Plugin = require('./plugin').Plugin; var Plugin = require('./plugin').Plugin;
var User = require('./user').User; var User = require('./user').User;
var API = require('./api').API; var API = require('./api').API;
var PlatformAccessory = require("./platformAccessory").PlatformAccessory;
var BridgeSetupManager = require("./bridgeSetupManager").BridgeSetupManager;
var log = require("./logger")._system; var log = require("./logger")._system;
var Logger = require('./logger').Logger; var Logger = require('./logger').Logger;
var mac = require("./util/mac");
var chalk = require('chalk');
'use strict'; 'use strict';
@@ -19,12 +24,50 @@ module.exports = {
Server: Server Server: Server
} }
function Server(insecureAccess) { function Server(insecureAccess, opts) {
opts = opts || {};
// Setup Accessory Cache Storage
accessoryStorage.initSync({ dir: User.cachedAccessoryPath() });
this._api = new API(); // object we feed to Plugins this._api = new API(); // object we feed to Plugins
this._api.on('registerPlatformAccessories', function(accessories) {
this._handleRegisterPlatformAccessories(accessories);
}.bind(this));
this._api.on('updatePlatformAccessories', function(accessories) {
this._handleUpdatePlatformAccessories(accessories);
}.bind(this));
this._api.on('unregisterPlatformAccessories', function(accessories) {
this._handleUnregisterPlatformAccessories(accessories);
}.bind(this));
this._api.on('publishCameraAccessories', function(accessories) {
this._handlePublishCameraAccessories(accessories);
}.bind(this));
this._plugins = this._loadPlugins(); // plugins[name] = Plugin instance this._plugins = this._loadPlugins(); // plugins[name] = Plugin instance
this._config = this._loadConfig(); this._config = opts.config || this._loadConfig();
this._cachedPlatformAccessories = this._loadCachedPlatformAccessories();
this._bridge = this._createBridge(); this._bridge = this._createBridge();
this._activeDynamicPlugins = {};
this._configurablePlatformPlugins = {};
this._publishedCameras = {};
this._setupManager = new BridgeSetupManager();
this._setupManager.on('newConfig', this._handleNewConfig.bind(this));
this._setupManager.on('requestCurrentConfig', function(callback) {
callback(this._config);
}.bind(this));
// Server is "secure by default", meaning it creates a top-level Bridge accessory that
// will not allow unauthenticated requests. This matches the behavior of actual HomeKit
// accessories. However you can set this to true to allow all requests without authentication,
// which can be useful for easy hacking. Note that this will expose all functions of your
// bridged accessories, like changing charactersitics (i.e. flipping your lights on and off).
this._allowInsecureAccess = insecureAccess || false; this._allowInsecureAccess = insecureAccess || false;
} }
@@ -36,27 +79,44 @@ Server.prototype.run = function() {
if (this._config.platforms) this._loadPlatforms(); if (this._config.platforms) this._loadPlatforms();
if (this._config.accessories) this._loadAccessories(); if (this._config.accessories) this._loadAccessories();
this._loadDynamicPlatforms();
this._configCachedPlatformAccessories();
this._setupManager.configurablePlatformPlugins = this._configurablePlatformPlugins;
this._bridge.addService(this._setupManager.service);
this._asyncWait = false; this._asyncWait = false;
// publish now unless we're waiting on anyone // publish now unless we're waiting on anyone
if (this._asyncCalls == 0) if (this._asyncCalls == 0)
this._publish(); this._publish();
this._api.emit('didFinishLaunching');
} }
Server.prototype._publish = function() { Server.prototype._publish = function() {
// pull out our custom Bridge settings from config.json, if any // pull out our custom Bridge settings from config.json, if any
var bridgeConfig = this._config.bridge || {}; var bridgeConfig = this._config.bridge || {};
var info = this._bridge.getService(Service.AccessoryInformation);
if (bridgeConfig.manufacturer)
info.setCharacteristic(Characteristic.Manufacturer, bridgeConfig.manufacturer);
if (bridgeConfig.model)
info.setCharacteristic(Characteristic.Model, bridgeConfig.model);
if (bridgeConfig.serialNumber)
info.setCharacteristic(Characteristic.SerialNumber, bridgeConfig.serialNumber);
this._printPin(bridgeConfig.pin); this._printPin(bridgeConfig.pin);
this._bridge.on('listening', function(port) {
log.info("Homebridge is running on port %s.", port);
});
this._bridge.publish({ this._bridge.publish({
username: bridgeConfig.username || "CC:22:3D:E3:CE:30", username: bridgeConfig.username || "CC:22:3D:E3:CE:30",
port: bridgeConfig.port || 51826, port: bridgeConfig.port || 0,
pincode: bridgeConfig.pin || "031-45-154", pincode: bridgeConfig.pin || "031-45-154",
category: Accessory.Categories.BRIDGE category: Accessory.Categories.BRIDGE
}, this._allowInsecureAccess); }, this._allowInsecureAccess);
log.info("Homebridge is running on port %s.", bridgeConfig.port || 51826);
} }
Server.prototype._loadPlugins = function(accessories, platforms) { Server.prototype._loadPlugins = function(accessories, platforms) {
@@ -110,8 +170,19 @@ Server.prototype._loadConfig = function() {
// Complain and exit if it doesn't exist yet // Complain and exit if it doesn't exist yet
if (!fs.existsSync(configPath)) { if (!fs.existsSync(configPath)) {
log.error("Couldn't find a config.json file at '"+configPath+"'. Look at config-sample.json for examples of how to format your config.js and add your home accessories."); log.warn("config.json (%s) not found.", configPath);
process.exit(1);
var config = {};
config.bridge = {
"name": "Homebridge",
"username": "CC:22:3D:E3:CE:30",
"pin": "031-45-154"
};
return config;
// log.error("Couldn't find a config.json file at '"+configPath+"'. Look at config-sample.json for examples of how to format your config.js and add your home accessories.");
// process.exit(1);
} }
// Load up the configuration file // Load up the configuration file
@@ -144,6 +215,23 @@ Server.prototype._loadConfig = function() {
return config; return config;
} }
Server.prototype._loadCachedPlatformAccessories = function() {
var cachedAccessories = accessoryStorage.getItem("cachedAccessories");
var platformAccessories = [];
if (cachedAccessories) {
for (var index in cachedAccessories) {
var serializedAccessory = cachedAccessories[index];
var platformAccessory = new PlatformAccessory(serializedAccessory.displayName, serializedAccessory.UUID, serializedAccessory.category);
platformAccessory._configFromData(serializedAccessory);
platformAccessories.push(platformAccessory);
}
}
return platformAccessories;
}
Server.prototype._createBridge = function() { Server.prototype._createBridge = function() {
// pull out our custom Bridge settings from config.json, if any // pull out our custom Bridge settings from config.json, if any
var bridgeConfig = this._config.bridge || {}; var bridgeConfig = this._config.bridge || {};
@@ -203,11 +291,64 @@ Server.prototype._loadPlatforms = function() {
platformLogger("Initializing %s platform...", platformType); platformLogger("Initializing %s platform...", platformType);
var platformInstance = new platformConstructor(platformLogger, platformConfig); var platformInstance = new platformConstructor(platformLogger, platformConfig, this._api);
this._loadPlatformAccessories(platformInstance, platformLogger, platformType);
if (platformInstance.configureAccessory == undefined) {
// Plugin 1.0, load accessories
this._loadPlatformAccessories(platformInstance, platformLogger, platformType);
} else {
this._activeDynamicPlugins[platformType] = platformInstance;
}
if (platformInstance.configurationRequestHandler != undefined) {
this._configurablePlatformPlugins[platformType] = platformInstance;
}
} }
} }
Server.prototype._loadDynamicPlatforms = function() {
for (var dynamicPluginName in this._api._dynamicPlatforms) {
if (!this._activeDynamicPlugins[dynamicPluginName] && !this._activeDynamicPlugins[dynamicPluginName.split(".")[1]]) {
console.log("Load " + dynamicPluginName);
var platformConstructor = this._api._dynamicPlatforms[dynamicPluginName];
var platformLogger = Logger.withPrefix(dynamicPluginName);
var platformInstance = new platformConstructor(platformLogger, null, this._api);
this._activeDynamicPlugins[dynamicPluginName] = platformInstance;
if (platformInstance.configurationRequestHandler != undefined) {
this._configurablePlatformPlugins[dynamicPluginName] = platformInstance;
}
}
}
}
Server.prototype._configCachedPlatformAccessories = function() {
for (var index in this._cachedPlatformAccessories) {
var accessory = this._cachedPlatformAccessories[index];
if (!(accessory instanceof PlatformAccessory)) {
console.log("Unexpected Accessory!");
continue;
}
var fullName = accessory._associatedPlugin + "." + accessory._associatedPlatform;
var platformInstance = this._activeDynamicPlugins[fullName];
if (!platformInstance) {
platformInstance = this._activeDynamicPlugins[accessory._associatedPlatform];
}
if (platformInstance) {
platformInstance.configureAccessory(accessory);
} else {
console.log("Failed to find plugin to handle accessory " + accessory.displayName);
}
accessory._prepareAssociatedHAPAccessory();
this._bridge.addBridgedAccessory(accessory._associatedHAPAccessory);
}
}
Server.prototype._loadPlatformAccessories = function(platformInstance, log, platformType) { Server.prototype._loadPlatformAccessories = function(platformInstance, log, platformType) {
this._asyncCalls++; this._asyncCalls++;
platformInstance.accessories(once(function(foundAccessories){ platformInstance.accessories(once(function(foundAccessories){
@@ -282,12 +423,167 @@ Server.prototype._createAccessory = function(accessoryInstance, displayName, acc
} }
} }
Server.prototype._handleRegisterPlatformAccessories = function(accessories) {
var hapAccessories = [];
for (var index in accessories) {
var accessory = accessories[index];
accessory._prepareAssociatedHAPAccessory();
hapAccessories.push(accessory._associatedHAPAccessory);
this._cachedPlatformAccessories.push(accessory);
}
this._bridge.addBridgedAccessories(hapAccessories);
this._updateCachedAccessories();
}
Server.prototype._handleUpdatePlatformAccessories = function(accessories) {
// Update persisted accessories
this._updateCachedAccessories();
}
Server.prototype._handleUnregisterPlatformAccessories = function(accessories) {
var hapAccessories = [];
for (var index in accessories) {
var accessory = accessories[index];
if (accessory._associatedHAPAccessory) {
hapAccessories.push(accessory._associatedHAPAccessory);
}
for (var targetIndex in this._cachedPlatformAccessories) {
var existing = this._cachedPlatformAccessories[targetIndex];
if (existing.UUID === accessory.UUID) {
this._cachedPlatformAccessories.splice(targetIndex, 1);
break;
}
}
}
this._bridge.removeBridgedAccessories(hapAccessories);
this._updateCachedAccessories();
}
Server.prototype._handlePublishCameraAccessories = function(accessories) {
var accessoryPin = (this._config.bridge || {}).pin || "031-45-154";
for (var index in accessories) {
var accessory = accessories[index];
accessory._prepareAssociatedHAPAccessory();
var hapAccessory = accessory._associatedHAPAccessory;
var advertiseAddress = mac.generate(accessory.UUID);
if (this._publishedCameras[advertiseAddress]) {
throw new Error("Camera accessory " + accessory.displayName + " experienced an address collision.");
} else {
this._publishedCameras[advertiseAddress] = accessory;
}
hapAccessory.on('listening', function(port) {
log.info("%s is running on port %s.", accessory.displayName, port);
});
hapAccessory.publish({
username: advertiseAddress,
pincode: accessoryPin,
category: accessory.category
}, this._allowInsecureAccess);
}
}
Server.prototype._updateCachedAccessories = function() {
var serializedAccessories = [];
for (var index in this._cachedPlatformAccessories) {
var accessory = this._cachedPlatformAccessories[index];
serializedAccessories.push(accessory._dictionaryPresentation());
}
accessoryStorage.setItemSync("cachedAccessories", serializedAccessories);
}
Server.prototype._handleNewConfig = function(type, name, replace, config) {
if (type === "accessory") {
// TODO: Load new accessory
if (!this._config.accessories) {
this._config.accessories = [];
}
if (!replace) {
this._config.accessories.push(config);
} else {
var targetName;
if (name.indexOf('.') !== -1) {
targetName = name.split(".")[1];
}
var found = false;
for (var index in this._config.accessories) {
var accessoryConfig = this._config.accessories[index];
if (accessoryConfig.accessory === name) {
this._config.accessories[index] = config;
found = true;
break;
}
if (targetName && (accessoryConfig.accessory === targetName)) {
this._config.accessories[index] = config;
found = true;
break;
}
}
if (!found) {
this._config.accessories.push(config);
}
}
} else if (type === "platform") {
if (!this._config.platforms) {
this._config.platforms = [];
}
if (!replace) {
this._config.platforms.push(config);
} else {
var targetName;
if (name.indexOf('.') !== -1) {
targetName = name.split(".")[1];
}
var found = false;
for (var index in this._config.platforms) {
var platformConfig = this._config.platforms[index];
if (platformConfig.platform === name) {
this._config.platforms[index] = config;
found = true;
break;
}
if (targetName && (platformConfig.platform === targetName)) {
this._config.platforms[index] = config;
found = true;
break;
}
}
if (!found) {
this._config.platforms.push(config);
}
}
}
var serializedConfig = JSON.stringify(this._config, null, ' ');
var configPath = User.configPath();
fs.writeFileSync(configPath, serializedConfig, 'utf8');
}
// Returns the setup code in a scannable format. // Returns the setup code in a scannable format.
Server.prototype._printPin = function(pin) { Server.prototype._printPin = function(pin) {
console.log("Scan this code with your HomeKit App on your iOS device to pair with Homebridge:"); console.log("Scan this code with your HomeKit App on your iOS device to pair with Homebridge:");
console.log("\x1b[30;47m%s\x1b[0m", " "); console.log(chalk.black.bgWhite(" "));
console.log("\x1b[30;47m%s\x1b[0m", " ┌────────────┐ "); console.log(chalk.black.bgWhite(" ┌────────────┐ "));
console.log("\x1b[30;47m%s\x1b[0m", " │ " + pin + " │ "); console.log(chalk.black.bgWhite(" │ " + pin + " │ "));
console.log("\x1b[30;47m%s\x1b[0m", " └────────────┘ "); console.log(chalk.black.bgWhite(" └────────────┘ "));
console.log("\x1b[30;47m%s\x1b[0m", " "); console.log(chalk.black.bgWhite(" "));
} }

View File

@@ -38,6 +38,10 @@ User.persistPath = function() {
return path.join(User.storagePath(), "persist"); return path.join(User.storagePath(), "persist");
} }
User.cachedAccessoryPath = function() {
return path.join(User.storagePath(), "accessories");
}
User.setStoragePath = function(path) { User.setStoragePath = function(path) {
customStoragePath = path; customStoragePath = path;
} }

18
lib/util/mac.js Normal file
View File

@@ -0,0 +1,18 @@
var crypto = require('crypto');
'use strict';
module.exports = {
generate: generate
}
function generate(data) {
var sha1sum = crypto.createHash('sha1');
sha1sum.update(data);
var s = sha1sum.digest('hex');
var i = -1;
return 'xx:xx:xx:xx:xx:xx'.replace(/[x]/g, function(c) {
i += 1;
return s[i];
}).toUpperCase();
};

View File

@@ -1,7 +1,7 @@
{ {
"name": "homebridge", "name": "homebridge",
"description": "HomeKit support for the impatient", "description": "HomeKit support for the impatient",
"version": "0.2.18", "version": "0.4.22",
"scripts": { "scripts": {
"dev": "DEBUG=* ./bin/homebridge -D -P example-plugins/ || true" "dev": "DEBUG=* ./bin/homebridge -D -P example-plugins/ || true"
}, },
@@ -15,23 +15,19 @@
"bugs": { "bugs": {
"url": "http://github.com/nfarina/homebridge/issues" "url": "http://github.com/nfarina/homebridge/issues"
}, },
"licenses": [ "license": "ISC",
{
"type": "ISC",
"url": "http://github.com/nfarina/homebridge/blob/master/LICENSE"
}
],
"bin": { "bin": {
"homebridge": "bin/homebridge" "homebridge": "bin/homebridge"
}, },
"engines": { "engines": {
"node": ">=0.12.0" "node": ">=4.3.2"
}, },
"preferGlobal": true, "preferGlobal": true,
"dependencies": { "dependencies": {
"chalk": "^1.1.1", "chalk": "^1.1.1",
"commander": "2.8.1", "commander": "2.8.1",
"hap-nodejs": "0.2.3", "hap-nodejs": "0.4.27",
"semver": "5.0.3" "semver": "5.0.3",
"node-persist": "^0.0.8"
} }
} }