From d8e27910ccfc9d2f6cc22a48075828ef4a4bb91f Mon Sep 17 00:00:00 2001 From: Nick Farina Date: Sat, 22 Aug 2015 21:37:42 -0700 Subject: [PATCH 1/4] Support new API in getServices() See `getServices()` implementation in `accessories/Http.js` for an example of how to use. Fixes #114 Fixes #57 --- accessories/Http.js | 162 ++++++++++++-------------------------------- app.js | 128 +++++++++++++++++++--------------- 2 files changed, 117 insertions(+), 173 deletions(-) diff --git a/accessories/Http.js b/accessories/Http.js index bfd418b..f1b15ce 100644 --- a/accessories/Http.js +++ b/accessories/Http.js @@ -1,6 +1,11 @@ -var types = require("HAP-NodeJS/accessories/types.js"); +var Service = require("HAP-NodeJS").Service; +var Characteristic = require("HAP-NodeJS").Characteristic; var request = require("request"); +module.exports = { + accessory: HttpAccessory +} + function HttpAccessory(log, config) { this.log = log; @@ -9,9 +14,6 @@ function HttpAccessory(log, config) { this.off_url = config["off_url"]; this.brightness_url = config["brightness_url"]; this.http_method = config["http_method"]; - - // device info - this.name = config["name"]; } HttpAccessory.prototype = { @@ -26,135 +28,59 @@ HttpAccessory.prototype = { }) }, - setPowerState: function(powerOn) { + setPowerState: function(powerOn, callback) { var url; if (powerOn) { - url = this.on_url - this.log("Setting power state on the '"+this.name+"' to on"); - }else{ - url = this.off_url - this.log("Setting power state on the '"+this.name+"' to off"); + url = this.on_url; + this.log("Setting power state to on"); + } + else { + url = this.off_url; + this.log("Setting power state to off"); } - this.httpRequest(url, this.http_method, function(error, response, body){ + this.httpRequest(url, this.http_method, function(error, response, body) { if (error) { - return console.error('http power function failed:', error); - }else{ - return console.log('http power function succeeded!'); + this.log('HTTP power function failed: %s', error.message); + callback(error); } - }); - + else { + this.log('HTTP power function succeeded!'); + callback(); + } + }.bind(this)); }, - setBrightness: function(level) { + setBrightness: function(level, callback) { var url = this.brightness_url.replace("%b", level) - this.log("Setting brightness on the '"+this.name+"' to " + level); + this.log("Setting brightness to %s", level); - this.httpRequest(url, this.http_method, function(error, response, body){ + this.httpRequest(url, this.http_method, function(error, response, body) { if (error) { - return console.error('http brightness function failed:', error); - }else{ - return console.log('http brightness function succeeded!'); + this.log('HTTP brightness function failed: %s', error); + callback(error); } - }); - + else { + this.log('HTTP brightness function succeeded!'); + callback(); + } + }.bind(this)); }, - + getServices: function() { - var that = this; - return [{ - sType: types.ACCESSORY_INFORMATION_STYPE, - characteristics: [{ - cType: types.NAME_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: this.name, - supportEvents: false, - supportBonjour: false, - manfDescription: "Name of the accessory", - designedMaxLength: 255 - },{ - cType: types.MANUFACTURER_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: "Http", - supportEvents: false, - supportBonjour: false, - manfDescription: "Manufacturer", - designedMaxLength: 255 - },{ - cType: types.MODEL_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: "Rev-1", - supportEvents: false, - supportBonjour: false, - manfDescription: "Model", - designedMaxLength: 255 - },{ - cType: types.SERIAL_NUMBER_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: "A1S2NASF88EW", - supportEvents: false, - supportBonjour: false, - manfDescription: "SN", - designedMaxLength: 255 - },{ - cType: types.IDENTIFY_CTYPE, - onUpdate: null, - perms: ["pw"], - format: "bool", - initialValue: false, - supportEvents: false, - supportBonjour: false, - manfDescription: "Identify Accessory", - designedMaxLength: 1 - }] - },{ - sType: types.LIGHTBULB_STYPE, - characteristics: [{ - cType: types.NAME_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: this.name, - supportEvents: true, - supportBonjour: false, - manfDescription: "Name of service", - designedMaxLength: 255 - },{ - cType: types.POWER_STATE_CTYPE, - onUpdate: function(value) { that.setPowerState(value); }, - perms: ["pw","pr","ev"], - format: "bool", - initialValue: 0, - supportEvents: true, - supportBonjour: false, - manfDescription: "Change the power state", - designedMaxLength: 1 - },{ - cType: types.BRIGHTNESS_CTYPE, - onUpdate: function(value) { that.setBrightness(value); }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: 0, - supportEvents: true, - supportBonjour: false, - manfDescription: "Adjust Brightness", - designedMinValue: 0, - designedMaxValue: 100, - designedMinStep: 1, - unit: "%" - }] - }]; + + var lightbulbService = new Service.Lightbulb(); + + lightbulbService + .getCharacteristic(Characteristic.On) + .on('set', this.setPowerState.bind(this)); + + lightbulbService + .addCharacteristic(new Characteristic.Brightness()) + .on('set', this.setBrightness.bind(this)); + + return [lightbulbService]; } }; - -module.exports.accessory = HttpAccessory; diff --git a/app.js b/app.js index f4423b3..e678fb9 100644 --- a/app.js +++ b/app.js @@ -5,6 +5,7 @@ var hap = require('HAP-NodeJS'); var uuid = require('HAP-NodeJS').uuid; var Bridge = require('HAP-NodeJS').Bridge; var Accessory = require('HAP-NodeJS').Accessory; +var Service = require('HAP-NodeJS').Service; var accessoryLoader = require('HAP-NodeJS').AccessoryLoader; console.log("Starting HomeBridge server..."); @@ -72,28 +73,19 @@ function loadAccessories() { var accessoryConfig = config.accessories[i]; // Load up the class for this accessory - var accessoryName = accessoryConfig["accessory"]; // like "WeMo" - var accessoryModule = require('./accessories/' + accessoryName + ".js"); // like "./accessories/WeMo.js" + var accessoryType = accessoryConfig["accessory"]; // like "WeMo" + var accessoryModule = require('./accessories/' + accessoryType + ".js"); // like "./accessories/WeMo.js" var accessoryConstructor = accessoryModule.accessory; // like "WeMoAccessory", a JavaScript constructor // Create a custom logging function that prepends the device display name for debugging - var name = accessoryConfig["name"]; - var log = function(name) { return function(s) { console.log("[" + name + "] " + s); }; }(name); + var accessoryName = accessoryConfig["name"]; + var log = createLog(accessoryName); - log("Initializing " + accessoryName + " accessory..."); + log("Initializing %s accessory...", accessoryType); var accessoryInstance = new accessoryConstructor(log, accessoryConfig); - - // Extract the raw "services" for this accessory which is a big array of objects describing the various - // hooks in and out of HomeKit for the HAP-NodeJS server. - var services = accessoryInstance.getServices(); + var accessory = createAccessory(accessoryInstance, accessoryName); - // Create the actual HAP-NodeJS "Accessory" instance - var accessory = accessoryLoader.parseAccessoryJSON({ - displayName: name, - services: services - }); - // add it to the bridge bridge.addBridgedAccessory(accessory); } @@ -108,54 +100,80 @@ function loadPlatforms() { var platformConfig = config.platforms[i]; // Load up the class for this accessory - var platformName = platformConfig["platform"]; // like "Wink" - var platformModule = require('./platforms/' + platformName + ".js"); // like "./platforms/Wink.js" + var platformType = platformConfig["platform"]; // like "Wink" + var platformName = platformConfig["name"]; + var platformModule = require('./platforms/' + platformType + ".js"); // like "./platforms/Wink.js" var platformConstructor = platformModule.platform; // like "WinkPlatform", a JavaScript constructor - // Create a custom logging function that prepends the platform display name for debugging - var name = platformConfig["name"]; - var log = function(name) { return function(s) { console.log("[" + name + "] " + s); }; }(name); + // Create a custom logging function that prepends the platform name for debugging + var log = createLog(platformName); - log("Initializing " + platformName + " platform..."); + log("Initializing %s platform...", platformType); var platformInstance = new platformConstructor(log, platformConfig); - - // wrap name and log in a closure so they don't change in the callback - function getAccessories(name, log) { - asyncCalls++; - platformInstance.accessories(function(foundAccessories){ - asyncCalls--; - // loop through accessories adding them to the list and registering them - for (var i = 0; i < foundAccessories.length; i++) { - var accessoryInstance = foundAccessories[i]; - - log("Initializing device with name " + accessoryInstance.name + "...") - - // Extract the raw "services" for this accessory which is a big array of objects describing the various - // hooks in and out of HomeKit for the HAP-NodeJS server. - var services = accessoryInstance.getServices(); - - // Create the actual HAP-NodeJS "Accessory" instance - var accessory = accessoryLoader.parseAccessoryJSON({ - displayName: name, - services: services - }); - - // add it to the bridge - bridge.addBridgedAccessory(accessory); - } - - // were we the last callback? - if (asyncCalls === 0 && !asyncWait) - publish(); - }) - } - - // query for devices - getAccessories(name, log); + loadPlatformAccessories(platformInstance, log); } } +function loadPlatformAccessories(platformInstance, log) { + asyncCalls++; + platformInstance.accessories(function(foundAccessories){ + asyncCalls--; + + // loop through accessories adding them to the list and registering them + for (var i = 0; i < foundAccessories.length; i++) { + var accessoryInstance = foundAccessories[i]; + var accessoryName = accessoryInstance.name; // assume this property was set + + log("Initializing platform accessory '%s'...", accessoryName); + + var accessory = createAccessory(accessoryInstance, accessoryName); + + // add it to the bridge + bridge.addBridgedAccessory(accessory); + } + + // were we the last callback? + if (asyncCalls === 0 && !asyncWait) + publish(); + }); +} + +function createAccessory(accessoryInstance, displayName) { + + var services = accessoryInstance.getServices(); + + if (!(services[0] instanceof Service)) { + // The returned "services" for this accessory is assumed to be the old style: a big array + // of JSON-style objects that will need to be parsed by HAP-NodeJS's AccessoryLoader. + + // Create the actual HAP-NodeJS "Accessory" instance + return accessoryLoader.parseAccessoryJSON({ + displayName: displayName, + services: services + }); + } + else { + // The returned "services" for this accessory are simply an array of new-API-style + // Service instances which we can add to a created HAP-NodeJS Accessory directly. + + var accessoryUUID = uuid.generate(accessoryInstance.constructor.name + ":" + displayName); + + var accessory = new Accessory(displayName, accessoryUUID); + services.forEach(function(service) { accessory.addService(service); }); + return accessory; + } +} + +// Returns a logging function that prepends messages with the given name in [brackets]. +function createLog(name) { + return function(message) { + var rest = Array.prototype.slice.call(arguments, 1 ); // any arguments after message + var args = ["[%s] " + message, name].concat(rest); + console.log.apply(console, args); + } +} + function publish() { bridge.publish({ username: bridgeConfig.username || "CC:22:3D:E3:CE:30", From dfdbc865c8b1d44eb5facef92282e27b9cb265a8 Mon Sep 17 00:00:00 2001 From: Nick Farina Date: Sun, 23 Aug 2015 10:07:31 -0700 Subject: [PATCH 2/4] Support AccessoryInformation service --- accessories/Http.js | 15 ++++++++++++++- app.js | 27 ++++++++++++++++++++++++++- package.json | 2 +- 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/accessories/Http.js b/accessories/Http.js index f1b15ce..0967be7 100644 --- a/accessories/Http.js +++ b/accessories/Http.js @@ -69,7 +69,20 @@ HttpAccessory.prototype = { }.bind(this)); }, + identify: function() { + this.log("Identify requested!"); + }, + getServices: function() { + + // you can OPTIONALLY create an information service if you wish to override + // the default values for things like serial number, model, etc. + var informationService = new Service.AccessoryInformation(); + + informationService + .setCharacteristic(Characteristic.Manufacturer, "HTTP Manufacturer") + .setCharacteristic(Characteristic.Model, "HTTP Model") + .setCharacteristic(Characteristic.SerialNumber, "HTTP Serial Number"); var lightbulbService = new Service.Lightbulb(); @@ -81,6 +94,6 @@ HttpAccessory.prototype = { .addCharacteristic(new Characteristic.Brightness()) .on('set', this.setBrightness.bind(this)); - return [lightbulbService]; + return [informationService, lightbulbService]; } }; diff --git a/app.js b/app.js index e678fb9..3f0c275 100644 --- a/app.js +++ b/app.js @@ -6,6 +6,7 @@ var uuid = require('HAP-NodeJS').uuid; var Bridge = require('HAP-NodeJS').Bridge; var Accessory = require('HAP-NodeJS').Accessory; var Service = require('HAP-NodeJS').Service; +var Characteristic = require('HAP-NodeJS').Characteristic; var accessoryLoader = require('HAP-NodeJS').AccessoryLoader; console.log("Starting HomeBridge server..."); @@ -160,7 +161,31 @@ function createAccessory(accessoryInstance, displayName) { var accessoryUUID = uuid.generate(accessoryInstance.constructor.name + ":" + displayName); var accessory = new Accessory(displayName, accessoryUUID); - services.forEach(function(service) { accessory.addService(service); }); + + // listen for the identify event if the accessory instance has defined an identify() method + if (accessoryInstance.identify) + accessory.on('identify', function(paired, callback) { accessoryInstance.identify(callback); }); + + services.forEach(function(service) { + + // if you returned an AccessoryInformation service, merge its values with ours + if (service instanceof Service.AccessoryInformation) { + var existingService = accessory.getService(Service.AccessoryInformation); + + // pull out any values you may have defined + var manufacturer = service.getCharacteristic(Characteristic.Manufacturer).value; + var model = service.getCharacteristic(Characteristic.Model).value; + var serialNumber = service.getCharacteristic(Characteristic.SerialNumber).value; + + if (manufacturer) existingService.setCharacteristic(Characteristic.Manufacturer, manufacturer); + if (model) existingService.setCharacteristic(Characteristic.Model, model); + if (serialNumber) existingService.setCharacteristic(Characteristic.SerialNumber, serialNumber); + } + else { + accessory.addService(service); + } + }); + return accessory; } } diff --git a/package.json b/package.json index 2814343..5604b0f 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "carwingsjs": "0.0.x", "color": "0.10.x", "elkington": "kevinohara80/elkington", - "hap-nodejs": "git+https://github.com/KhaosT/HAP-NodeJS#187174846dc4b8970efba74b9eb2968b35f15d87", + "hap-nodejs": "git+https://github.com/KhaosT/HAP-NodeJS#46ba0597eb339983a14d98c53764a58a5516fcd2", "harmonyhubjs-client": "^1.1.4", "harmonyhubjs-discover": "git+https://github.com/swissmanu/harmonyhubjs-discover.git", "mdns": "^2.2.4", From 76e7ef267756dc17420cd24f5649839c5882f002 Mon Sep 17 00:00:00 2001 From: Nick Farina Date: Sun, 23 Aug 2015 11:45:34 -0700 Subject: [PATCH 3/4] Fix identify() in Http --- accessories/Http.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/accessories/Http.js b/accessories/Http.js index 0967be7..e1859cf 100644 --- a/accessories/Http.js +++ b/accessories/Http.js @@ -69,8 +69,9 @@ HttpAccessory.prototype = { }.bind(this)); }, - identify: function() { + identify: function(callback) { this.log("Identify requested!"); + callback(); // success }, getServices: function() { From 77fbe5b53c194ef58ef250f3c5361d4c626e3816 Mon Sep 17 00:00:00 2001 From: Nick Farina Date: Mon, 24 Aug 2015 19:23:43 -0700 Subject: [PATCH 4/4] Fix for Domoticz platform async callbacks --- platforms/Domoticz.js | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/platforms/Domoticz.js b/platforms/Domoticz.js index a9e8e72..d80e6d6 100644 --- a/platforms/Domoticz.js +++ b/platforms/Domoticz.js @@ -66,11 +66,17 @@ DomoticzPlatform.prototype = { }, accessories: function(callback) { - this.log("Fetching Domoticz lights and switches..."); - var that = this; - var foundAccessories = []; - if (this.roomid == 0) { + this.log("Fetching Domoticz lights and switches..."); + var that = this; + var foundAccessories = []; + + // mechanism to ensure callback is only executed once all requests complete + var asyncCalls = 0; + function callbackLater() { if (--asyncCalls == 0) callback(foundAccessories); } + + if (this.roomid == 0) { //Get Lights + asyncCalls++; request.get({ url: this.urlForQuery("type=devices&filter=light&used=true&order=Name"), json: true @@ -83,14 +89,15 @@ DomoticzPlatform.prototype = { foundAccessories.push(accessory); }) } - callback(foundAccessories); + callbackLater(); } else { that.log("There was a problem connecting to Domoticz. (" + err + ")"); } }); - } - else { + } + else { //Get all devices specified in the room + asyncCalls++; request.get({ url: this.urlForQuery("type=devices&plan=" + this.roomid), json: true @@ -106,30 +113,30 @@ DomoticzPlatform.prototype = { } }) } - callback(foundAccessories); + callbackLater(); } else { that.log("There was a problem connecting to Domoticz."); } }); - } + } //Get Scenes - foundAccessories = []; + asyncCalls++; request.get({ url: this.urlForQuery("type=scenes"), - json: true - }, function(err, response, json) { + json: true + }, function(err, response, json) { if (!err && response.statusCode == 200) { if (json['result'] != undefined) { var sArray=sortByKey(json['result'],"Name"); sArray.map(function(s) { accessory = new DomoticzAccessory(that.log, that, true, s.idx, s.Name, false, 0, false); foundAccessories.push(accessory); - }) + }) } - callback(foundAccessories); + callbackLater(); } else { that.log("There was a problem connecting to Domoticz."); - } + } }); } }