mirror of
https://github.com/mtan93/homebridge.git
synced 2026-03-08 05:31:55 +00:00
Compare commits
66 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff35ece65c | ||
|
|
66ea6e640d | ||
|
|
ecd06d7239 | ||
|
|
c89ff97ac5 | ||
|
|
ceec4c74fd | ||
|
|
925c1630c4 | ||
|
|
4eabc4ad52 | ||
|
|
c0859a29d3 | ||
|
|
c15707e875 | ||
|
|
8c476b45a0 | ||
|
|
f49229d73c | ||
|
|
fbccc031f4 | ||
|
|
d70fa741d8 | ||
|
|
4740bf1fc5 | ||
|
|
da57b29972 | ||
|
|
5944365bc6 | ||
|
|
a8908fd9b8 | ||
|
|
8ef7e62094 | ||
|
|
15c8eaaf29 | ||
|
|
e6648375c7 | ||
|
|
a52bc9e437 | ||
|
|
b78c081cd4 | ||
|
|
87050a2267 | ||
|
|
35dfaabc69 | ||
|
|
77ce39e157 | ||
|
|
0af8a43dc9 | ||
|
|
f203a2ac6f | ||
|
|
620c8473b8 | ||
|
|
b2f476f833 | ||
|
|
c6d2f889fc | ||
|
|
2ea2052769 | ||
|
|
64e8c83d9c | ||
|
|
13333999f3 | ||
|
|
87c48d7267 | ||
|
|
9b42fafdaf | ||
|
|
842ec105be | ||
|
|
df8508a38f | ||
|
|
9d7c1de9dd | ||
|
|
195255bf0d | ||
|
|
6b182fc4e7 | ||
|
|
c7b2500518 | ||
|
|
1f1030766a | ||
|
|
8cb22efb83 | ||
|
|
f6df85695d | ||
|
|
32e776203f | ||
|
|
c3c2f8815d | ||
|
|
012005ddc7 | ||
|
|
27ffd6e944 | ||
|
|
815ea7abea | ||
|
|
40266af8b2 | ||
|
|
d3c77a4cda | ||
|
|
8e360491cf | ||
|
|
e546440575 | ||
|
|
902fdded65 | ||
|
|
8de375a4b0 | ||
|
|
c02e212b4c | ||
|
|
7436be9b44 | ||
|
|
2ad7932fbc | ||
|
|
7dd8e12791 | ||
|
|
c93b0b0df1 | ||
|
|
b49fd2d6a5 | ||
|
|
9c8812da70 | ||
|
|
9e6bf028ba | ||
|
|
aebd152ff9 | ||
|
|
5b9c5192fe | ||
|
|
e1334c5196 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -9,4 +9,4 @@ npm-debug.log
|
||||
|
||||
# Ignore any extra plugins in the example directory that aren't in Git already
|
||||
# (this is a sandbox for the user)
|
||||
example-plugins
|
||||
example-plugins
|
||||
|
||||
44
README.md
44
README.md
@@ -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:
|
||||
|
||||
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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
**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:
|
||||
|
||||
@@ -79,15 +83,11 @@ You can explore all available plugins at the NPM website by [searching for the k
|
||||
|
||||
# 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.
|
||||
|
||||
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`.
|
||||
Using the Home app (or most other HomeKit apps), you should be able to add the single accessory "Homebridge", assuming that you're still running Homebridge and you're on the same Wifi network. Adding this accessory will automatically add all accessories and platforms defined in `config.json`.
|
||||
|
||||
When you attempt to add Homebridge, it will ask for a "PIN code". The default code is `031-45-154` (but this can be changed, see `config-sample.json`).
|
||||
|
||||
@@ -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.
|
||||
|
||||
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.
|
||||
|
||||
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:
|
||||
|
||||
homebridge.registerPlatform("homebridge-myplugin", "MyPlatform", MyPlatform);
|
||||
And you can find an example plugin that publishes an individual accessory at [here](https://github.com/nfarina/homebridge/tree/6500912f54a70ff479e63e2b72760ab589fa558a/example-plugins/homebridge-lockitron).
|
||||
|
||||
See more examples on how to create Platform classes in the [Legacy Plugins](https://github.com/nfarina/homebridge-legacy-plugins/tree/master/platforms) repository.
|
||||
|
||||
# Plugin Development
|
||||
|
||||
When writing your plugin, you'll want Homebridge to load it from your development directory instead of publishing it to `npm` each time. You can tell Homebridge to look for your plugin at a specific location using the command-line parameter `-P`. For example, if you are in the Homebridge directory (as checked out from Github), you might type:
|
||||
|
||||
```sh
|
||||
DEBUG=* ./bin/homebridge -D -P ../my-great-plugin/
|
||||
```
|
||||
|
||||
This will start up Homebridge and load your in-development plugin from a nearby directory. Note that you can also direct Homebridge to load your configuration from somewhere besides the default `~/.homebridge`, for example:
|
||||
|
||||
```sh
|
||||
DEBUG=* ./bin/homebridge -D -U ~/.homebridge-dev -P ../my-great-plugin/
|
||||
```
|
||||
|
||||
This is very useful when you are already using your development machine to host a "real" Homebridge instance (with all your accessories) that you don't want to disturb.
|
||||
|
||||
# Common Issues
|
||||
|
||||
### My iOS App Can't Find Homebridge
|
||||
@@ -143,3 +157,5 @@ Technically, the device manufacturers should be the ones implementing the HomeKi
|
||||
# Credit
|
||||
|
||||
The original HomeKit API work was done by [KhaosT](http://twitter.com/khaost) in his [HAP-NodeJS](https://github.com/KhaosT/HAP-NodeJS) project.
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
218
example-plugins/homebridge-samplePlatform/index.js
Normal file
218
example-plugins/homebridge-samplePlatform/index.js
Normal file
@@ -0,0 +1,218 @@
|
||||
var http = require('http');
|
||||
var Accessory, Service, Characteristic, UUIDGen;
|
||||
|
||||
module.exports = function(homebridge) {
|
||||
console.log("homebridge API version: " + homebridge.version);
|
||||
|
||||
// Accessory must be created from PlatformAccessory Constructor
|
||||
Accessory = homebridge.platformAccessory;
|
||||
|
||||
// Service and Characteristic are from hap-nodejs
|
||||
Service = homebridge.hap.Service;
|
||||
Characteristic = homebridge.hap.Characteristic;
|
||||
UUIDGen = homebridge.hap.uuid;
|
||||
|
||||
// For platform plugin to be considered as dynamic platform plugin,
|
||||
// registerPlatform(pluginName, platformName, constructor, dynamic), dynamic must be true
|
||||
homebridge.registerPlatform("homebridge-samplePlatform", "SamplePlatform", SamplePlatform, true);
|
||||
}
|
||||
|
||||
// Platform constructor
|
||||
// config may be null
|
||||
// api may be null if launched from old homebridge version
|
||||
function SamplePlatform(log, config, api) {
|
||||
log("SamplePlatform Init");
|
||||
var platform = this;
|
||||
this.log = log;
|
||||
this.config = config;
|
||||
this.accessories = [];
|
||||
|
||||
this.requestServer = http.createServer(function(request, response) {
|
||||
if (request.url === "/add") {
|
||||
this.addAccessory(new Date().toISOString());
|
||||
response.writeHead(204);
|
||||
response.end();
|
||||
}
|
||||
|
||||
if (request.url == "/reachability") {
|
||||
this.updateAccessoriesReachability();
|
||||
response.writeHead(204);
|
||||
response.end();
|
||||
}
|
||||
|
||||
if (request.url == "/remove") {
|
||||
this.removeAccessory();
|
||||
response.writeHead(204);
|
||||
response.end();
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
this.requestServer.listen(18081, function() {
|
||||
platform.log("Server Listening...");
|
||||
});
|
||||
|
||||
if (api) {
|
||||
// Save the API object as plugin needs to register new accessory via this object.
|
||||
this.api = api;
|
||||
|
||||
// Listen to event "didFinishLaunching", this means homebridge already finished loading cached accessories
|
||||
// Platform Plugin should only register new accessory that doesn't exist in homebridge after this event.
|
||||
// Or start discover new accessories
|
||||
this.api.on('didFinishLaunching', function() {
|
||||
platform.log("DidFinishLaunching");
|
||||
}.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
// Function invoked when homebridge tries to restore cached accessory
|
||||
// Developer can configure accessory at here (like setup event handler)
|
||||
// Update current value
|
||||
SamplePlatform.prototype.configureAccessory = function(accessory) {
|
||||
this.log(accessory.displayName, "Configure Accessory");
|
||||
var platform = this;
|
||||
|
||||
// set the accessory to reachable if plugin can currently process the accessory
|
||||
// otherwise set to false and update the reachability later by invoking
|
||||
// accessory.updateReachability()
|
||||
accessory.reachable = true;
|
||||
|
||||
accessory.on('identify', function(paired, callback) {
|
||||
platform.log(accessory.displayName, "Identify!!!");
|
||||
callback();
|
||||
});
|
||||
|
||||
if (accessory.getService(Service.Lightbulb)) {
|
||||
accessory.getService(Service.Lightbulb)
|
||||
.getCharacteristic(Characteristic.On)
|
||||
.on('set', function(value, callback) {
|
||||
platform.log(accessory.displayName, "Light -> " + value);
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
this.accessories.push(accessory);
|
||||
}
|
||||
|
||||
//Handler will be invoked when user try to config your plugin
|
||||
//Callback can be cached and invoke when nessary
|
||||
SamplePlatform.prototype.configurationRequestHandler = function(context, request, callback) {
|
||||
this.log("Context: ", JSON.stringify(context));
|
||||
this.log("Request: ", JSON.stringify(request));
|
||||
|
||||
// Check the request response
|
||||
if (request && request.response && request.response.inputs && request.response.inputs.name) {
|
||||
this.addAccessory(request.response.inputs.name);
|
||||
|
||||
// Invoke callback with config will let homebridge save the new config into config.json
|
||||
// Callback = function(response, type, replace, config)
|
||||
// set "type" to platform if the plugin is trying to modify platforms section
|
||||
// set "replace" to true will let homebridge replace existing config in config.json
|
||||
// "config" is the data platform trying to save
|
||||
callback(null, "platform", true, {"platform":"SamplePlatform", "otherConfig":"SomeData"});
|
||||
return;
|
||||
}
|
||||
|
||||
// - UI Type: Input
|
||||
// Can be used to request input from user
|
||||
// User response can be retrieved from request.response.inputs next time
|
||||
// when configurationRequestHandler being invoked
|
||||
|
||||
var respDict = {
|
||||
"type": "Interface",
|
||||
"interface": "input",
|
||||
"title": "Add Accessory",
|
||||
"items": [
|
||||
{
|
||||
"id": "name",
|
||||
"title": "Name",
|
||||
"placeholder": "Fancy Light"
|
||||
}//,
|
||||
// {
|
||||
// "id": "pw",
|
||||
// "title": "Password",
|
||||
// "secure": true
|
||||
// }
|
||||
]
|
||||
}
|
||||
|
||||
// - UI Type: List
|
||||
// Can be used to ask user to select something from the list
|
||||
// User response can be retrieved from request.response.selections next time
|
||||
// when configurationRequestHandler being invoked
|
||||
|
||||
// var respDict = {
|
||||
// "type": "Interface",
|
||||
// "interface": "list",
|
||||
// "title": "Select Something",
|
||||
// "allowMultipleSelection": true,
|
||||
// "items": [
|
||||
// "A","B","C"
|
||||
// ]
|
||||
// }
|
||||
|
||||
// - UI Type: Instruction
|
||||
// Can be used to ask user to do something (other than text input)
|
||||
// Hero image is base64 encoded image data. Not really sure the maximum length HomeKit allows.
|
||||
|
||||
// var respDict = {
|
||||
// "type": "Interface",
|
||||
// "interface": "instruction",
|
||||
// "title": "Almost There",
|
||||
// "detail": "Please press the button on the bridge to finish the setup.",
|
||||
// "heroImage": "base64 image data",
|
||||
// "showActivityIndicator": true,
|
||||
// "showNextButton": true,
|
||||
// "buttonText": "Login in browser",
|
||||
// "actionURL": "https://google.com"
|
||||
// }
|
||||
|
||||
// Plugin can set context to allow it track setup process
|
||||
context.ts = "Hello";
|
||||
|
||||
//invoke callback to update setup UI
|
||||
callback(respDict);
|
||||
}
|
||||
|
||||
// Sample function to show how developer can add accessory dynamically from outside event
|
||||
SamplePlatform.prototype.addAccessory = function(accessoryName) {
|
||||
this.log("Add Accessory");
|
||||
var platform = this;
|
||||
var uuid;
|
||||
|
||||
uuid = UUIDGen.generate(accessoryName);
|
||||
|
||||
var newAccessory = new Accessory(accessoryName, uuid);
|
||||
newAccessory.on('identify', function(paired, callback) {
|
||||
platform.log(accessory.displayName, "Identify!!!");
|
||||
callback();
|
||||
});
|
||||
// Plugin can save context on accessory
|
||||
// To help restore accessory in configureAccessory()
|
||||
// newAccessory.context.something = "Something"
|
||||
|
||||
newAccessory.addService(Service.Lightbulb, "Test Light")
|
||||
.getCharacteristic(Characteristic.On)
|
||||
.on('set', function(value, callback) {
|
||||
platform.log(accessory.displayName, "Light -> " + value);
|
||||
callback();
|
||||
});
|
||||
|
||||
this.accessories.push(newAccessory);
|
||||
this.api.registerPlatformAccessories("homebridge-samplePlatform", "SamplePlatform", [newAccessory]);
|
||||
}
|
||||
|
||||
SamplePlatform.prototype.updateAccessoriesReachability = function() {
|
||||
this.log("Update Reachability");
|
||||
for (var index in this.accessories) {
|
||||
var accessory = this.accessories[index];
|
||||
accessory.updateReachability(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Sample function to show how developer can remove accessory dynamically from outside event
|
||||
SamplePlatform.prototype.removeAccessory = function() {
|
||||
this.log("Remove Accessory");
|
||||
this.api.unregisterPlatformAccessories("homebridge-samplePlatform", "SamplePlatform", this.accessories);
|
||||
|
||||
this.accessories = [];
|
||||
}
|
||||
20
example-plugins/homebridge-samplePlatform/package.json
Normal file
20
example-plugins/homebridge-samplePlatform/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "homebridge-samplePlatform",
|
||||
"version": "0.0.1",
|
||||
"description": "Sample Platform plugin for homebridge: https://github.com/nfarina/homebridge",
|
||||
"license": "ISC",
|
||||
"keywords": [
|
||||
"homebridge-plugin"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/example/homebridge.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "http://github.com/example/homebridge/issues"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.12.0",
|
||||
"homebridge": ">=0.2.0"
|
||||
}
|
||||
}
|
||||
65
lib/api.js
65
lib/api.js
@@ -1,7 +1,10 @@
|
||||
var inherits = require('util').inherits;
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var hap = require("hap-nodejs");
|
||||
var hapLegacyTypes = require("hap-nodejs/accessories/types.js");
|
||||
var log = require("./logger")._system;
|
||||
var User = require("./user").User;
|
||||
var PlatformAccessory = require("./platformAccessory").PlatformAccessory;
|
||||
|
||||
// The official homebridge API is the object we feed the plugin's exported initializer function.
|
||||
|
||||
@@ -12,7 +15,13 @@ module.exports = {
|
||||
function API() {
|
||||
this._accessories = {}; // this._accessories[pluginName.accessoryName] = accessory constructor
|
||||
this._platforms = {}; // this._platforms[pluginName.platformName] = platform constructor
|
||||
|
||||
this._configurableAccessories = {};
|
||||
this._dynamicPlatforms = {}; // this._dynamicPlatforms[pluginName.platformName] = platform constructor
|
||||
|
||||
// expose the homebridge API version
|
||||
this.version = 2.1;
|
||||
|
||||
// expose the User class methods to plugins to get paths. Example: homebridge.user.storagePath()
|
||||
this.user = User;
|
||||
|
||||
@@ -24,8 +33,12 @@ function API() {
|
||||
// we also need to "bolt on" the legacy "types" constants for older accessories/platforms
|
||||
// still using the "object literal" style JSON.
|
||||
this.hapLegacyTypes = hapLegacyTypes;
|
||||
|
||||
this.platformAccessory = PlatformAccessory;
|
||||
}
|
||||
|
||||
inherits(API, EventEmitter);
|
||||
|
||||
API.prototype.accessory = function(name) {
|
||||
|
||||
// if you passed the "short form" name like "Lockitron" instead of "homebridge-lockitron.Lockitron",
|
||||
@@ -56,7 +69,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;
|
||||
|
||||
if (this._accessories[fullName])
|
||||
@@ -65,6 +78,23 @@ API.prototype.registerAccessory = function(pluginName, accessoryName, constructo
|
||||
log.info("Registering accessory '%s'", fullName);
|
||||
|
||||
this._accessories[fullName] = constructor;
|
||||
|
||||
// The plugin supports configuration
|
||||
if (configurationRequestHandler) {
|
||||
this._configurableAccessories[fullName] = configurationRequestHandler;
|
||||
}
|
||||
}
|
||||
|
||||
API.prototype.publishCameraAccessories = function(pluginName, accessories) {
|
||||
for (var index in accessories) {
|
||||
var accessory = accessories[index];
|
||||
if (!(accessory instanceof PlatformAccessory)) {
|
||||
throw new Error(pluginName + " attempt to register an accessory that isn\'t PlatformAccessory!");
|
||||
}
|
||||
accessory._associatedPlugin = pluginName;
|
||||
}
|
||||
|
||||
this.emit('publishCameraAccessories', accessories);
|
||||
}
|
||||
|
||||
API.prototype.platform = function(name) {
|
||||
@@ -97,7 +127,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;
|
||||
|
||||
if (this._platforms[fullName])
|
||||
@@ -106,4 +136,35 @@ API.prototype.registerPlatform = function(pluginName, platformName, constructor)
|
||||
log.info("Registering platform '%s'", fullName);
|
||||
|
||||
this._platforms[fullName] = constructor;
|
||||
|
||||
if (dynamic) {
|
||||
this._dynamicPlatforms[fullName] = constructor;
|
||||
}
|
||||
}
|
||||
|
||||
API.prototype.registerPlatformAccessories = function(pluginName, platformName, accessories) {
|
||||
for (var index in accessories) {
|
||||
var accessory = accessories[index];
|
||||
if (!(accessory instanceof PlatformAccessory)) {
|
||||
throw new Error(pluginName + " - " + platformName + " attempt to register an accessory that isn\'t PlatformAccessory!");
|
||||
}
|
||||
accessory._associatedPlugin = pluginName;
|
||||
accessory._associatedPlatform = platformName;
|
||||
}
|
||||
|
||||
this.emit('registerPlatformAccessories', accessories);
|
||||
}
|
||||
|
||||
API.prototype.updatePlatformAccessories = function(accessories) {
|
||||
this.emit('updatePlatformAccessories', accessories);
|
||||
}
|
||||
|
||||
API.prototype.unregisterPlatformAccessories = function(pluginName, platformName, accessories) {
|
||||
for (var index in accessories) {
|
||||
var accessory = accessories[index];
|
||||
if (!(accessory instanceof PlatformAccessory)) {
|
||||
throw new Error(pluginName + " - " + platformName + " attempt to unregister an accessory that isn\'t PlatformAccessory!");
|
||||
}
|
||||
}
|
||||
this.emit('unregisterPlatformAccessories', accessories);
|
||||
}
|
||||
96
lib/bridgeSetupManager.js
Normal file
96
lib/bridgeSetupManager.js
Normal file
@@ -0,0 +1,96 @@
|
||||
var inherits = require('util').inherits;
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var Service = require("hap-nodejs").Service;
|
||||
var Characteristic = require("hap-nodejs").Characteristic;
|
||||
var SetupSession = require("./bridgeSetupSession").SetupSession;
|
||||
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
BridgeSetupManager: BridgeSetupManager
|
||||
}
|
||||
|
||||
function BridgeSetupManager() {
|
||||
this.session;
|
||||
|
||||
this.service = new Service(null, "49FB9D4D-0FEA-4BF1-8FA6-E7B18AB86DCE");
|
||||
|
||||
this.stateCharacteristic = new Characteristic("State", "77474A2F-FA98-485E-97BE-4762458774D8", {
|
||||
format: Characteristic.Formats.UINT8,
|
||||
minValue: 0,
|
||||
maxValue: 1,
|
||||
minStep: 1,
|
||||
perms: [Characteristic.Perms.READ, Characteristic.Perms.NOTIFY]
|
||||
});
|
||||
this.stateCharacteristic.value = 0;
|
||||
this.service.addCharacteristic(this.stateCharacteristic);
|
||||
|
||||
this.versionCharacteristic = new Characteristic("Version", "FD9FE4CC-D06F-4FFE-96C6-595D464E1026", {
|
||||
format: Characteristic.Formats.STRING,
|
||||
perms: [Characteristic.Perms.READ, Characteristic.Perms.NOTIFY]
|
||||
});
|
||||
this.versionCharacteristic.value = "1.0";
|
||||
this.service.addCharacteristic(this.versionCharacteristic);
|
||||
|
||||
this.controlPointCharacteristic = new Characteristic("Control Point", "5819A4C2-E1B0-4C9D-B761-3EB1AFF43073", {
|
||||
format: Characteristic.Formats.DATA,
|
||||
perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY]
|
||||
})
|
||||
this.controlPointCharacteristic.on('get', function(callback, context) {
|
||||
this.handleReadRequest(callback, context);
|
||||
}.bind(this));
|
||||
this.controlPointCharacteristic.on('set', function(newValue, callback, context) {
|
||||
this.handleWriteRequest(newValue, callback, context);
|
||||
}.bind(this));
|
||||
|
||||
this.controlPointCharacteristic.value = null;
|
||||
this.service.addCharacteristic(this.controlPointCharacteristic);
|
||||
}
|
||||
|
||||
inherits(BridgeSetupManager, EventEmitter);
|
||||
|
||||
BridgeSetupManager.prototype.handleReadRequest = function(callback, context) {
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.session) {
|
||||
callback(null, null);
|
||||
} else {
|
||||
this.session.handleReadRequest(callback);
|
||||
}
|
||||
}
|
||||
|
||||
BridgeSetupManager.prototype.handleWriteRequest = function(value, callback, context) {
|
||||
if (!context) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
var data = new Buffer(value, 'base64');
|
||||
var request = JSON.parse(data.toString());
|
||||
callback();
|
||||
|
||||
if (!this.session || this.session.sessionUUID !== request.sid) {
|
||||
if (this.session) {
|
||||
this.session.removeAllListeners();
|
||||
this.session.validSession = false;
|
||||
}
|
||||
|
||||
this.session = new SetupSession(this.stateCharacteristic, this.controlPointCharacteristic);
|
||||
this.session.configurablePlatformPlugins = this.configurablePlatformPlugins;
|
||||
this.session.on('newConfig', function(type, name, replace, config) {
|
||||
this.emit('newConfig', type, name, replace, config);
|
||||
}.bind(this));
|
||||
|
||||
this.session.on('requestCurrentConfig', function(callback) {
|
||||
this.emit('requestCurrentConfig', callback);
|
||||
}.bind(this));
|
||||
|
||||
this.session.on('end', function() {
|
||||
this.session = null;
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
this.session.handleWriteRequest(request);
|
||||
}
|
||||
191
lib/bridgeSetupSession.js
Normal file
191
lib/bridgeSetupSession.js
Normal file
File diff suppressed because one or more lines are too long
@@ -30,7 +30,9 @@ module.exports = function() {
|
||||
process.on(signal, function () {
|
||||
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]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -65,6 +65,10 @@ Logger.prototype.log = function(level, msg) {
|
||||
if (this.prefix)
|
||||
msg = chalk.cyan("[" + this.prefix + "]") + " " + msg;
|
||||
|
||||
// prepend timestamp
|
||||
var date = new Date();
|
||||
msg = "[" + date.toLocaleString() + "]" + " " + msg;
|
||||
|
||||
func(msg);
|
||||
}
|
||||
|
||||
|
||||
226
lib/platformAccessory.js
Normal file
226
lib/platformAccessory.js
Normal file
@@ -0,0 +1,226 @@
|
||||
var uuid = require("hap-nodejs").uuid;
|
||||
var Accessory = require("hap-nodejs").Accessory;
|
||||
var Service = require("hap-nodejs").Service;
|
||||
var Characteristic = require("hap-nodejs").Characteristic;
|
||||
var inherits = require('util').inherits;
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
PlatformAccessory: PlatformAccessory
|
||||
}
|
||||
|
||||
function PlatformAccessory(displayName, UUID, category) {
|
||||
if (!displayName) throw new Error("Accessories must be created with a non-empty displayName.");
|
||||
if (!UUID) throw new Error("Accessories must be created with a valid UUID.");
|
||||
if (!uuid.isValid(UUID)) throw new Error("UUID '" + UUID + "' is not a valid UUID. Try using the provided 'generateUUID' function to create a valid UUID from any arbitrary string, like a serial number.");
|
||||
|
||||
this.displayName = displayName;
|
||||
this.UUID = UUID;
|
||||
this.category = category || Accessory.Categories.OTHER;
|
||||
this.services = [];
|
||||
this.reachable = false;
|
||||
this.context = {};
|
||||
|
||||
this._associatedPlugin;
|
||||
this._associatedPlatform;
|
||||
this._associatedHAPAccessory;
|
||||
|
||||
this
|
||||
.addService(Service.AccessoryInformation)
|
||||
.setCharacteristic(Characteristic.Name, displayName)
|
||||
.setCharacteristic(Characteristic.Manufacturer, "Default-Manufacturer")
|
||||
.setCharacteristic(Characteristic.Model, "Default-Model")
|
||||
.setCharacteristic(Characteristic.SerialNumber, "Default-SerialNumber");
|
||||
}
|
||||
|
||||
inherits(PlatformAccessory, EventEmitter);
|
||||
|
||||
PlatformAccessory.prototype.addService = function(service) {
|
||||
// service might be a constructor like `Service.AccessoryInformation` instead of an instance
|
||||
// of Service. Coerce if necessary.
|
||||
if (typeof service === 'function')
|
||||
service = new (Function.prototype.bind.apply(service, arguments));
|
||||
|
||||
// check for UUID+subtype conflict
|
||||
for (var index in this.services) {
|
||||
var existing = this.services[index];
|
||||
if (existing.UUID === service.UUID) {
|
||||
// OK we have two Services with the same UUID. Check that each defines a `subtype` property and that each is unique.
|
||||
if (!service.subtype)
|
||||
throw new Error("Cannot add a Service with the same UUID '" + existing.UUID + "' as another Service in this Accessory without also defining a unique 'subtype' property.");
|
||||
|
||||
if (service.subtype.toString() === existing.subtype.toString())
|
||||
throw new Error("Cannot add a Service with the same UUID '" + existing.UUID + "' and subtype '" + existing.subtype + "' as another Service in this Accessory.");
|
||||
}
|
||||
}
|
||||
|
||||
this.services.push(service);
|
||||
|
||||
if (this._associatedHAPAccessory) {
|
||||
this._associatedHAPAccessory.addService(service);
|
||||
}
|
||||
return service;
|
||||
}
|
||||
|
||||
PlatformAccessory.prototype.removeService = function(service) {
|
||||
var targetServiceIndex;
|
||||
|
||||
for (var index in this.services) {
|
||||
var existingService = this.services[index];
|
||||
|
||||
if (existingService === service) {
|
||||
targetServiceIndex = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetServiceIndex) {
|
||||
this.services.splice(targetServiceIndex, 1);
|
||||
service.removeAllListeners();
|
||||
|
||||
if (this._associatedHAPAccessory) {
|
||||
this._associatedHAPAccessory.removeService(service);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* searchs for a Service in the services collection and returns the first Service object that matches.
|
||||
* If multiple services of the same type are present in one accessory, use getServiceByUUIDAndSubType instead.
|
||||
* @param {ServiceConstructor|string} name
|
||||
* @returns Service
|
||||
*/
|
||||
PlatformAccessory.prototype.getService = function(name) {
|
||||
for (var index in this.services) {
|
||||
var service = this.services[index];
|
||||
|
||||
if (typeof name === 'string' && (service.displayName === name || service.name === name))
|
||||
return service;
|
||||
else if (typeof name === 'function' && ((service instanceof name) || (name.UUID === service.UUID)))
|
||||
return service;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* searchs for a Service in the services collection and returns the first Service object that matches.
|
||||
* If multiple services of the same type are present in one accessory, use getServiceByUUIDAndSubType instead.
|
||||
* @param {string} UUID Can be an UUID, a service.displayName, or a constructor of a Service
|
||||
* @param {string} subtype A subtype string to match
|
||||
* @returns Service
|
||||
*/
|
||||
PlatformAccessory.prototype.getServiceByUUIDAndSubType = function(UUID, subtype) {
|
||||
for (var index in this.services) {
|
||||
var service = this.services[index];
|
||||
|
||||
if (typeof UUID === 'string' && (service.displayName === UUID || service.name === UUID) && service.subtype === subtype )
|
||||
return service;
|
||||
else if (typeof UUID === 'function' && ((service instanceof UUID) || (UUID.UUID === service.UUID)) && service.subtype === subtype)
|
||||
return service;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
PlatformAccessory.prototype.updateReachability = function(reachable) {
|
||||
this.reachable = reachable;
|
||||
|
||||
if (this._associatedHAPAccessory) {
|
||||
this._associatedHAPAccessory.updateReachability(reachable);
|
||||
}
|
||||
}
|
||||
|
||||
PlatformAccessory.prototype.configureCameraSource = function(cameraSource) {
|
||||
this.cameraSource = cameraSource;
|
||||
for (var index in cameraSource.services) {
|
||||
var service = cameraSource.services[index];
|
||||
this.addService(service);
|
||||
}
|
||||
}
|
||||
|
||||
PlatformAccessory.prototype._prepareAssociatedHAPAccessory = function () {
|
||||
this._associatedHAPAccessory = new Accessory(this.displayName, this.UUID);
|
||||
|
||||
if (this.cameraSource) {
|
||||
this._associatedHAPAccessory.configureCameraSource(this.cameraSource);
|
||||
}
|
||||
|
||||
this._associatedHAPAccessory._sideloadServices(this.services);
|
||||
this._associatedHAPAccessory.category = this.category;
|
||||
this._associatedHAPAccessory.reachable = this.reachable;
|
||||
this._associatedHAPAccessory.on('identify', function(paired, callback) {
|
||||
if (this.listeners('identify').length > 0) {
|
||||
// allow implementors to identify this Accessory in whatever way is appropriate, and pass along
|
||||
// the standard callback for completion.
|
||||
this.emit('identify', paired, callback);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
PlatformAccessory.prototype._dictionaryPresentation = function() {
|
||||
var accessory = {};
|
||||
|
||||
accessory.plugin = this._associatedPlugin;
|
||||
accessory.platform = this._associatedPlatform;
|
||||
accessory.displayName = this.displayName;
|
||||
accessory.UUID = this.UUID;
|
||||
accessory.category = this.category;
|
||||
accessory.context = this.context;
|
||||
|
||||
var services = [];
|
||||
for (var index in this.services) {
|
||||
var service = this.services[index];
|
||||
var servicePresentation = {};
|
||||
servicePresentation.displayName = service.displayName;
|
||||
servicePresentation.UUID = service.UUID;
|
||||
servicePresentation.subtype = service.subtype;
|
||||
|
||||
var characteristics = [];
|
||||
for (var cIndex in service.characteristics) {
|
||||
var characteristic = service.characteristics[cIndex];
|
||||
var characteristicPresentation = {};
|
||||
characteristicPresentation.displayName = characteristic.displayName;
|
||||
characteristicPresentation.UUID = characteristic.UUID;
|
||||
characteristicPresentation.props = characteristic.props;
|
||||
characteristicPresentation.value = characteristic.value;
|
||||
characteristics.push(characteristicPresentation);
|
||||
}
|
||||
|
||||
servicePresentation.characteristics = characteristics;
|
||||
services.push(servicePresentation);
|
||||
}
|
||||
|
||||
accessory.services = services;
|
||||
return accessory;
|
||||
}
|
||||
|
||||
PlatformAccessory.prototype._configFromData = function(data) {
|
||||
this._associatedPlugin = data.plugin;
|
||||
this._associatedPlatform = data.platform;
|
||||
this.displayName = data.displayName;
|
||||
this.UUID = data.UUID;
|
||||
this.category = data.category;
|
||||
this.context = data.context;
|
||||
this.reachable = false;
|
||||
|
||||
var services = [];
|
||||
for (var index in data.services) {
|
||||
var service = data.services[index];
|
||||
var hapService = new Service(service.displayName, service.UUID, service.subtype);
|
||||
|
||||
var characteristics = [];
|
||||
for (var cIndex in service.characteristics) {
|
||||
var characteristic = service.characteristics[cIndex];
|
||||
var hapCharacteristic = new Characteristic(characteristic.displayName, characteristic.UUID, characteristic.props);
|
||||
hapCharacteristic.value = characteristic.value;
|
||||
characteristics.push(hapCharacteristic);
|
||||
}
|
||||
|
||||
hapService._sideloadCharacteristics(characteristics);
|
||||
services.push(hapService);
|
||||
}
|
||||
|
||||
this.services = services;
|
||||
}
|
||||
303
lib/server.js
303
lib/server.js
@@ -1,6 +1,7 @@
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
var uuid = require("hap-nodejs").uuid;
|
||||
var accessoryStorage = require('node-persist').create();
|
||||
var Bridge = require("hap-nodejs").Bridge;
|
||||
var Accessory = require("hap-nodejs").Accessory;
|
||||
var Service = require("hap-nodejs").Service;
|
||||
@@ -10,8 +11,11 @@ var once = require("hap-nodejs/lib/util/once").once;
|
||||
var Plugin = require('./plugin').Plugin;
|
||||
var User = require('./user').User;
|
||||
var API = require('./api').API;
|
||||
var PlatformAccessory = require("./platformAccessory").PlatformAccessory;
|
||||
var BridgeSetupManager = require("./bridgeSetupManager").BridgeSetupManager;
|
||||
var log = require("./logger")._system;
|
||||
var Logger = require('./logger').Logger;
|
||||
var mac = require("./util/mac");
|
||||
|
||||
'use strict';
|
||||
|
||||
@@ -20,11 +24,42 @@ module.exports = {
|
||||
}
|
||||
|
||||
function Server(insecureAccess) {
|
||||
// Setup Accessory Cache Storage
|
||||
accessoryStorage.initSync({ dir: User.cachedAccessoryPath() });
|
||||
|
||||
this._api = new API(); // object we feed to Plugins
|
||||
|
||||
this._api.on('registerPlatformAccessories', function(accessories) {
|
||||
this._handleRegisterPlatformAccessories(accessories);
|
||||
}.bind(this));
|
||||
|
||||
this._api.on('updatePlatformAccessories', function(accessories) {
|
||||
this._handleUpdatePlatformAccessories(accessories);
|
||||
}.bind(this));
|
||||
|
||||
this._api.on('unregisterPlatformAccessories', function(accessories) {
|
||||
this._handleUnregisterPlatformAccessories(accessories);
|
||||
}.bind(this));
|
||||
|
||||
this._api.on('publishCameraAccessories', function(accessories) {
|
||||
this._handlePublishCameraAccessories(accessories);
|
||||
}.bind(this));
|
||||
|
||||
this._plugins = this._loadPlugins(); // plugins[name] = Plugin instance
|
||||
this._config = this._loadConfig();
|
||||
this._config = this._loadConfig();
|
||||
this._cachedPlatformAccessories = this._loadCachedPlatformAccessories();
|
||||
this._bridge = this._createBridge();
|
||||
|
||||
this._activeDynamicPlugins = {};
|
||||
this._configurablePlatformPlugins = {};
|
||||
this._publishedCameras = {};
|
||||
this._setupManager = new BridgeSetupManager();
|
||||
this._setupManager.on('newConfig', this._handleNewConfig.bind(this));
|
||||
|
||||
this._setupManager.on('requestCurrentConfig', function(callback) {
|
||||
callback(this._config);
|
||||
}.bind(this));
|
||||
|
||||
// Server is "secure by default", meaning it creates a top-level Bridge accessory that
|
||||
// will not allow unauthenticated requests. This matches the behavior of actual HomeKit
|
||||
// accessories. However you can set this to true to allow all requests without authentication,
|
||||
@@ -41,27 +76,43 @@ Server.prototype.run = function() {
|
||||
|
||||
if (this._config.platforms) this._loadPlatforms();
|
||||
if (this._config.accessories) this._loadAccessories();
|
||||
this._loadDynamicPlatforms();
|
||||
this._configCachedPlatformAccessories();
|
||||
this._setupManager.configurablePlatformPlugins = this._configurablePlatformPlugins;
|
||||
this._bridge.addService(this._setupManager.service);
|
||||
|
||||
this._asyncWait = false;
|
||||
|
||||
// publish now unless we're waiting on anyone
|
||||
if (this._asyncCalls == 0)
|
||||
this._publish();
|
||||
|
||||
this._api.emit('didFinishLaunching');
|
||||
}
|
||||
|
||||
Server.prototype._publish = function() {
|
||||
// pull out our custom Bridge settings from config.json, if any
|
||||
var bridgeConfig = this._config.bridge || {};
|
||||
|
||||
var info = this._bridge.getService(Service.AccessoryInformation);
|
||||
if (bridgeConfig.manufacturer)
|
||||
info.setCharacteristic(Characteristic.Manufacturer, bridgeConfig.manufacturer);
|
||||
if (bridgeConfig.model)
|
||||
info.setCharacteristic(Characteristic.Model, bridgeConfig.model);
|
||||
if (bridgeConfig.serialNumber)
|
||||
info.setCharacteristic(Characteristic.SerialNumber, bridgeConfig.serialNumber);
|
||||
|
||||
this._printPin(bridgeConfig.pin);
|
||||
|
||||
this._bridge.on('listening', function(port) {
|
||||
log.info("Homebridge is running on port %s.", port);
|
||||
});
|
||||
|
||||
this._bridge.publish({
|
||||
username: bridgeConfig.username || "CC:22:3D:E3:CE:30",
|
||||
port: bridgeConfig.port || 51826,
|
||||
pincode: bridgeConfig.pin || "031-45-154",
|
||||
category: Accessory.Categories.BRIDGE
|
||||
}, this._allowInsecureAccess);
|
||||
|
||||
log.info("Homebridge is running on port %s.", bridgeConfig.port || 51826);
|
||||
}
|
||||
|
||||
Server.prototype._loadPlugins = function(accessories, platforms) {
|
||||
@@ -115,8 +166,19 @@ Server.prototype._loadConfig = function() {
|
||||
|
||||
// Complain and exit if it doesn't exist yet
|
||||
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.");
|
||||
process.exit(1);
|
||||
log.warn("config.json (%s) not found.", configPath);
|
||||
|
||||
var config = {};
|
||||
|
||||
config.bridge = {
|
||||
"name": "Homebridge",
|
||||
"username": "CC:22:3D:E3:CE:30",
|
||||
"pin": "031-45-154"
|
||||
};
|
||||
|
||||
return config;
|
||||
// log.error("Couldn't find a config.json file at '"+configPath+"'. Look at config-sample.json for examples of how to format your config.js and add your home accessories.");
|
||||
// process.exit(1);
|
||||
}
|
||||
|
||||
// Load up the configuration file
|
||||
@@ -149,6 +211,23 @@ Server.prototype._loadConfig = function() {
|
||||
return config;
|
||||
}
|
||||
|
||||
Server.prototype._loadCachedPlatformAccessories = function() {
|
||||
var cachedAccessories = accessoryStorage.getItem("cachedAccessories");
|
||||
var platformAccessories = [];
|
||||
|
||||
if (cachedAccessories) {
|
||||
for (var index in cachedAccessories) {
|
||||
var serializedAccessory = cachedAccessories[index];
|
||||
var platformAccessory = new PlatformAccessory(serializedAccessory.displayName, serializedAccessory.UUID, serializedAccessory.category);
|
||||
platformAccessory._configFromData(serializedAccessory);
|
||||
|
||||
platformAccessories.push(platformAccessory);
|
||||
}
|
||||
}
|
||||
|
||||
return platformAccessories;
|
||||
}
|
||||
|
||||
Server.prototype._createBridge = function() {
|
||||
// pull out our custom Bridge settings from config.json, if any
|
||||
var bridgeConfig = this._config.bridge || {};
|
||||
@@ -208,11 +287,64 @@ Server.prototype._loadPlatforms = function() {
|
||||
|
||||
platformLogger("Initializing %s platform...", platformType);
|
||||
|
||||
var platformInstance = new platformConstructor(platformLogger, platformConfig);
|
||||
this._loadPlatformAccessories(platformInstance, platformLogger, platformType);
|
||||
var platformInstance = new platformConstructor(platformLogger, platformConfig, this._api);
|
||||
|
||||
if (platformInstance.configureAccessory == undefined) {
|
||||
// Plugin 1.0, load accessories
|
||||
this._loadPlatformAccessories(platformInstance, platformLogger, platformType);
|
||||
} else {
|
||||
this._activeDynamicPlugins[platformType] = platformInstance;
|
||||
}
|
||||
|
||||
if (platformInstance.configurationRequestHandler != undefined) {
|
||||
this._configurablePlatformPlugins[platformType] = platformInstance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Server.prototype._loadDynamicPlatforms = function() {
|
||||
for (var dynamicPluginName in this._api._dynamicPlatforms) {
|
||||
if (!this._activeDynamicPlugins[dynamicPluginName] && !this._activeDynamicPlugins[dynamicPluginName.split(".")[1]]) {
|
||||
console.log("Load " + dynamicPluginName);
|
||||
var platformConstructor = this._api._dynamicPlatforms[dynamicPluginName];
|
||||
var platformLogger = Logger.withPrefix(dynamicPluginName);
|
||||
var platformInstance = new platformConstructor(platformLogger, null, this._api);
|
||||
this._activeDynamicPlugins[dynamicPluginName] = platformInstance;
|
||||
|
||||
if (platformInstance.configurationRequestHandler != undefined) {
|
||||
this._configurablePlatformPlugins[dynamicPluginName] = platformInstance;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Server.prototype._configCachedPlatformAccessories = function() {
|
||||
for (var index in this._cachedPlatformAccessories) {
|
||||
var accessory = this._cachedPlatformAccessories[index];
|
||||
|
||||
if (!(accessory instanceof PlatformAccessory)) {
|
||||
console.log("Unexpected Accessory!");
|
||||
continue;
|
||||
}
|
||||
|
||||
var fullName = accessory._associatedPlugin + "." + accessory._associatedPlatform;
|
||||
var platformInstance = this._activeDynamicPlugins[fullName];
|
||||
|
||||
if (!platformInstance) {
|
||||
platformInstance = this._activeDynamicPlugins[accessory._associatedPlatform];
|
||||
}
|
||||
|
||||
if (platformInstance) {
|
||||
platformInstance.configureAccessory(accessory);
|
||||
} else {
|
||||
console.log("Failed to find plugin to handle accessory " + accessory.displayName);
|
||||
}
|
||||
|
||||
accessory._prepareAssociatedHAPAccessory();
|
||||
this._bridge.addBridgedAccessory(accessory._associatedHAPAccessory);
|
||||
}
|
||||
}
|
||||
|
||||
Server.prototype._loadPlatformAccessories = function(platformInstance, log, platformType) {
|
||||
this._asyncCalls++;
|
||||
platformInstance.accessories(once(function(foundAccessories){
|
||||
@@ -287,6 +419,161 @@ 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 %s experienced an address collision.", accessory.displayName);
|
||||
} else {
|
||||
this._publishedCameras[advertiseAddress] = accessory;
|
||||
}
|
||||
|
||||
hapAccessory.on('listening', function(port) {
|
||||
log.info("%s is running on port %s.", accessory.displayName, port);
|
||||
});
|
||||
|
||||
hapAccessory.publish({
|
||||
username: advertiseAddress,
|
||||
pincode: accessoryPin,
|
||||
category: accessory.category
|
||||
}, this._allowInsecureAccess);
|
||||
}
|
||||
}
|
||||
|
||||
Server.prototype._updateCachedAccessories = function() {
|
||||
var serializedAccessories = [];
|
||||
|
||||
for (var index in this._cachedPlatformAccessories) {
|
||||
var accessory = this._cachedPlatformAccessories[index];
|
||||
serializedAccessories.push(accessory._dictionaryPresentation());
|
||||
}
|
||||
|
||||
accessoryStorage.setItemSync("cachedAccessories", serializedAccessories);
|
||||
}
|
||||
|
||||
Server.prototype._handleNewConfig = function(type, name, replace, config) {
|
||||
if (type === "accessory") {
|
||||
// TODO: Load new accessory
|
||||
if (!this._config.accessories) {
|
||||
this._config.accessories = [];
|
||||
}
|
||||
|
||||
if (!replace) {
|
||||
this._config.accessories.push(config);
|
||||
} else {
|
||||
var targetName;
|
||||
if (name.indexOf('.') !== -1) {
|
||||
targetName = name.split(".")[1];
|
||||
}
|
||||
var found = false;
|
||||
for (var index in this._config.accessories) {
|
||||
var accessoryConfig = this._config.accessories[index];
|
||||
if (accessoryConfig.accessory === name) {
|
||||
this._config.accessories[index] = config;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (targetName && (accessoryConfig.accessory === targetName)) {
|
||||
this._config.accessories[index] = config;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
this._config.accessories.push(config);
|
||||
}
|
||||
}
|
||||
} else if (type === "platform") {
|
||||
if (!this._config.platforms) {
|
||||
this._config.platforms = [];
|
||||
}
|
||||
|
||||
if (!replace) {
|
||||
this._config.platforms.push(config);
|
||||
} else {
|
||||
var targetName;
|
||||
if (name.indexOf('.') !== -1) {
|
||||
targetName = name.split(".")[1];
|
||||
}
|
||||
|
||||
var found = false;
|
||||
for (var index in this._config.platforms) {
|
||||
var platformConfig = this._config.platforms[index];
|
||||
if (platformConfig.platform === name) {
|
||||
this._config.platforms[index] = config;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (targetName && (platformConfig.platform === targetName)) {
|
||||
this._config.platforms[index] = config;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
this._config.platforms.push(config);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var serializedConfig = JSON.stringify(this._config, null, ' ');
|
||||
var configPath = User.configPath();
|
||||
fs.writeFileSync(configPath, serializedConfig, 'utf8');
|
||||
}
|
||||
|
||||
// Returns the setup code in a scannable format.
|
||||
Server.prototype._printPin = function(pin) {
|
||||
console.log("Scan this code with your HomeKit App on your iOS device to pair with Homebridge:");
|
||||
|
||||
@@ -38,6 +38,10 @@ User.persistPath = function() {
|
||||
return path.join(User.storagePath(), "persist");
|
||||
}
|
||||
|
||||
User.cachedAccessoryPath = function() {
|
||||
return path.join(User.storagePath(), "accessories");
|
||||
}
|
||||
|
||||
User.setStoragePath = function(path) {
|
||||
customStoragePath = path;
|
||||
}
|
||||
18
lib/util/mac.js
Normal file
18
lib/util/mac.js
Normal file
@@ -0,0 +1,18 @@
|
||||
var crypto = require('crypto');
|
||||
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
generate: generate
|
||||
}
|
||||
|
||||
function generate(data) {
|
||||
var sha1sum = crypto.createHash('sha1');
|
||||
sha1sum.update(data);
|
||||
var s = sha1sum.digest('hex');
|
||||
var i = -1;
|
||||
return 'xx:xx:xx:xx:xx:xx'.replace(/[x]/g, function(c) {
|
||||
i += 1;
|
||||
return s[i];
|
||||
}).toUpperCase();
|
||||
};
|
||||
16
package.json
16
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "homebridge",
|
||||
"description": "HomeKit support for the impatient",
|
||||
"version": "0.2.19",
|
||||
"version": "0.4.0",
|
||||
"scripts": {
|
||||
"dev": "DEBUG=* ./bin/homebridge -D -P example-plugins/ || true"
|
||||
},
|
||||
@@ -15,23 +15,19 @@
|
||||
"bugs": {
|
||||
"url": "http://github.com/nfarina/homebridge/issues"
|
||||
},
|
||||
"licenses": [
|
||||
{
|
||||
"type": "ISC",
|
||||
"url": "http://github.com/nfarina/homebridge/blob/master/LICENSE"
|
||||
}
|
||||
],
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"homebridge": "bin/homebridge"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
"node": ">=4.3.2"
|
||||
},
|
||||
"preferGlobal": true,
|
||||
"dependencies": {
|
||||
"chalk": "^1.1.1",
|
||||
"commander": "2.8.1",
|
||||
"hap-nodejs": "0.2.5",
|
||||
"semver": "5.0.3"
|
||||
"hap-nodejs": "0.4.4",
|
||||
"semver": "5.0.3",
|
||||
"node-persist": "^0.0.8"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user