Merge pull request #497 from nfarina/plugin-2

Plugin 2.0
This commit is contained in:
Khaos Tian
2016-03-01 18:33:27 -08:00
14 changed files with 1048 additions and 125 deletions

2
.gitignore vendored
View File

@@ -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

View File

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

View File

@@ -1,82 +0,0 @@
var request = require("request");
var Service, Characteristic;
module.exports = function(homebridge) {
Service = homebridge.hap.Service;
Characteristic = homebridge.hap.Characteristic;
homebridge.registerAccessory("homebridge-lockitron", "Lockitron", LockitronAccessory);
}
function LockitronAccessory(log, config) {
this.log = log;
this.name = config["name"];
this.accessToken = config["api_token"];
this.lockID = config["lock_id"];
this.service = new Service.LockMechanism(this.name);
this.service
.getCharacteristic(Characteristic.LockCurrentState)
.on('get', this.getState.bind(this));
this.service
.getCharacteristic(Characteristic.LockTargetState)
.on('get', this.getState.bind(this))
.on('set', this.setState.bind(this));
}
LockitronAccessory.prototype.getState = function(callback) {
this.log("Getting current state...");
request.get({
url: "https://api.lockitron.com/v2/locks/"+this.lockID,
qs: { access_token: this.accessToken }
}, function(err, response, body) {
if (!err && response.statusCode == 200) {
var json = JSON.parse(body);
var state = json.state; // "lock" or "unlock"
this.log("Lock state is %s", state);
var locked = state == "lock"
callback(null, locked); // success
}
else {
this.log("Error getting state (status code %s): %s", response.statusCode, err);
callback(err);
}
}.bind(this));
}
LockitronAccessory.prototype.setState = function(state, callback) {
var lockitronState = (state == Characteristic.LockTargetState.SECURED) ? "lock" : "unlock";
this.log("Set state to %s", lockitronState);
request.put({
url: "https://api.lockitron.com/v2/locks/"+this.lockID,
qs: { access_token: this.accessToken, state: lockitronState }
}, function(err, response, body) {
if (!err && response.statusCode == 200) {
this.log("State change complete.");
// we succeeded, so update the "current" state as well
var currentState = (state == Characteristic.LockTargetState.SECURED) ?
Characteristic.LockCurrentState.SECURED : Characteristic.LockCurrentState.UNSECURED;
this.service
.setCharacteristic(Characteristic.LockCurrentState, currentState);
callback(null); // success
}
else {
this.log("Error '%s' setting lock state. Response: %s", err, body);
callback(err || new Error("Error setting lock state."));
}
}.bind(this));
}
LockitronAccessory.prototype.getServices = function() {
return [this.service];
}

View File

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

View File

@@ -0,0 +1,219 @@
var http = require('http');
var Accessory, Service, Characteristic, UUIDGen;
module.exports = function(homebridge) {
console.log("homebridge API version: " + homebridge.version);
// Accessory must be created from PlatformAccessory Constructor
Accessory = homebridge.platformAccessory;
// Service and Characteristic are from hap-nodejs
Service = homebridge.hap.Service;
Characteristic = homebridge.hap.Characteristic;
UUIDGen = homebridge.hap.uuid;
// For platform plugin to be considered as dynamic platform plugin,
// registerPlatform(pluginName, platformName, constructor, dynamic), dynamic must be true
homebridge.registerPlatform("homebridge-samplePlatform", "SamplePlatform", SamplePlatform, true);
}
// Platform constructor
// config may be null
// api may be null if launched from old homebridge version
function SamplePlatform(log, config, api) {
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 = [];
}

View File

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

View File

@@ -1,7 +1,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.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
View File

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

191
lib/bridgeSetupSession.js Normal file

File diff suppressed because one or more lines are too long

View File

@@ -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
View 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;
}

View File

@@ -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._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:");

View File

@@ -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;
}

View File

@@ -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"
}
}