From e1334c51961393799110bf84efd90434cb9b6d7e Mon Sep 17 00:00:00 2001 From: Khaos Tian Date: Sat, 30 Jan 2016 18:36:55 -0800 Subject: [PATCH 01/21] 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/21] 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": "iVBORw0KGgoAAAANSUhEUgAAAWgAAAFoCAYAAAB65WHVAAAAAXNSR0IArs4c6QAAJB5JREFUeAHtnQfUZVV5hmcYOkMXhi5VAelqEGlKS7CAujQEQYkaQrHFkBhYEX9UXCoasGCBaIBADBFwWSg2MIiGooBSRPowVEHq0IZm3nfxXz1zuXufc889/Tx7rXf+e3f99rPP/WbfffbZd8YMAgQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAs0iMLNZ5nTWmpXVs/2kXaWtpVWkJSVCuwk8LvPvluZJP5DOlG6RCBCAQAsIzJaNn5MWSH9EnWfwrMb4a9JKEgECEGgwgfVl200Sjrl/DG7WuK8rESAwEQGWOCbCFyy8mlKukFYP5iCh6wRuUwe3kh7qekfpX3kEZpVXda9rPl2936bXBOj8CkKwhvQdUEAgLwFm0HnJhcttq6RLwskznlOabyo9EslDUjsIrCoz/W0pFDzWG0s3hjIQDwEIVEvgy2outO78XaW9uFpzaK1kAmup/u9LoTH/dMntUz0EIDAGgcuVd9SH9QbFLzlGPWRtD4FFZepcadS4X9aebmBp0wgs0jSDOmDP2oE+/FTxTwbSiG43gWdkvvdAjwobjYokDgJZCOCgs1AaL48fQhkVrh8VSVxnCNwZ6IlvFhIgkIsADjoXtlyFfMOI0F0CXt4gQKBQAjjoQnFSGQQgAIHiCOCgi2NJTRCAAAQKJYCDLhQnlUEAAhAojgAOujiW1AQBCECgUAI46EJxUhkEIACB4gjgoItjSU0QgAAECiWAgy4UJ5VBAAIQKI4ADro4loOafGj7qLD4qEjiOkNgqUBP/JQhAQK5COCgc2GLFrovkLppIJ7obhDYKtCNuwLxREMAAjUQOE9tjjo05zHFv7gGe2iyfAIbqokHpVHjflH5zdNCVwn4FC5CsQTOVXV/NaLKpRV3ifQJ6ZfSvRKh3QSWk/k7SodLoTM3zml3F7G+TgIc2F88fX9ob5f8l9BvAl5/Xk+6o98Y6H1eAqxB5yUXLudfSvlIOJmUHhHwtyWcc48GnK62g4C/mZwljVqTJK4fXLy0wRJiOz6vWNlDAv5wniLhkPvH4ESNO865hx96utw+Aq+TyVdJOOpuM/B53+dLO0kECBRCgJuEhWDMVMnGyuXdHetIq0ve1VFmWEyV75mhgUeVx/95NDH4+pydYphtP1uqow/+CTPve79SukC6VSJAAAIQSCXwGuWw0xqlPyj+HVKa81OW2sOqsmAf6X5pVF8ct7VEgAAEINAaAkfL0pBD27k1vfizodtH+vPRP2fjFQS6Q4Btdt0Zy+GerDUcMf3eD8hcGEhrcvQvZNzvAgbyy9kBMES3mwAOut3jF7P+RYHEawPxbYieGzByTiCeaAi0mgAOutXDFzU+NLZtPl0tdFJgqK9RQCRCoOkEuLCbPkLYBwEI9JYADrq3Q0/HIQCBphPAQTd9hLAPAhDoLQEcdG+Hno5DAAJNJ4CDbvoIYR8EINBbAjjo3g49HYcABJpOAAfd9BHCPghAoLcEcNDdHfrnAl2bFYhvQ7QPgBoVQvujR+UlDgKtIYCDbs1QjW2oDxcaFdYbFdmSuM0Ddt4TiCcaAq0mgINu9fBFjb8mkGoHvUcgrcnRu8g4H9M6Kvg3IAkQgAAEWkNgS1kaOs1uvtIOlDZoQW983OjB0gNSqD+vbUE/MBECYxOYOXYJCrSJwM9lrI/pTAt2fE0MWa7P22T4elJT+9BErtgEAQg0gMAOssE30EIzzy7Ev7kBnDEBAhCAQC4CH1OpLjjiUX04PhcRCkEAAhBoEIGPy5ZRDq6tcd5C+MkG8cUUCEAAAhMR2FmlfyO11SkP7L5QffiLiUhQGAItITCzJXZiZnEE/AOrb5JeKnnb2opSk4N3nNwlXSr517tDP3ulJAIEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAgY4QmNmRftCNegjMUrMvkdaQVpSWlQbX1LN6/Zg0X7pDuklaIBEgAAEIQKAkAlur3k9JF0t2wH/MKDvsK6WPSi+TCBCAAAQgUACBJVTHh6QbpKwOOS3f9arrfZJn4QQIQAACEBiTwCLK/y5pnpTmcPOme1b9KokAAQhAAAIZCSynfOdIeR3vOOWeUzufyGgX2SAAAQj0msBG6v110jhOtoi8n+s1dToPAQhAIIXAHKXfLRXhcPPUcUyKfSRDoDcEBluietNhOhol4DXnH0u7RHM9n/i4/pwnXShdLd0uDXZ16OUM31hcXVpTWkfaS9pZchtpYQ9lsB0ECEAAAhCYJnCU/qbNeu9Xng9Ky0vjhrVV4DjJa86xduYqfbZEgAAEIAABEfCDJk9IMcfpGfNq0qRhN1VwrxRri5uGk1KmPAQg0BkCnhXHHOYFSl+8wN5up7qejLRpB+5lEgIEIACB3hO4VgRCDtqPaudZ0kiDmvafwv5pFZAOAQhAoOsEtlEHQ87Z8R8uCYBn5LdF2j6rpHapFgIQgEBrCBwkS0MO2jszypg9D+BMRdr2MgcBAr0lkGXLU2/h9Kjjm0X6+lulPRxJnzTJTyuGwipK2CCUSDwEuk4AB931Ec7Wv5Uj2W6NpBWR5D3UPukuFHyUKQECvSSAg+7lsL+g0z53IxTmhhIKivcZ0d5bHQqeRRMg0EsCOOheDvsLOr3YC2L+HOE16LLDU5EG2GoXgUNStwngoLs9vvQOAhBoMQEcdIsHD9MhAIFuE8BBd3t86R0EINBiAjjoFg8epkMAAt0mgIPu9vjSOwhAoMUEcNAtHjxMhwAEuk0AB93t8aV3EIBAiwngoFs8eJgOAQh0mwAOutvjS+8gAIEWE8BBt3jwMB0CEOg2ARx0t8eX3kEAAi0mgINu8eBhOgQg0G0COOhujy+9gwAEWkwAB93iwcN0CECg2wRw0N0eX3oHAQi0mAAOusWDh+kQgEC3CeCguz2+9A4CEGgxARx0iwcP0yEAgW4TwEF3e3zpHQQg0GICOOgWDx6mQwAC3SaAg+72+NI7CECgxQRw0C0ePEyHAAS6TQAH3e3xpXcQgECLCeCgWzx4mA4BCHSbAA662+NL7yAAgRYTwEG3ePAwHQIQ6DYBHHS3x5feQQACLSaAg27x4GE6BCDQbQI46G6PL72DAARaTAAH3eLBw3QIQKDbBHDQ3R5fegcBCLSYAA66xYOH6RCAQLcJ4KC7Pb70DgIQaDEBHHSLBw/TIQCBbhNYtNvdo3c9ITBL/dxcerm0qrTytBbT3z9KD0v3J3SNXl8tPScRINBYAjjoxg5NrwybmaO3c1TmXdJu0rbSbGmc8KAy/1w6XzpFekgiQAACEGgcgR/KIs80R2mqAmtvD7Rte/Ydan8nvT9dekoaZW+euEdV15ekDSQCBBpDgDXoxgwFhqQQ2ELpF0kXSvtIXr4oKiyjit4n/Vb6F4nPhSAQ6ifAhVj/GGBBnICXPw6XrpB2iGedOHVx1fBpyf8RrDVxbVQAAQhAoAACTV7imKf+5Vm2mLTMzWp37QLYUgUEchPgJmFudBQskIBnyaGQx0neq8p8E9C7NFy3lzCWk5aXsob1ldHLKZ6135W1EPkgUCQBHHSRNKmrLgKXq+EzJO/K8FLIE9Ko4O13W0lew7bstGNhPSWeKu0uTbIlz5+zFaVZ0/U8pr+PS57lEyAAAQhECdS9xHGHrMuzJPEdldsy2rNw4tJKeo/kmXZa2x8OV7NQim9cvkr6J+m70o1SqH47fO/NvkQ6RTpI2lAiQAACEFiIQNsctJcw3rJQD/K/WVdFL5ViTtoz3jlSKPg/iW9IzherJ0uaH6L5kLS8RIAABCAwo24HfbvGIIvzch4/UOJliiLDEqoszUkfN6LBNyvO69RZbR8n3+9V7ztHtEkUBCDQMwJ1O+isSxx+OKWsrXbeVueZeciJenY8eFpxA73+SSRvqI488Repnc0kAgQg0FMCdTvorDPo/yh5fA5U/TEnup/S3yg9kpIvVkeetPlqbyeJAAEI9JBAGxz0sxqXl5Y8Nn5QJTab938ktiOPk520jGfwu0iEHhFYtEd9bWNX/bXb+3G9PWylafnciLunZWdypzRpmDlpBROWz9K+/xO5fsJ20op7CeWb0j8HMno8soaHlfFi6WrpNmkw615Er71UsoLk/3BeIW0qpQXvOjlbeq3k9XICBCBQMYHV1d7fSidJt0hZZl03Kd/x0hskf4jzhLpn0LFZ64DBEXk6lqOMHeCgzXH/Pqmyp0muw444a9haGb8kPSCltXmd8iwpESAAgQoIeDZ1sPS/0qRfn72v9iNS2gMYyrJQaIODLmpb3UIdH/HG29vSnOSo9B+pnL/tTBL8TekcaVT9yTifF0KAAARKJLCu6v43ydvGkh++Il57JmZH7QcnsoQ2OGjPSqsKD6qhccbhsAINm6m6jkxp/xmlb1Rgm1QFAQhME/Ds9kRp0tlyFgfyC7Wz5nS7sT91O+gsuzi2i3Wg4DSv8Wfh6zxTBbc9qO7rKTZ8fpCRvxCAQDEE9lA186SsH/4i8vmBh11TzK/bQWdZg26ig/5uCtdJkpdQ4Wuk0DXgb14+BIrQYQLj3MjoMIZKunaMWrEjXDtna95h4K/e1jjBv9F3nlTlEsE49rU579ElGr9AdX88Ur/XyveKpJPUAQI46PIH0WuK3mUR2roVsuB3SvDXWO/OeLHkO/eDrXZL6fWrpc9KN0lpwWvR35Y2TstIemYCP1HOX2bOnS/jmSo2N1J050gaSRCAQAYCJyhP6GvqcPwNyjslZdkXm2z6bXqTZZngZuVbJVlw+rVn9sO2DN7bnrJDFtu3K9uIRP1Z1qD/PpG/zJdfVOWDsRj+e22ZDVM3BLpO4BB1cPhDNer9VcrnR4g9284bllXB06RR9SfjTh7RAA56YShZHPQ2Cxcp7d2bVHNy/JKvfaOZb8Gloa+/4kXrN6GzFmyonnkJIhb8YMMRkmdJz8UyZkibrzzvkLwX+gOR/M5zrOT/FAj5CdyTv+hYJb3UFQp2zt47fV8oQyJ+A73eUvI3qBdJK0gubyf/mPSo5Hr8Le56adx7HSpCgEA7CHgm7C1uydnO8Ot5St+spO78Z0rb5w61ywx6YSBZZtDLLFyktHd2psPXTvJ96L7CSir3fsn3HvyfSbJMltfXqMwXpB0kAgQ6RWBP9Sb2IfAMJcv+5LxQfBPx6hQbkqej4aAXJp3FQS+5cJHS3tnRxq6llw217JvH/g/6iZRysTqH0zyL31ciQKATBL6nXgxf5IP3/io57k3APFC2UyEvmwzaHf57VqLSuh10Gx9UaZqDfrnGM+1b2/A1MO7789VGaMaeuJx4CYHmElhHpnldL3TxV3X334TOiNjxjNLWciaFuh10G3dxNMVBe135M1Lsmgtdi3nin1JbWX+jUVkJkxDgJuEk9EaX3VvRvvkyKtylyJNHJZQUd5zqfWug7lmK938WHw2k9zn6y+r82pJvpK0oeZnBN+O8HuzDrZoU/J/wRhUatJja8n8I3jV0ZIXt0hQECiFwgmoJzUyOKqSF8Sr5dcQer7X6A/ejSJ4ppZUdmjaDjvV3CSX6/sEkWyJj9Q+npa1Bh661WPwCNfKHaXm3xtNSLH8o7VMqRyiRQGimV2KTna96+KZNssNXJt9U9NqzwVBYTQlvCSUSP5KAndudkp1WW8LlMnRK2l3ymPs/GX8bsPwNwf9JLydtK31Qcv4s4XBl2i9LRvJAoCkEPDMJzTiquDk4zGFpRXiWFLLpZ0pjDXqYWnPe551Be1bs3Ryb5OzKFip3qhS6bgbx9yvPnJxtUAwClRP4vVocXLzDf9ev3JrnG/x8xCbbGNtFMVWBzbEljvvU/lckP2DRx5DHQd8iUK8oCNY7VM+j0vC1nHx/ZkFtUQ0ESidwvVpIXrzJ168pvfXRDbxE0bEtd0kbh19Pja6y0NhhB+09vP8tvV7q+43scR30RWLmm5tFhleqsiel4Wsj+X6zIhukLgiUReAyVZy8cJOvDyqr0Qz1xm4EJm0cfj2Voe5Jswxm8GZ3iFS0g5nUvjrLj+OgvTZe1nLDu1X38LWRfP/VOiHRNgSyEvieMiYv3OTrs7NWUkK+vSN2JW0cfj1Vgi3DVR6hCGZgw1Sefz+Og95ldBWFxcb+k/cyiLfeESDQaAJ2NsNObvB+gdJ8t7yOMEuNzpMGtmT9O1WHsbT5JwJZHfQ1fypR3oudVXXsunlDeU33s+ZF+tntUnvtg9xDYXEl7BtKLDneT5p9reQ2qL4+At+ooOkL1caNkXZ2jKSRBIHGEPBFHJpp/KZGK/3zV57Fh2wbFT9Vo700/TwBz0xvkEaNzyBu3eezlv6vT7gbtDn81zcoCQUSYAZdIMxEVSclXg+/9P7Suo5wvFdtnzlsEO8bT8D3LrxG7zMw5o+w9m7FzR0RX0bUryKVrhlJIwkCjSGQNlP1FrK6wqvV8PDMJ/Z+qi5DaXckgdUV6+snOWZnjcxZTqSXMZJtJ1+P+s+jHCuoFQITEjhN5ZMXb/L1U0oraztUFrP9yHnSntjrqSwVkqdyArupxeslj91hFbb+iuk2Q9dMhaZ0vymWOMobYz/9Fgo+++DAUGIF8bHzOSponiYKIOCb0V4u82mEvnlHgAAExiQQO0nOD2d461sdIe18juTsiBl0HSPU3DaZQVc4Nsygy4V9fKR6H5a/VyS9zKTHVfnJZTZA3RCAwOQEcNCTM4zV8E0lPhzJ8N5IWtlJfjTXM2UCBCDQUAI46HIHxjPVkyJN7Kq0un7jzftqYw/VRMwmCQIQqIIADrp8ymkz1UPLNyHYAjcLg2hIgED9BHDQ5Y+BZ6o/jjTzTqUtE0kvM8kPQMwrswHqhgAE8hPAQednN07J2Ja75VXR/uNUVmBen89xQoH1URUEIACB1hHwdrrbpOT2teTrJp7P8bTsvU7aTyJAYECAbXYDEvztFIEj1JukUx5+vUONvf2M2vbjw0dKb5U2lfwwDQECwwRw0MNEeN8JAquqFwukYcc8eF/n+RydAEwnKiGAg64EM43UQaDJ53PUwYM220cAB13hmHGTsELYaiq2ra3u8zmqJUFrEIAABBpIIHaSXJ3nczQQFSY1kAAz6AoHhRl0hbCnm4rNon0+h3/clQABCEAAAjUQSDtJjsevaxgUmsxMgBl0ZlSTZ2QGPTnDcWtIO0muzvM5xu0L+SEAgRIJ4KBLhBupusnnc0TMJgkCEIBAPwj8UN0c7IEe/usjSus6n6Mf9OllXgIsceQll6McM+gc0AoqEjufYzm1Udf5HAV1j2ogAAEItJdAk8/naC9VLC+bADPosgkn6mcGnYBR8UufJPe1SJv+QVD/xD0BAhCAAARqIMD5HDVAp8mJCDCDnggfhdtGIO18jtXa1iHs7TQBHHSFw8sSR4WwA03FnizkfI4ANKIhAAEIVEWA8zmqIk07kxJgBj0pwTHKM4MeA1aJWWOzaM7nKBE8VUMAAhBII5B2Psf5aRWQDoGKCDCDrgi0m2EGXSHsSFM+n+OkSPouSts4kk4SBCDQQQI46OYMqvdE+5HvUDg0lEA8BCAAAQiUT4DzOcpnTAuTEWCJYzJ+Y5VmBj0WrtIzx24Wcj5H6fhpAAIQgECYQNr5HFeFi5ICgUoIMIOuBPPzjTCDrhB2hqbSzufYXHVwPkcGkGSBAAQgUAYBzucogyp1FkWAGXRRJKmntQQ4n6O1Q9d5w3HQFQ4xSxwVwh6jqdjNQs7nGAMkWSEAAQiUQSDtfI5Fy2iUOiGQQoAZdAqgIpOZQRdJs9i6jo9U5/M59oqkkwQBCEAAAiUS4HyOEuFSdW4CzKBzoxu/IDPo8ZlVVYLzOaoiTTsQaCgBHHRDB2barK/qL+dzNHuMsA4CEOgxgbTzOWb3mA1dr54ASxwVMmcGXSHsnE3FttxxPkdOqBSDAAQgUAQBzucogiJ1FEWAGXRRJDPUwww6A6Sas3A+R80DQPMQgAAEYgRWUeICyTcMR+n0WGHSIFAgAWbQBcKkqu4QOFVdGeWcHfeUtFp3ukpPGkwAB13h4LDEUSHsCZv6SqQ853NE4JAEAQhAoAoCV6iR0Cz6DqVxPkcVo9DvNphBVzj+zKArhF1AU7Etd2uqfs7nKAAyVUAAAhDIQ4DzOfJQo0yRBJhBF0kzpS5m0CmAGpac5XyOTRpmM+ZAAAI5CeCgc4KrsRjnc9QIn6YhAAEIpBH4gTKEbhY+rDTO50gjSHpeAturYOja83ZPQoEEmEEXCLPCqmJb7jifo8KB6GFTS0X6/EQkjSQI9IaA/2OdK4VmMlf1hgQdrZqAdwqFrrvfV21M19tjBt3OEX5OZp8QMX1zpe0YSScJAnkJeCdRKDwZSiA+HwEcdD5uTSj1dRnh8zlC4b2hBOIhMAGBlSNl74ukkZSDAA46B7SGFPGH4YyILW9RGudzRACRlIuAf7A4FO4JJRCfjwAOOh+3ppSKPVnI+RxNGaVu2bF2pDt3R9JIykEAB50DWoOKXCJbrozYc5DSOJ8jAoiksQnEHPRdY9dGgSgBHHQUTysSY7Non8+xdyt6gZFtIbBhxNBrImkkQaCXBHxX/QEptPXp/F5SodNlEHhR5Drz9Rdz3mXYQ50QaAWBY2VlyEE7nvM5WjGMjTdyt8h19ojSZja+By0zkCWOlg1YwFw/WWhHHAqHhhKIh8AYBLaK5P2N0mLXYKQoSRDoPgHO5+j+GNfdw2/JgNA3tS/WbRztQ6DJBGKP4PpDdXCTjce2xhPw8oX33occ9Lsb3wMMhECNBLxcNVcKfYA4n6PGwelA01tGri1fc1t3oI90AQKlEjhctYcctON3KrV1Ku8ygcPUudC15RuEfjCKAAEIRAisojQfWBP6IJ0eKUsSBGIEfqxErqsYIdIgkIHAqcoT+iD5QPXYYTcZqidLDwn4P/5npNB1tU8PmdBlCOQi8CqVCn2QHM/NwlxYe13IRwaEril/Y1u213ToPATGJOBHbkMfqJ+NWRfZIXBB5Ho6GzwQgMB4BD6s7CEH7cP+1xmvOnL3mIAPR3pWCl1P7+kxG7oOgVwEfGZv7EM1latWCvWRwCfV6ZBz9jXm9WkCBCAwJoGfKH/og+Vzexcfsz6y94+Ar5F7pNB19NP+IaHHECiGwAGqJvTBcvz+xTRDLR0msK/6FruG3tbhvtM1CJRKYLZqf1gKfcAuK7V1Ku8CgV9Frp9blTarC52kDxCoi8AxajjkoB2/e12G0W7jCbxeFsaunX9ofA8wEAINJ7CG7PMvf4c+aJc03H7Mq4/ApZHr5iGlsfe5vrGh5Q4R+Lr6EnLQjn9Dh/pKV4ohsKeqiV0zny2mGWqBAAReKgTe+xz6wPlHZ/nhBq6TAQGvK18tha6Xp5XmvdEECECgIAJnqZ7QB87xfpSXAAETeK8Uu1b+C0wQgECxBF6i6nxQUuiDd7/SOESpWOZtrG1FGf0HKXSd+NyN9dvYMWyGQNMJHCcDQx88x5/Y9A5gX+kETlALsWvk6NItoAEI9JRA2uzIj+3u0lM2dHvGjDcKQsw53670pQEFAQiURyBtffEuNb1qec1Tc0MJzJFd90oxB82Zzw0dPMzqDoFF1ZXYHXp/QH8g+QdCCf0hcK66GnPOF/YHBT2FQL0EtlHz3ioV+0D6uFJCPwi8T92MXQvPKH2LfqCglxBoBoEpmRH7UHrHx7bNMBUrSiSwqep+QopdC18osX2qhgAERhDwUscvpdgH81alLz+iLFHdIOCjRH8txa4Bpy/Zje7SCwi0i8AmMjdt9nSm8rAe3a5xzWrtScoYc86PKt1PoRIgAIGaCByqdmMfUqd5/zShWwQ+re6kjfs7u9VlegOBdhL4hsxO+7Ae1c6uYfUIAj4mNG28Tx5RjigIQKAGAkuoTR87mvah/ccabKPJYgm8XdXFDs7yNXCdtEyxzVIbBCAwCYE1VPhuKc1J/90kjVC2VgJ7qHXvzomN8eNKZ0tdrcNE4xAYTeDVivZhOLEPsB8H32d0cWIbTOCVsm2+FBtb7433494ECECgoQT2kl1pD7F4FsYHuaEDOMIsO+f7pJhz9rLHASPKEgUBCDSMwH6yJ22d0k78kIbZjTkvJLC3oh6TYs7ZaYe9sCgxEIBAUwnY+aZ9qJ3+RYlfdm7mKL5fZnlJKm0cP9NM87EKAhCIEThciWkfbqefJy0Xq4i0Sgn458uOlbKMnbdYEiAAgZYS8J7ZtOUOO4JrpfVa2scumb2UOvNtKYtzPkP5+PbTpdGnL70ksL96nXbj0A7BN6J26CWhZnR6NZlxsZTFOR+vfPxQcDPGDSsgMDGB16sG75FN+/AvUJ5/lXwYE6E6Am9VU2k7NTx2/jbEUbLVjQstQaAyAturpdgPiiad96+Ud7PKLOtvQ/4Zs29KSfah1/7P8+39RUXPIdB9Auuqi1dKISeQjLdD+IjEbFoQSgh7qs47pSTz0OuHlI/fmyxhEKgSAk0j4BtRp0khZzAcf7nybt60TrTYnmVl+4nSMOfQ+3nKC/8WDzimQyAPAe/wyHLz0I7Ds+kjJQ5/F4QJgh88uVUKOePh+G8pr5dBCBCAQA8JeF36ZmnYMYTe+yv5wdJiEiE7gd2U9VIpxHU43uduvCt79eSEAAS6SmC2OjbOV247k1ukAyT24QpCJGyntAukYQcce29HvmGkTpIgAIEeEnid+pzlyNKkc/HZw38t8bNaC18wW+rt96Ukq7TXfqz7aGlRiQABCEDgBQRWVswpUpanD5MOxztD/kbyjwf0Oeyszv+PNC6/G1Vmxz6Do+8QgEB2Av5q7t0bSSec5fX9KvMFqU+7DlZXf33uiZ1sFkbJPA+qjH/phjV9QSBAAALZCfhR4oOkrA+3JB2PX182Xb6LBzF5GWIv6XvSM9Jw39Pee/fMlyR/YyFAAAIQyE1gJZX8nJTlXOJRjsnlTpbs0NrsrBeX/a+RjpHGXatPcjlH5TeWCBCAAAQKIzBHNR0rZTnTI+mQkq89c/yFdJS0vdT0G2JeqvESxLlS3v+gBv33Ov3uEgECEIBAaQS85up15iekgfPJ+/cR1eFlgg9I20jLSHUF3+C0Qz5AOk26R8rbr0E53yw8W9pVIvSQANubejjoDemy108PlA6R1inIJju2O6TfTcvb+AavvaxQRPCTeZtIXmYY/PXr9aSi9nV7tu3dMP6P7AaJ0FMCOOieDnyDum2n5vXl90uvLdGuR1X3/ZL/zg/IN+yWlbzWPfibfL284q2ywu2q+Hjp3yXv0CBAAAIQaAwBz0Q/Lt0oDb7md/3vA+rrSZLP3G76mrpMJEAAAhCYMeOVgnCsdKfUNSftw/U9S/5LaTGJAAEIQKCVBLyf2s7aZ0tfJD0ttdFh3yq7vyrtKnlZhwCBVAKsQaciIkPDCHhN2E7O2lbaQvIe4yaFBTLmCun/pnWx/hZ1k1JVEfpCAAfdl5Hubj/tnLeUPMu2/FNbG0orSFWEe9WIZ8e3SJdLdsp2znbSBAhMRAAHPRE+CjeYwEqyzY7aWl/ytj7vwPAM3H+H5WUH781Oyg/VDN5758dtkp1xUs5DgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgAAEIQAACEIAABCAAAQhAAAIQ6AWB/wclGkkWAIKpGAAAAABJRU5ErkJggg==", + "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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] =?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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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" }