mirror of
https://github.com/mtan93/homebridge.git
synced 2026-03-08 05:31:55 +00:00
@@ -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) {
|
||||
console.log("SamplePlatform Init");
|
||||
this.log = log;
|
||||
this.config = config;
|
||||
this.accessories = [];
|
||||
|
||||
this.requestServer = http.createServer(function(request, response) {
|
||||
if (request.url === "/add") {
|
||||
this.addAccessory();
|
||||
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() {
|
||||
console.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() {
|
||||
console.log("Plugin - 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) {
|
||||
console.log("Plugin - Configure Accessory: " + accessory.displayName);
|
||||
|
||||
// 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) {
|
||||
console.log("Identify!!!");
|
||||
callback();
|
||||
});
|
||||
|
||||
if (accessory.getService(Service.Lightbulb)) {
|
||||
accessory.getService(Service.Lightbulb)
|
||||
.getCharacteristic(Characteristic.On)
|
||||
.on('set', function(value, callback) {
|
||||
console.log("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) {
|
||||
console.log("Context: ", JSON.stringify(context));
|
||||
console.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) {
|
||||
console.log("Add Accessory");
|
||||
var uuid;
|
||||
|
||||
if (!accessoryName) {
|
||||
accessoryName = "Test Accessory"
|
||||
}
|
||||
|
||||
uuid = UUIDGen.generate(accessoryName);
|
||||
|
||||
var newAccessory = new Accessory(accessoryName, uuid);
|
||||
newAccessory.on('identify', function(paired, callback) {
|
||||
console.log("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) {
|
||||
console.log("Light -> " + value);
|
||||
callback();
|
||||
});
|
||||
|
||||
this.accessories.push(newAccessory);
|
||||
this.api.registerPlatformAccessories("homebridge-samplePlatform", "SamplePlatform", [newAccessory]);
|
||||
}
|
||||
|
||||
SamplePlatform.prototype.updateAccessoriesReachability = function() {
|
||||
console.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() {
|
||||
console.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"
|
||||
}
|
||||
}
|
||||
53
lib/api.js
53
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.
|
||||
|
||||
@@ -13,6 +16,12 @@ 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.0;
|
||||
|
||||
// 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,11 @@ 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.platform = function(name) {
|
||||
@@ -97,7 +115,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 +124,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]);
|
||||
});
|
||||
});
|
||||
|
||||
212
lib/platformAccessory.js
Normal file
212
lib/platformAccessory.js
Normal file
@@ -0,0 +1,212 @@
|
||||
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._prepareAssociatedHAPAccessory = function () {
|
||||
this._associatedHAPAccessory = new Accessory(this.displayName, this.UUID);
|
||||
this._associatedHAPAccessory._sideloadServices(this.services);
|
||||
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;
|
||||
}
|
||||
250
lib/server.js
250
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,6 +11,8 @@ 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;
|
||||
|
||||
@@ -20,11 +23,37 @@ 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._plugins = this._loadPlugins(); // plugins[name] = Plugin instance
|
||||
this._config = this._loadConfig();
|
||||
this._cachedPlatformAccessories = this._loadCachedPlatformAccessories();
|
||||
this._bridge = this._createBridge();
|
||||
|
||||
this._activeDynamicPlugins = {};
|
||||
this._configurablePlatformPlugins = {};
|
||||
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,12 +70,18 @@ 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() {
|
||||
@@ -115,8 +150,18 @@ 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);
|
||||
var config = {};
|
||||
|
||||
config.bridge = {
|
||||
"name": "Homebridge",
|
||||
"username": "CC:22:3D:E3:CE:30",
|
||||
"port": 51826,
|
||||
"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 +194,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 +270,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 +402,133 @@ 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._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;
|
||||
}
|
||||
12
package.json
12
package.json
@@ -15,12 +15,7 @@
|
||||
"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"
|
||||
},
|
||||
@@ -31,7 +26,8 @@
|
||||
"dependencies": {
|
||||
"chalk": "^1.1.1",
|
||||
"commander": "2.8.1",
|
||||
"hap-nodejs": "0.2.5",
|
||||
"semver": "5.0.3"
|
||||
"hap-nodejs": "0.2.6",
|
||||
"semver": "5.0.3",
|
||||
"node-persist": "^0.0.8"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user