mirror of
https://github.com/mtan93/homebridge.git
synced 2026-03-08 05:31:55 +00:00
Compare commits
140 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a561f8754 | ||
|
|
d9d58855cd | ||
|
|
3acded3ea2 | ||
|
|
8a35d75cb5 | ||
|
|
c70cb8be07 | ||
|
|
7406f6c9f3 | ||
|
|
cbae68afdd | ||
|
|
495143e1a1 | ||
|
|
7585cf3f83 | ||
|
|
d521755a49 | ||
|
|
6f4ce80aea | ||
|
|
efda0fac11 | ||
|
|
600760884d | ||
|
|
3a4830ee57 | ||
|
|
582e00a6ef | ||
|
|
debba05d2f | ||
|
|
027a693c0d | ||
|
|
b5e1fc52a8 | ||
|
|
f17fe59590 | ||
|
|
69e3ed5ee4 | ||
|
|
3354842e81 | ||
|
|
e5e2a400ec | ||
|
|
4884087041 | ||
|
|
e1867b2bc0 | ||
|
|
4d0f9d86f6 | ||
|
|
ecda18029f | ||
|
|
7acac442a8 | ||
|
|
efc570e5a9 | ||
|
|
7955049337 | ||
|
|
8c6cb53dcb | ||
|
|
b6cfe3ba7c | ||
|
|
f836d4a42c | ||
|
|
f893322887 | ||
|
|
63ab1025e9 | ||
|
|
9a25a363d4 | ||
|
|
dc43d0b7c4 | ||
|
|
1513e5398f | ||
|
|
7c3543ba61 | ||
|
|
5adb5f3282 | ||
|
|
ffe343c65f | ||
|
|
fedd341970 | ||
|
|
c7c9aa0150 | ||
|
|
a2baa93801 | ||
|
|
e3e08414f6 | ||
|
|
ea9df45d2d | ||
|
|
7425f8beca | ||
|
|
8192fc2672 | ||
|
|
dbb7b89cf9 | ||
|
|
4f000485db | ||
|
|
c0884f484e | ||
|
|
363c997058 | ||
|
|
332385d605 | ||
|
|
0ccd80c8e7 | ||
|
|
ef8fe5ced2 | ||
|
|
4a531ede5c | ||
|
|
ff35ece65c | ||
|
|
66ea6e640d | ||
|
|
ecd06d7239 | ||
|
|
c89ff97ac5 | ||
|
|
ceec4c74fd | ||
|
|
925c1630c4 | ||
|
|
41c53f8f10 | ||
|
|
4eabc4ad52 | ||
|
|
c0859a29d3 | ||
|
|
c15707e875 | ||
|
|
8c476b45a0 | ||
|
|
f49229d73c | ||
|
|
fbccc031f4 | ||
|
|
d70fa741d8 | ||
|
|
4740bf1fc5 | ||
|
|
da57b29972 | ||
|
|
5944365bc6 | ||
|
|
a8908fd9b8 | ||
|
|
8ef7e62094 | ||
|
|
15c8eaaf29 | ||
|
|
e6648375c7 | ||
|
|
4251b15291 | ||
|
|
a52bc9e437 | ||
|
|
b78c081cd4 | ||
|
|
3f2cd08383 | ||
|
|
87050a2267 | ||
|
|
c8cb0731ff | ||
|
|
35dfaabc69 | ||
|
|
77ce39e157 | ||
|
|
0af8a43dc9 | ||
|
|
f203a2ac6f | ||
|
|
39af2ebbef | ||
|
|
620c8473b8 | ||
|
|
b2f476f833 | ||
|
|
c6d2f889fc | ||
|
|
f73783787d | ||
|
|
2ea2052769 | ||
|
|
64e8c83d9c | ||
|
|
b94c3caa3b | ||
|
|
1a710badef | ||
|
|
73fdec5928 | ||
|
|
13333999f3 | ||
|
|
87c48d7267 | ||
|
|
9b42fafdaf | ||
|
|
911f088df9 | ||
|
|
6fade3c3cc | ||
|
|
842ec105be | ||
|
|
df8508a38f | ||
|
|
191c75c281 | ||
|
|
1fb58be2b9 | ||
|
|
9d7c1de9dd | ||
|
|
195255bf0d | ||
|
|
6b182fc4e7 | ||
|
|
c7b2500518 | ||
|
|
1f1030766a | ||
|
|
8cb22efb83 | ||
|
|
ca66cc3499 | ||
|
|
6ae2a19d37 | ||
|
|
ffe4232c3b | ||
|
|
f6df85695d | ||
|
|
32e776203f | ||
|
|
c3c2f8815d | ||
|
|
fa9561d98a | ||
|
|
16a29f302d | ||
|
|
012005ddc7 | ||
|
|
27ffd6e944 | ||
|
|
815ea7abea | ||
|
|
40266af8b2 | ||
|
|
d3c77a4cda | ||
|
|
8e360491cf | ||
|
|
e546440575 | ||
|
|
902fdded65 | ||
|
|
8de375a4b0 | ||
|
|
c02e212b4c | ||
|
|
7436be9b44 | ||
|
|
2ad7932fbc | ||
|
|
7dd8e12791 | ||
|
|
c93b0b0df1 | ||
|
|
b49fd2d6a5 | ||
|
|
9c8812da70 | ||
|
|
9e6bf028ba | ||
|
|
aebd152ff9 | ||
|
|
5b9c5192fe | ||
|
|
e1334c5196 | ||
|
|
40fc7acbed |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -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
|
||||||
49
README.md
49
README.md
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
[](https://homebridge-slackin.herokuapp.com)
|
[](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.
|
||||||
|
|||||||
@@ -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.
|
|
||||||
@@ -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];
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
219
example-plugins/homebridge-samplePlatform/index.js
Normal file
219
example-plugins/homebridge-samplePlatform/index.js
Normal 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 = [];
|
||||||
|
}
|
||||||
20
example-plugins/homebridge-samplePlatform/package.json
Normal file
20
example-plugins/homebridge-samplePlatform/package.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "homebridge-samplePlatform",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "Sample Platform plugin for homebridge: https://github.com/nfarina/homebridge",
|
||||||
|
"license": "ISC",
|
||||||
|
"keywords": [
|
||||||
|
"homebridge-plugin"
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git://github.com/example/homebridge.git"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "http://github.com/example/homebridge/issues"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12.0",
|
||||||
|
"homebridge": ">=0.2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
69
lib/api.js
69
lib/api.js
@@ -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
96
lib/bridgeSetupManager.js
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
var inherits = require('util').inherits;
|
||||||
|
var EventEmitter = require('events').EventEmitter;
|
||||||
|
var Service = require("hap-nodejs").Service;
|
||||||
|
var Characteristic = require("hap-nodejs").Characteristic;
|
||||||
|
var SetupSession = require("./bridgeSetupSession").SetupSession;
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
BridgeSetupManager: BridgeSetupManager
|
||||||
|
}
|
||||||
|
|
||||||
|
function BridgeSetupManager() {
|
||||||
|
this.session;
|
||||||
|
|
||||||
|
this.service = new Service(null, "49FB9D4D-0FEA-4BF1-8FA6-E7B18AB86DCE");
|
||||||
|
|
||||||
|
this.stateCharacteristic = new Characteristic("State", "77474A2F-FA98-485E-97BE-4762458774D8", {
|
||||||
|
format: Characteristic.Formats.UINT8,
|
||||||
|
minValue: 0,
|
||||||
|
maxValue: 1,
|
||||||
|
minStep: 1,
|
||||||
|
perms: [Characteristic.Perms.READ, Characteristic.Perms.NOTIFY]
|
||||||
|
});
|
||||||
|
this.stateCharacteristic.value = 0;
|
||||||
|
this.service.addCharacteristic(this.stateCharacteristic);
|
||||||
|
|
||||||
|
this.versionCharacteristic = new Characteristic("Version", "FD9FE4CC-D06F-4FFE-96C6-595D464E1026", {
|
||||||
|
format: Characteristic.Formats.STRING,
|
||||||
|
perms: [Characteristic.Perms.READ, Characteristic.Perms.NOTIFY]
|
||||||
|
});
|
||||||
|
this.versionCharacteristic.value = "1.0";
|
||||||
|
this.service.addCharacteristic(this.versionCharacteristic);
|
||||||
|
|
||||||
|
this.controlPointCharacteristic = new Characteristic("Control Point", "5819A4C2-E1B0-4C9D-B761-3EB1AFF43073", {
|
||||||
|
format: Characteristic.Formats.DATA,
|
||||||
|
perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY]
|
||||||
|
})
|
||||||
|
this.controlPointCharacteristic.on('get', function(callback, context) {
|
||||||
|
this.handleReadRequest(callback, context);
|
||||||
|
}.bind(this));
|
||||||
|
this.controlPointCharacteristic.on('set', function(newValue, callback, context) {
|
||||||
|
this.handleWriteRequest(newValue, callback, context);
|
||||||
|
}.bind(this));
|
||||||
|
|
||||||
|
this.controlPointCharacteristic.value = null;
|
||||||
|
this.service.addCharacteristic(this.controlPointCharacteristic);
|
||||||
|
}
|
||||||
|
|
||||||
|
inherits(BridgeSetupManager, EventEmitter);
|
||||||
|
|
||||||
|
BridgeSetupManager.prototype.handleReadRequest = function(callback, context) {
|
||||||
|
if (!context) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.session) {
|
||||||
|
callback(null, null);
|
||||||
|
} else {
|
||||||
|
this.session.handleReadRequest(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BridgeSetupManager.prototype.handleWriteRequest = function(value, callback, context) {
|
||||||
|
if (!context) {
|
||||||
|
callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = new Buffer(value, 'base64');
|
||||||
|
var request = JSON.parse(data.toString());
|
||||||
|
callback();
|
||||||
|
|
||||||
|
if (!this.session || this.session.sessionUUID !== request.sid) {
|
||||||
|
if (this.session) {
|
||||||
|
this.session.removeAllListeners();
|
||||||
|
this.session.validSession = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.session = new SetupSession(this.stateCharacteristic, this.controlPointCharacteristic);
|
||||||
|
this.session.configurablePlatformPlugins = this.configurablePlatformPlugins;
|
||||||
|
this.session.on('newConfig', function(type, name, replace, config) {
|
||||||
|
this.emit('newConfig', type, name, replace, config);
|
||||||
|
}.bind(this));
|
||||||
|
|
||||||
|
this.session.on('requestCurrentConfig', function(callback) {
|
||||||
|
this.emit('requestCurrentConfig', callback);
|
||||||
|
}.bind(this));
|
||||||
|
|
||||||
|
this.session.on('end', function() {
|
||||||
|
this.session = null;
|
||||||
|
}.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.session.handleWriteRequest(request);
|
||||||
|
}
|
||||||
191
lib/bridgeSetupSession.js
Normal file
191
lib/bridgeSetupSession.js
Normal file
File diff suppressed because one or more lines are too long
@@ -30,7 +30,9 @@ module.exports = function() {
|
|||||||
process.on(signal, function () {
|
process.on(signal, function () {
|
||||||
log.info("Got %s, shutting down Homebridge...", signal);
|
log.info("Got %s, shutting down Homebridge...", signal);
|
||||||
|
|
||||||
// FIXME: Shut down server cleanly
|
// Save cached accessories to persist storage.
|
||||||
|
server._updateCachedAccessories();
|
||||||
|
|
||||||
process.exit(128 + signals[signal]);
|
process.exit(128 + signals[signal]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
228
lib/platformAccessory.js
Normal 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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
319
lib/server.js
319
lib/server.js
@@ -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,45 @@ 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
|
// 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
|
// 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,
|
// accessories. However you can set this to true to allow all requests without authentication,
|
||||||
@@ -41,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) {
|
||||||
@@ -115,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
|
||||||
@@ -149,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 || {};
|
||||||
@@ -208,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){
|
||||||
@@ -287,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(" "));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
18
lib/util/mac.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
var crypto = require('crypto');
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
generate: generate
|
||||||
|
}
|
||||||
|
|
||||||
|
function generate(data) {
|
||||||
|
var sha1sum = crypto.createHash('sha1');
|
||||||
|
sha1sum.update(data);
|
||||||
|
var s = sha1sum.digest('hex');
|
||||||
|
var i = -1;
|
||||||
|
return 'xx:xx:xx:xx:xx:xx'.replace(/[x]/g, function(c) {
|
||||||
|
i += 1;
|
||||||
|
return s[i];
|
||||||
|
}).toUpperCase();
|
||||||
|
};
|
||||||
16
package.json
16
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "homebridge",
|
"name": "homebridge",
|
||||||
"description": "HomeKit support for the impatient",
|
"description": "HomeKit support for the impatient",
|
||||||
"version": "0.2.19",
|
"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.5",
|
"hap-nodejs": "0.4.27",
|
||||||
"semver": "5.0.3"
|
"semver": "5.0.3",
|
||||||
|
"node-persist": "^0.0.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user