From e1334c51961393799110bf84efd90434cb9b6d7e Mon Sep 17 00:00:00 2001 From: Khaos Tian Date: Sat, 30 Jan 2016 18:36:55 -0800 Subject: [PATCH 01/23] Init Plugin 2.0 --- .../homebridge-lockitron/README.md | 3 - example-plugins/homebridge-lockitron/index.js | 82 ------ .../homebridge-lockitron/package.json | 23 -- lib/api.js | 49 +++- lib/bridgeSetupManager.js | 96 +++++++ lib/bridgeSetupSession.js | 177 +++++++++++++ lib/platformAccessory.js | 152 +++++++++++ lib/server.js | 243 +++++++++++++++++- lib/user.js | 4 + package.json | 5 +- 10 files changed, 717 insertions(+), 117 deletions(-) delete mode 100644 example-plugins/homebridge-lockitron/README.md delete mode 100644 example-plugins/homebridge-lockitron/index.js delete mode 100644 example-plugins/homebridge-lockitron/package.json create mode 100644 lib/bridgeSetupManager.js create mode 100644 lib/bridgeSetupSession.js create mode 100644 lib/platformAccessory.js diff --git a/example-plugins/homebridge-lockitron/README.md b/example-plugins/homebridge-lockitron/README.md deleted file mode 100644 index 4a900da..0000000 --- a/example-plugins/homebridge-lockitron/README.md +++ /dev/null @@ -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. \ No newline at end of file diff --git a/example-plugins/homebridge-lockitron/index.js b/example-plugins/homebridge-lockitron/index.js deleted file mode 100644 index 1d5bfd0..0000000 --- a/example-plugins/homebridge-lockitron/index.js +++ /dev/null @@ -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]; -} diff --git a/example-plugins/homebridge-lockitron/package.json b/example-plugins/homebridge-lockitron/package.json deleted file mode 100644 index f0a17f0..0000000 --- a/example-plugins/homebridge-lockitron/package.json +++ /dev/null @@ -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" - } -} diff --git a/lib/api.js b/lib/api.js index 85f142a..5a10168 100644 --- a/lib/api.js +++ b/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.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,31 @@ 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.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); } \ No newline at end of file diff --git a/lib/bridgeSetupManager.js b/lib/bridgeSetupManager.js new file mode 100644 index 0000000..5d61b6c --- /dev/null +++ b/lib/bridgeSetupManager.js @@ -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); + } 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); +} \ No newline at end of file diff --git a/lib/bridgeSetupSession.js b/lib/bridgeSetupSession.js new file mode 100644 index 0000000..88d5205 --- /dev/null +++ b/lib/bridgeSetupSession.js @@ -0,0 +1,177 @@ +var crypto = require('crypto'); +var uuid = require("hap-nodejs").uuid; +var inherits = require('util').inherits; +var EventEmitter = require('events').EventEmitter; + +'use strict'; + +module.exports = { + SetupSession: BridgeSetupSession +} + +function BridgeSetupSession(stateChar, controlChar) { + this.validSession = false + this.sessionUUID = uuid.generate(crypto.randomBytes(32)); + this.stateChar = stateChar; + this.controlChar = controlChar; + + this.transactionID = 0; + this.preferedLanguage = "en-US"; + + this.lastResponse = null; + + // 0 - Waiting for negotiate + // 1 - Waiting for selection + // 2 - List platforms, waiting selection to give session to plugin + // 3 - Forward message to platform + // 4 - Manage accessory config, waiting selection + this.currentStage = 0; + + this.currentPluginName; + this.currentPlatformInstance; + this.currentPlatformContext = {}; +} + +inherits(BridgeSetupSession, EventEmitter); + +BridgeSetupSession.prototype.handleWriteRequest = function(request) { + if (request.type === "Negotiate") { + this.transactionID = request.tid; + this.preferedLanguage = request.language; + this.validSession = true + + var respDict = { + "tid": this.transactionID + 1, + "type": "Negotiate", + "sid": this.sessionUUID, + "attachment": { + "type": "Interface", + "interface": "list", + "title": "How can I help you?", + "items": [ + "Manage Platform", + "Manage Accessories" + ] + } + } + + this.currentStage = 1; + + this.sendResponse(respDict); + } else if (request.type === "Interface") { + this.transactionID = request.tid; + + if (this.currentStage === 1) { + if (request.response.selections[0] === 0) { + this.presentManagePlatformMenu(); + } else if (request.response.selections[0] === 1) { + this.presentManageAccessoryMenu(); + } + } else if (this.currentStage === 2) { + var selectedIndex = request.response.selections[0]; + var targetPlatformName = this.listOfPlatforms[selectedIndex]; + var targetPlatform = this.configurablePlatformPlugins[targetPlatformName]; + + this.currentPlatformContext = {}; + this.currentPlatformContext.preferedLanguage = this.preferedLanguage; + this.currentPluginName = targetPlatformName; + this.currentPlatformInstance = targetPlatform; + this.currentStage = 3; + this.currentPlatformInstance.configurationRequestHandler(this.currentPlatformContext, null, this.pluginResponseHandler.bind(this)); + } else if (this.currentStage === 3) { + this.currentPlatformInstance.configurationRequestHandler(this.currentPlatformContext, request, this.pluginResponseHandler.bind(this)); + } else if (this.currentStage === 4) { + this.handleManageAccessory(request); + } + } +} + +BridgeSetupSession.prototype.pluginResponseHandler = function(response, type, replace, config) { + if (config) { + this.emit('newConfig', type, this.currentPluginName, replace, config); + this.presentMainMenu(); + } else if (response) { + response.tid = this.transactionID + 1; + response.sid = this.sessionUUID; + + this.sendResponse(response); + } +} + +BridgeSetupSession.prototype.presentMainMenu = function() { + this.currentStage = 1; + + var respDict = { + "tid": this.transactionID + 1, + "sid": this.sessionUUID, + "type": "Interface", + "interface": "list", + "title": "How can I help you?", + "items": [ + "Manage Platform", + "Manage Accessories" + ] + } + + this.sendResponse(respDict); +} + +BridgeSetupSession.prototype.presentManagePlatformMenu = function() { + var listOfPlatforms = []; + for (var name in this.configurablePlatformPlugins) { + listOfPlatforms.push(name); + } + this.listOfPlatforms = listOfPlatforms; + + var respDict = { + "tid": this.transactionID + 1, + "type": "Interface", + "sid": this.sessionUUID, + "interface": "list", + "title": "Which platform?", + "items": listOfPlatforms + } + + this.currentStage = 2; + + this.sendResponse(respDict); +} + +BridgeSetupSession.prototype.presentManageAccessoryMenu = function() { + this.emit('requestCurrentConfig', function(config) { + this.currentConfig = config; + }.bind(this)); + + var respDict = { + "tid": this.transactionID + 1, + "type": "Interface", + "sid": this.sessionUUID, + "interface": "instruction", + "title": "Not Implemented", + "detail": "This function is not yet implemented.\nPlease manually edit config.json for now.", + "showNextButton": true, + "heroImage": "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" + } + + this.currentStage = 4; + this.sendResponse(respDict); +} + +BridgeSetupSession.prototype.handleManageAccessory = function(request) { + this.presentMainMenu(); +} + +BridgeSetupSession.prototype.sendResponse = function(response) { + if (this.validSession) { + var serializedReponse = JSON.stringify(response); + var respData = Buffer(serializedReponse).toString('base64'); + this.lastResponse = respData; + setTimeout(function() { + this.controlChar.setValue(respData); + }.bind(this), 100); + } +} + +BridgeSetupSession.prototype.handleReadRequest = function(callback) { + callback(this.lastResponse); +} \ No newline at end of file diff --git a/lib/platformAccessory.js b/lib/platformAccessory.js new file mode 100644 index 0000000..e6cfdf5 --- /dev/null +++ b/lib/platformAccessory.js @@ -0,0 +1,152 @@ +var uuid = require("hap-nodejs").uuid; +var Accessory = require("hap-nodejs").Accessory; +var Service = require("hap-nodejs").Service; +var Characteristic = require("hap-nodejs").Characteristic; + +'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"); +} + +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.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; + } +} + +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; +} + +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; +} \ No newline at end of file diff --git a/lib/server.js b/lib/server.js index eaf4622..2409366 100644 --- a/lib/server.js +++ b/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,10 +23,32 @@ module.exports = { } function Server() { + // 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('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.prototype.run = function() { @@ -34,12 +59,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() { @@ -108,8 +139,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 @@ -142,6 +183,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 || {}; @@ -201,11 +259,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){ @@ -280,6 +391,128 @@ 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._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:"); diff --git a/lib/user.js b/lib/user.js index 45679b8..a5d3fec 100644 --- a/lib/user.js +++ b/lib/user.js @@ -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; } \ No newline at end of file diff --git a/package.json b/package.json index cd3f502..7282c94 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "dependencies": { "chalk": "^1.1.1", "commander": "2.8.1", - "hap-nodejs": "0.1.1", - "semver": "5.0.3" + "hap-nodejs": "0.1.6", + "semver": "5.0.3", + "node-persist": "^0.0.8" } } From 5b9c5192fe2f8463c4bc9eb3470bf14db68226eb Mon Sep 17 00:00:00 2001 From: Khaos Tian Date: Sat, 30 Jan 2016 21:55:12 -0800 Subject: [PATCH 02/23] add SamplePlatform --- .gitignore | 2 +- .../homebridge-samplePlatform/index.js | 197 ++++++++++++++++++ .../homebridge-samplePlatform/package.json | 20 ++ 3 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 example-plugins/homebridge-samplePlatform/index.js create mode 100644 example-plugins/homebridge-samplePlatform/package.json diff --git a/.gitignore b/.gitignore index 137b758..29852c3 100644 --- a/.gitignore +++ b/.gitignore @@ -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 \ No newline at end of file +# example-plugins diff --git a/example-plugins/homebridge-samplePlatform/index.js b/example-plugins/homebridge-samplePlatform/index.js new file mode 100644 index 0000000..4b2beef --- /dev/null +++ b/example-plugins/homebridge-samplePlatform/index.js @@ -0,0 +1,197 @@ +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 == "/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; + + 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", "TEST":"asafas"}); + 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": "Login", + // "items": [ + // { + // "id": "user", + // "title": "Username", + // "placeholder": "jappleseed" + // }, + // { + // "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": "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", + "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); + + // 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]); +} + +// 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 = []; +} \ No newline at end of file diff --git a/example-plugins/homebridge-samplePlatform/package.json b/example-plugins/homebridge-samplePlatform/package.json new file mode 100644 index 0000000..65e9e66 --- /dev/null +++ b/example-plugins/homebridge-samplePlatform/package.json @@ -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" + } +} From aebd152ff9c6229b9ec49b7cb61447db241025e8 Mon Sep 17 00:00:00 2001 From: Khaos Tian Date: Sat, 30 Jan 2016 21:55:37 -0800 Subject: [PATCH 03/23] Reverse .gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 29852c3..4b235ea 100644 --- a/.gitignore +++ b/.gitignore @@ -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 From 9e6bf028ba0ceb7b7dbef705aed420b25e8c6260 Mon Sep 17 00:00:00 2001 From: Nick Farina Date: Thu, 4 Feb 2016 13:34:53 -0800 Subject: [PATCH 04/23] Fix license field for Node --- package.json | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/package.json b/package.json index 7282c94..1376f0d 100644 --- a/package.json +++ b/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" }, From 9c8812da70654772c84f5cad68089480fd0be7c2 Mon Sep 17 00:00:00 2001 From: Raoul Date: Mon, 8 Feb 2016 00:01:44 +0100 Subject: [PATCH 05/23] new getServiceByUUIDAndSubtype(UUID, subtype) function Some platforms may have accessories that contain more than one service of a given type, such as multiple lightbulbs. --- lib/platformAccessory.js | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/lib/platformAccessory.js b/lib/platformAccessory.js index e6cfdf5..d4a3fb4 100644 --- a/lib/platformAccessory.js +++ b/lib/platformAccessory.js @@ -60,6 +60,12 @@ PlatformAccessory.prototype.addService = function(service) { 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 {ServiceConstructor|string} name + * @returns Service + */ PlatformAccessory.prototype.getService = function(name) { for (var index in this.services) { var service = this.services[index]; @@ -71,6 +77,25 @@ PlatformAccessory.prototype.getService = function(name) { } } +/** + * 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 name === 'function' && ((service instanceof name) || (name.UUID === service.UUID)) && service.subtype === subtype) + return service; + } +} + + PlatformAccessory.prototype.updateReachability = function(reachable) { this.reachable = reachable; @@ -149,4 +174,4 @@ PlatformAccessory.prototype._configFromData = function(data) { } this.services = services; -} \ No newline at end of file +} From c93b0b0df10596161f39fa39836c1ed6aaaf227d Mon Sep 17 00:00:00 2001 From: Raoul Date: Mon, 8 Feb 2016 22:18:54 +0100 Subject: [PATCH 06/23] Update platformAccessory.js Small type, big result. --- lib/platformAccessory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/platformAccessory.js b/lib/platformAccessory.js index d4a3fb4..e81933a 100644 --- a/lib/platformAccessory.js +++ b/lib/platformAccessory.js @@ -90,7 +90,7 @@ PlatformAccessory.prototype.getServiceByUUIDAndSubType = function(UUID, subtype) if (typeof UUID === 'string' && (service.displayName === UUID || service.name === UUID) && service.subtype === subtype ) return service; - else if (typeof name === 'function' && ((service instanceof name) || (name.UUID === service.UUID)) && service.subtype === subtype) + else if (typeof name === 'function' && ((service instanceof name) || (UUID.UUID === service.UUID)) && service.subtype === subtype) return service; } } From 7dd8e12791f360ddddc348e576151409605aa65a Mon Sep 17 00:00:00 2001 From: Raoul Date: Mon, 8 Feb 2016 23:06:32 +0100 Subject: [PATCH 07/23] Update platformAccessory.js Some more copy-and-paste errors in the same line. Was wondering why my platform was failing, but never looked to that simple lines of code :-( --- lib/platformAccessory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/platformAccessory.js b/lib/platformAccessory.js index e81933a..1079738 100644 --- a/lib/platformAccessory.js +++ b/lib/platformAccessory.js @@ -90,7 +90,7 @@ PlatformAccessory.prototype.getServiceByUUIDAndSubType = function(UUID, subtype) if (typeof UUID === 'string' && (service.displayName === UUID || service.name === UUID) && service.subtype === subtype ) return service; - else if (typeof name === 'function' && ((service instanceof name) || (UUID.UUID === service.UUID)) && service.subtype === subtype) + else if (typeof UUID === 'function' && ((service instanceof UUID) || (UUID.UUID === service.UUID)) && service.subtype === subtype) return service; } } From 7436be9b44d20778954c5ff7ab5d332c30023b25 Mon Sep 17 00:00:00 2001 From: Khaos Tian Date: Mon, 8 Feb 2016 14:50:50 -0800 Subject: [PATCH 08/23] Add example to update reachability --- example-plugins/homebridge-samplePlatform/index.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/example-plugins/homebridge-samplePlatform/index.js b/example-plugins/homebridge-samplePlatform/index.js index 4b2beef..a7f1a8b 100644 --- a/example-plugins/homebridge-samplePlatform/index.js +++ b/example-plugins/homebridge-samplePlatform/index.js @@ -33,6 +33,12 @@ function SamplePlatform(log, config, api) { response.end(); } + if (request.url == "/reachability") { + this.updateAccessoriesReachability(); + response.writeHead(204); + response.end(); + } + if (request.url == "/remove") { this.removeAccessory(); response.writeHead(204); @@ -188,6 +194,14 @@ SamplePlatform.prototype.addAccessory = function(accessoryName) { 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"); From c02e212b4cf2a6b9f76384da1a62da091ae38ecf Mon Sep 17 00:00:00 2001 From: Khaos Tian Date: Mon, 8 Feb 2016 14:51:34 -0800 Subject: [PATCH 09/23] bump hap-nodejs version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1376f0d..be3316b 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "dependencies": { "chalk": "^1.1.1", "commander": "2.8.1", - "hap-nodejs": "0.1.6", + "hap-nodejs": "0.2.0", "semver": "5.0.3", "node-persist": "^0.0.8" } From 8de375a4b03870a6890e8989bd51c7185e9c37d9 Mon Sep 17 00:00:00 2001 From: Khaos Tian Date: Sat, 13 Feb 2016 21:57:11 -0800 Subject: [PATCH 10/23] Fix the issue with transaction ID Update hap-nodejs to fix #497-183825263 --- lib/bridgeSetupSession.js | 19 +++++++++++++------ package.json | 2 +- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/lib/bridgeSetupSession.js b/lib/bridgeSetupSession.js index 88d5205..eb70a96 100644 --- a/lib/bridgeSetupSession.js +++ b/lib/bridgeSetupSession.js @@ -36,12 +36,12 @@ inherits(BridgeSetupSession, EventEmitter); BridgeSetupSession.prototype.handleWriteRequest = function(request) { if (request.type === "Negotiate") { - this.transactionID = request.tid; + this.transactionID = request.tid + 1; this.preferedLanguage = request.language; this.validSession = true var respDict = { - "tid": this.transactionID + 1, + "tid": this.transactionID, "type": "Negotiate", "sid": this.sessionUUID, "attachment": { @@ -91,7 +91,8 @@ BridgeSetupSession.prototype.pluginResponseHandler = function(response, type, re this.emit('newConfig', type, this.currentPluginName, replace, config); this.presentMainMenu(); } else if (response) { - response.tid = this.transactionID + 1; + this.transactionID += 1; + response.tid = this.transactionID; response.sid = this.sessionUUID; this.sendResponse(response); @@ -101,8 +102,10 @@ BridgeSetupSession.prototype.pluginResponseHandler = function(response, type, re BridgeSetupSession.prototype.presentMainMenu = function() { this.currentStage = 1; + this.transactionID += 1; + var respDict = { - "tid": this.transactionID + 1, + "tid": this.transactionID, "sid": this.sessionUUID, "type": "Interface", "interface": "list", @@ -123,8 +126,10 @@ BridgeSetupSession.prototype.presentManagePlatformMenu = function() { } this.listOfPlatforms = listOfPlatforms; + this.transactionID += 1; + var respDict = { - "tid": this.transactionID + 1, + "tid": this.transactionID, "type": "Interface", "sid": this.sessionUUID, "interface": "list", @@ -142,8 +147,10 @@ BridgeSetupSession.prototype.presentManageAccessoryMenu = function() { this.currentConfig = config; }.bind(this)); + this.transactionID += 1; + var respDict = { - "tid": this.transactionID + 1, + "tid": this.transactionID, "type": "Interface", "sid": this.sessionUUID, "interface": "instruction", diff --git a/package.json b/package.json index be3316b..8b382cd 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "dependencies": { "chalk": "^1.1.1", "commander": "2.8.1", - "hap-nodejs": "0.2.0", + "hap-nodejs": "0.2.1", "semver": "5.0.3", "node-persist": "^0.0.8" } From 902fdded659b8de4cc8b0e466be0b20c3a54d68d Mon Sep 17 00:00:00 2001 From: Khaos Tian Date: Sat, 13 Feb 2016 22:21:54 -0800 Subject: [PATCH 11/23] =?UTF-8?q?Address=20the=20problem=20that=20callback?= =?UTF-8?q?=20get=20invoked=20with=20wrong=20signature=20=F0=9F=98=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/bridgeSetupManager.js | 2 +- lib/bridgeSetupSession.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/bridgeSetupManager.js b/lib/bridgeSetupManager.js index 5d61b6c..d256b74 100644 --- a/lib/bridgeSetupManager.js +++ b/lib/bridgeSetupManager.js @@ -55,7 +55,7 @@ BridgeSetupManager.prototype.handleReadRequest = function(callback, context) { } if (!this.session) { - callback(null); + callback(null, null); } else { this.session.handleReadRequest(callback); } diff --git a/lib/bridgeSetupSession.js b/lib/bridgeSetupSession.js index eb70a96..ed8dea9 100644 --- a/lib/bridgeSetupSession.js +++ b/lib/bridgeSetupSession.js @@ -180,5 +180,5 @@ BridgeSetupSession.prototype.sendResponse = function(response) { } BridgeSetupSession.prototype.handleReadRequest = function(callback) { - callback(this.lastResponse); + callback(null, this.lastResponse); } \ No newline at end of file From e5464405759dc0e272c493f2fdd780d69f0105ba Mon Sep 17 00:00:00 2001 From: Khaos Tian Date: Sat, 13 Feb 2016 22:44:22 -0800 Subject: [PATCH 12/23] Update hap-nodejs to 0.2.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8b382cd..2ff506a 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "dependencies": { "chalk": "^1.1.1", "commander": "2.8.1", - "hap-nodejs": "0.2.1", + "hap-nodejs": "0.2.2", "semver": "5.0.3", "node-persist": "^0.0.8" } From 8e360491cf1805ece206bf31c2d7fb8fc2e31743 Mon Sep 17 00:00:00 2001 From: Khaos Tian Date: Mon, 15 Feb 2016 19:22:43 -0800 Subject: [PATCH 13/23] Update hap-nodejs to 0.2.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2ff506a..54068bf 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "dependencies": { "chalk": "^1.1.1", "commander": "2.8.1", - "hap-nodejs": "0.2.2", + "hap-nodejs": "0.2.3", "semver": "5.0.3", "node-persist": "^0.0.8" } From 40266af8b2e290240cf5afb61ea5e71564281579 Mon Sep 17 00:00:00 2001 From: Khaos Tian Date: Wed, 17 Feb 2016 13:18:25 -0800 Subject: [PATCH 14/23] Add the ability to remove services/characteristics --- lib/api.js | 4 ++++ lib/platformAccessory.js | 22 ++++++++++++++++++++++ lib/server.js | 9 +++++++++ package.json | 2 +- 4 files changed, 36 insertions(+), 1 deletion(-) diff --git a/lib/api.js b/lib/api.js index 5a10168..d2aa1b3 100644 --- a/lib/api.js +++ b/lib/api.js @@ -143,6 +143,10 @@ API.prototype.registerPlatformAccessories = function(pluginName, platformName, a 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]; diff --git a/lib/platformAccessory.js b/lib/platformAccessory.js index 1079738..a923257 100644 --- a/lib/platformAccessory.js +++ b/lib/platformAccessory.js @@ -60,6 +60,28 @@ PlatformAccessory.prototype.addService = function(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. diff --git a/lib/server.js b/lib/server.js index 814aa09..8d02d5a 100644 --- a/lib/server.js +++ b/lib/server.js @@ -32,6 +32,10 @@ function Server(insecureAccess) { 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)); @@ -408,6 +412,11 @@ Server.prototype._handleRegisterPlatformAccessories = function(accessories) { this._updateCachedAccessories(); } +Server.prototype._handleUpdatePlatformAccessories = function(accessories) { + // Update persisted accessories + this._updateCachedAccessories(); +} + Server.prototype._handleUnregisterPlatformAccessories = function(accessories) { var hapAccessories = []; for (var index in accessories) { diff --git a/package.json b/package.json index 18c6747..9456967 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "dependencies": { "chalk": "^1.1.1", "commander": "2.8.1", - "hap-nodejs": "0.2.3", + "hap-nodejs": "0.2.4", "semver": "5.0.3", "node-persist": "^0.0.8" } From 815ea7abeaa8bb3562e4c118bb8ad928386faecf Mon Sep 17 00:00:00 2001 From: Khaos Tian Date: Wed, 17 Feb 2016 17:45:47 -0800 Subject: [PATCH 15/23] Track setup session termination --- lib/bridgeSetupSession.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/bridgeSetupSession.js b/lib/bridgeSetupSession.js index ed8dea9..8d140d9 100644 --- a/lib/bridgeSetupSession.js +++ b/lib/bridgeSetupSession.js @@ -83,6 +83,13 @@ BridgeSetupSession.prototype.handleWriteRequest = function(request) { } else if (this.currentStage === 4) { this.handleManageAccessory(request); } + } else if (request.type === "Terminate") { + this.transactionID = request.tid; + this.validSession = false; + + if (this.currentStage === 3) { + this.currentPlatformInstance.configurationRequestHandler(this.currentPlatformContext, request, this.pluginResponseHandler.bind(this)); + } } } From 012005ddc7f0eed2c581992a0e4074d3b45b96c7 Mon Sep 17 00:00:00 2001 From: Khaos Tian Date: Thu, 18 Feb 2016 14:50:55 -0800 Subject: [PATCH 16/23] Save cached accessories to persist storage when shutting down homebridge. --- lib/cli.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/cli.js b/lib/cli.js index 68e44f8..fbfeeac 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -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]); }); }); From 32e776203f7514f58e63cf480b7c66247742b59c Mon Sep 17 00:00:00 2001 From: Khaos Tian Date: Sun, 28 Feb 2016 18:17:46 -0800 Subject: [PATCH 17/23] Forward "identify" event --- lib/platformAccessory.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/platformAccessory.js b/lib/platformAccessory.js index a923257..324aa28 100644 --- a/lib/platformAccessory.js +++ b/lib/platformAccessory.js @@ -130,6 +130,9 @@ 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) { + this.emit('identify', paired, callback); + }.bind(this)); } PlatformAccessory.prototype._dictionaryPresentation = function() { From f6df85695df1070470804e00b592c3c3988d3b83 Mon Sep 17 00:00:00 2001 From: Khaos Tian Date: Sun, 28 Feb 2016 18:19:46 -0800 Subject: [PATCH 18/23] Forward identity only if plugin cares about event --- lib/platformAccessory.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/platformAccessory.js b/lib/platformAccessory.js index 324aa28..a3d8f61 100644 --- a/lib/platformAccessory.js +++ b/lib/platformAccessory.js @@ -131,7 +131,13 @@ PlatformAccessory.prototype._prepareAssociatedHAPAccessory = function () { this._associatedHAPAccessory._sideloadServices(this.services); this._associatedHAPAccessory.reachable = this.reachable; this._associatedHAPAccessory.on('identify', function(paired, callback) { - this.emit('identify', 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)); } From 8cb22efb833ce957a32080b4dbe3c8237f6433ec Mon Sep 17 00:00:00 2001 From: Khaos Tian Date: Tue, 1 Mar 2016 00:25:05 -0800 Subject: [PATCH 19/23] Add a example to use "identify" event --- example-plugins/homebridge-samplePlatform/index.js | 8 +++++++- lib/platformAccessory.js | 4 ++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/example-plugins/homebridge-samplePlatform/index.js b/example-plugins/homebridge-samplePlatform/index.js index a7f1a8b..f3d3ba0 100644 --- a/example-plugins/homebridge-samplePlatform/index.js +++ b/example-plugins/homebridge-samplePlatform/index.js @@ -74,6 +74,10 @@ SamplePlatform.prototype.configureAccessory = function(accessory) { // accessory.updateReachability() accessory.reachable = true; + accessory.on('identify', function() { + console.log("Identify!!!"); + }); + if (accessory.getService(Service.Lightbulb)) { accessory.getService(Service.Lightbulb) .getCharacteristic(Characteristic.On) @@ -178,7 +182,9 @@ SamplePlatform.prototype.addAccessory = function(accessoryName) { uuid = UUIDGen.generate(accessoryName); var newAccessory = new Accessory(accessoryName, uuid); - + newAccessory.on('identify', function() { + console.log("Identify!!!"); + }); // Plugin can save context on accessory // To help restore accessory in configureAccessory() // newAccessory.context.something = "Something" diff --git a/lib/platformAccessory.js b/lib/platformAccessory.js index a3d8f61..cbb882e 100644 --- a/lib/platformAccessory.js +++ b/lib/platformAccessory.js @@ -2,6 +2,8 @@ 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'; @@ -33,6 +35,8 @@ function PlatformAccessory(displayName, UUID, category) { .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. From 1f1030766ad8571321ab217d92a8141fd4ccca1b Mon Sep 17 00:00:00 2001 From: Khaos Tian Date: Tue, 1 Mar 2016 18:26:40 -0800 Subject: [PATCH 20/23] Update sample plugin --- .../homebridge-samplePlatform/index.js | 64 ++++++++++--------- 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/example-plugins/homebridge-samplePlatform/index.js b/example-plugins/homebridge-samplePlatform/index.js index f3d3ba0..353513e 100644 --- a/example-plugins/homebridge-samplePlatform/index.js +++ b/example-plugins/homebridge-samplePlatform/index.js @@ -74,8 +74,9 @@ SamplePlatform.prototype.configureAccessory = function(accessory) { // accessory.updateReachability() accessory.reachable = true; - accessory.on('identify', function() { + accessory.on('identify', function(paired, callback) { console.log("Identify!!!"); + callback(); }); if (accessory.getService(Service.Lightbulb)) { @@ -105,7 +106,7 @@ SamplePlatform.prototype.configurationRequestHandler = function(context, request // 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", "TEST":"asafas"}); + callback(null, "platform", true, {"platform":"SamplePlatform", "otherConfig":"SomeData"}); return; } @@ -114,23 +115,23 @@ SamplePlatform.prototype.configurationRequestHandler = function(context, request // User response can be retrieved from request.response.inputs next time // when configurationRequestHandler being invoked - // var respDict = { - // "type": "Interface", - // "interface": "input", - // "title": "Login", - // "items": [ - // { - // "id": "user", - // "title": "Username", - // "placeholder": "jappleseed" - // }, - // { - // "id": "pw", - // "title": "Password", - // "secure": true - // } - // ] - // } + 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 @@ -151,17 +152,17 @@ SamplePlatform.prototype.configurationRequestHandler = function(context, request // 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": "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", - "showActivityIndicator": true, - // "showNextButton": true, - // "buttonText": "Login in browser", - // "actionURL": "https://google.com" - } + // 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"; @@ -182,8 +183,9 @@ SamplePlatform.prototype.addAccessory = function(accessoryName) { uuid = UUIDGen.generate(accessoryName); var newAccessory = new Accessory(accessoryName, uuid); - newAccessory.on('identify', function() { + newAccessory.on('identify', function(paired, callback) { console.log("Identify!!!"); + callback(); }); // Plugin can save context on accessory // To help restore accessory in configureAccessory() From c7b2500518bfd93b76bf89fc78169165b90a288b Mon Sep 17 00:00:00 2001 From: Khaos Tian Date: Tue, 1 Mar 2016 18:28:41 -0800 Subject: [PATCH 21/23] Prepare for merge --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 43a4fb0..2fd7790 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "dependencies": { "chalk": "^1.1.1", "commander": "2.8.1", - "hap-nodejs": "0.2.5", + "hap-nodejs": "0.2.6", "semver": "5.0.3", "node-persist": "^0.0.8" } From 195255bf0d2fa32e69f05d7cd6bb16167cfd34aa Mon Sep 17 00:00:00 2001 From: Khaos Tian Date: Tue, 1 Mar 2016 18:34:15 -0800 Subject: [PATCH 22/23] 0.3.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2fd7790..94bd28f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "homebridge", "description": "HomeKit support for the impatient", - "version": "0.2.19", + "version": "0.3.0", "scripts": { "dev": "DEBUG=* ./bin/homebridge -D -P example-plugins/ || true" }, From 9d7c1de9dddb0c4e2938614f815b3a8bbf73c937 Mon Sep 17 00:00:00 2001 From: Khaos Tian Date: Tue, 1 Mar 2016 18:41:29 -0800 Subject: [PATCH 23/23] Update readme to reflect new API system --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ce2e076..c1d3462 100644 --- a/README.md +++ b/README.md @@ -101,15 +101,13 @@ 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. @@ -143,3 +141,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. + +