From d8e27910ccfc9d2f6cc22a48075828ef4a4bb91f Mon Sep 17 00:00:00 2001 From: Nick Farina Date: Sat, 22 Aug 2015 21:37:42 -0700 Subject: [PATCH 01/63] 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 02/63] 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 03/63] 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 5782ff997f38870ffaa68c7da94df1fb73f13c4e Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Fri, 7 Aug 2015 20:42:28 +0200 Subject: [PATCH 04/63] Initial commit of ZWayServer platform. Not much working, having problems with authenticating. --- package.json | 1 + platforms/ZWayServer.js | 300 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 301 insertions(+) create mode 100644 platforms/ZWayServer.js diff --git a/package.json b/package.json index 2814343..75a45b1 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "node-hue-api": "^1.0.5", "node-milight-promise": "0.0.2", "node-persist": "0.0.x", + "q": "1.4.x", "request": "2.49.x", "sonos": "0.8.x", "telldus-live": "0.2.x", diff --git a/platforms/ZWayServer.js b/platforms/ZWayServer.js new file mode 100644 index 0000000..539e7df --- /dev/null +++ b/platforms/ZWayServer.js @@ -0,0 +1,300 @@ +var types = require("HAP-NodeJS/accessories/types.js"); +var request = require("request"); +var Q = require("q"); + +function ZWayServerPlatform(log, config){ + this.log = log; + this.url = config["url"]; + this.login = config["login"]; + this.password = config["password"]; + this.name_overrides = config["name_overrides"]; + + this.jar = request.jar(); +} + +ZWayServerPlatform.prototype = { + + zwayRequest: function(verb, opts){ + var that = this; + var deferred = Q.defer(); + + opts.jar = this.jar; + opts.json = true; +//opts.proxy = 'http://localhost:8888'; + + var rmethod = request[verb]; + rmethod(opts) + .on('response', function(response){ + if(response.statusCode == 401){ +that.log("Authenticating..."); + request.post({ + url: that.url + 'ZAutomation/api/v1/login', + body: { //JSON.stringify({ + "form": true, + "login": that.login, + "password": that.password, + "keepme": false, + "default_ui": 1 + }, + headers: { + "Accept": "application/json", + "Content-Type": "application/json" + }, + json: true, + jar: that.jar + }).on('response', function(response){ + if(response.statusCode == 200){ + that.log("Authenticated. Resubmitting original request..."); + rmethod(opts).on('response', function(response){ + if(response.statusCode == 200){ + deferred.resolve(response); + } else { + deferred.reject(response); + } + }); + } else { + deferred.reject(response); + } + }); + } else if(response.statusCode == 200) { + deferred.resolve(response); + } else { + deferred.reject(response); + } + }); + return deferred.promise; + } + , + + accessories: function(callback) { + this.log("Fetching Z-Way devices..."); + + var that = this; + var foundAccessories = []; + + this.zwayRequest('get', { + url: this.url + 'ZAutomation/api/v1/devices' + }).then(function(result){ + var devices = result.data.devices; + var groupedDevices = {}; + for(var i = 0; i < devices.length; i++){ + var dentry = devices[i]; + var gdid = dentry.id.replace(/^(.*?)_zwqy_(\d+-\d+)-\d/, '$1_$2'); + var gd = groupedDevices[gdid] || (groupedDevices[gdid] = []); + gd.push(dentry); + } + for(var gdid in groupedDevices) { + if(!groupedDevices.hasOwnProperty(gdid)) continue; + this.log('Got grouped device ' + gdid + ' consiting of devices:'); + var gd = groupedDevices[gdid]; + for(var j = 0; j < gd.length; j++){ + this.log(gd[j].id); + } + var accessory = new ZWayServerAccessory(); + foundAccessories.push(accessory); + } + //callback(foundAccessories); + }); + + } + +} + +function ZWayServerAccessory(log, name, commands) { + // device info + this.name = name; + this.commands = commands; + this.log = log; +} +/* +SmartThingsAccessory.prototype = { + + command: function(c,value) { + this.log(this.name + " sending command " + c); + var url = this.commands[c]; + if (value != undefined) { + url = this.commands[c] + "&value="+value + } + + var that = this; + request.put({ + url: url + }, function(err, response) { + if (err) { + that.log("There was a problem sending command " + c + " to" + that.name); + that.log(url); + } else { + that.log(that.name + " sent command " + c); + } + }) + }, + + informationCharacteristics: function() { + return [ + { + 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: "SmartThings", + 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 + } + ] + }, + + controlCharacteristics: function(that) { + cTypes = [{ + cType: types.NAME_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: this.name, + supportEvents: true, + supportBonjour: false, + manfDescription: "Name of service", + designedMaxLength: 255 + }] + + if (this.commands['on'] != undefined) { + cTypes.push({ + cType: types.POWER_STATE_CTYPE, + onUpdate: function(value) { + if (value == 0) { + that.command("off") + } else { + that.command("on") + } + }, + perms: ["pw","pr","ev"], + format: "bool", + initialValue: 0, + supportEvents: true, + supportBonjour: false, + manfDescription: "Change the power state", + designedMaxLength: 1 + }) + } + + if (this.commands['on'] != undefined) { + cTypes.push({ + cType: types.BRIGHTNESS_CTYPE, + onUpdate: function(value) { that.command("setLevel", value); }, + perms: ["pw","pr","ev"], + format: "int", + initialValue: 0, + supportEvents: true, + supportBonjour: false, + manfDescription: "Adjust Brightness of Light", + designedMinValue: 0, + designedMaxValue: 100, + designedMinStep: 1, + unit: "%" + }) + } + + if (this.commands['setHue'] != undefined) { + cTypes.push({ + cType: types.HUE_CTYPE, + onUpdate: function(value) { that.command("setHue", value); }, + perms: ["pw","pr","ev"], + format: "int", + initialValue: 0, + supportEvents: true, + supportBonjour: false, + manfDescription: "Adjust Hue of Light", + designedMinValue: 0, + designedMaxValue: 360, + designedMinStep: 1, + unit: "arcdegrees" + }) + } + + if (this.commands['setSaturation'] != undefined) { + cTypes.push({ + cType: types.SATURATION_CTYPE, + onUpdate: function(value) { that.command("setSaturation", value); }, + perms: ["pw","pr","ev"], + format: "int", + initialValue: 0, + supportEvents: true, + supportBonjour: false, + manfDescription: "Adjust Brightness of Light", + designedMinValue: 0, + designedMaxValue: 100, + designedMinStep: 1, + unit: "%" + }) + } + + return cTypes + }, + + sType: function() { + if (this.commands['setLevel'] != undefined) { + return types.LIGHTBULB_STYPE + } else { + return types.SWITCH_STYPE + } + }, + + getServices: function() { + var that = this; + var services = [{ + sType: types.ACCESSORY_INFORMATION_STYPE, + characteristics: this.informationCharacteristics(), + }, + { + sType: this.sType(), + characteristics: this.controlCharacteristics(that) + }]; + this.log("Loaded services for " + this.name) + return services; + } +}; +*/ + +module.exports.accessory = ZWayServerAccessory; +module.exports.platform = ZWayServerPlatform; From c6877193cc1c8346b5460f9c287d91845dc46363 Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Sat, 15 Aug 2015 06:17:58 +0200 Subject: [PATCH 05/63] Early, but lots done This works (sometimes at least) but has lots of flaws, including ones that make the whole bridge unreachable. --- package.json | 1 + platforms/ZWayServer.js | 691 +++++++++++++++++++++++++++------------- 2 files changed, 475 insertions(+), 217 deletions(-) diff --git a/package.json b/package.json index 75a45b1..873fc73 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "node-milight-promise": "0.0.2", "node-persist": "0.0.x", "q": "1.4.x", + "tough-cookie": "^2.0.0", "request": "2.49.x", "sonos": "0.8.x", "telldus-live": "0.2.x", diff --git a/platforms/ZWayServer.js b/platforms/ZWayServer.js index 539e7df..c81b093 100644 --- a/platforms/ZWayServer.js +++ b/platforms/ZWayServer.js @@ -1,34 +1,121 @@ var types = require("HAP-NodeJS/accessories/types.js"); var request = require("request"); +var tough = require('tough-cookie'); var Q = require("q"); -function ZWayServerPlatform(log, config){ - this.log = log; - this.url = config["url"]; - this.login = config["login"]; - this.password = config["password"]; - this.name_overrides = config["name_overrides"]; +var zwshkDeviceClasses = [ + { + primaryType: "switchBinary", + subTypes: { + "battery": true, + "sensorMultilevel.Electric": true + }, + tcType: types.SWITCH_TCTYPE + } + , + { + primaryType: "thermostat", + subTypes: { + "sensorMultiLevel.Temperature": true, + "battery": true + }, + tcType: types.THERMOSTAT_TCTYPE + } + , + { + primaryType: "sensorBinary.Door/Window", + subTypes: { + "battery": true + }, + tcType: types.SENSOR_TCTYPE + } + , + { + primaryType: "sensorMultilevel.Temperature", + subTypes: { + "battery": true + }, + tcType: types.SENSOR_TCTYPE + } + , + { + primaryType: "switchMultilevel", + subTypes: { + "battery": true + }, + tcType: types.LIGHTBULB_TCTYPE + } +]; - this.jar = request.jar(); +function ZWayServerPlatform(log, config){ + this.log = log; + this.url = config["url"]; + this.login = config["login"]; + this.password = config["password"]; + this.name_overrides = config["name_overrides"]; + this.userAgent = "HomeBridge/-1^0.5"; + this.sessionId = ""; + this.jar = request.jar(new tough.CookieJar()); +} + +ZWayServerPlatform.getVDevTypeKey = function(vdev){ + return vdev.deviceType + (vdev.metrics && vdev.metrics.probeTitle ? "." + vdev.metrics.probeTitle : "") +} + +ZWayServerPlatform.getVDevServiceTypes = function(vdev){ + var typeKey = ZWayServerPlatform.getVDevTypeKey(vdev); + switch (typeKey) { + case "switchBinary": + return [types.SWITCH_STYPE]; + case "switchMultilevel": + return [types.LIGHTBULB_STYPE]; + case "thermostat": + return [types.THERMOSTAT_STYPE]; + case "sensorMultilevel.Temperature": + return [types.TEMPERATURE_SENSOR_STYPE]; + case "sensorBinary.Door/Window": + return [types.GARAGE_DOOR_OPENER_STYPE]; + } +} + +ZWayServerPlatform.getVDevCharacteristicsTypes = function(vdev){ + var typeKey = ZWayServerPlatform.getVDevTypeKey(vdev); + switch (typeKey) { + case "switchBinary": + return [types.POWER_STATE_CTYPE]; + case "switchMultilevel": + return [types.POWER_STATE_CTYPE, types.BRIGHTNESS_CTYPE]; + case "thermostat": + return [types.TARGET_TEMPERATURE_CTYPE, types.TEMPERATURE_UNITS_CTYPE, types.CURRENTHEATINGCOOLING_CTYPE, types.TARGETHEATINGCOOLING_CTYPE]; + case "sensorMultilevel.Temperature": + return [types.CURRENT_TEMPERATURE_CTYPE, types.TEMPERATURE_UNITS_CTYPE]; + case "sensorBinary.Door/Window": + return [types.CURRENT_DOOR_STATE_CTYPE]; + case "battery.Battery": + return [types.BATTERY_LEVEL_CTYPE, types.STATUS_LOW_BATTERY_CTYPE]; + } } ZWayServerPlatform.prototype = { - zwayRequest: function(verb, opts){ + zwayRequest: function(opts){ var that = this; var deferred = Q.defer(); - opts.jar = this.jar; + opts.jar = true;//this.jar; opts.json = true; -//opts.proxy = 'http://localhost:8888'; + opts.headers = { + "Cookie": "ZWAYSession=" + this.sessionId + }; +opts.proxy = 'http://localhost:8888'; - var rmethod = request[verb]; - rmethod(opts) - .on('response', function(response){ + request(opts, function(error, response, body){ if(response.statusCode == 401){ that.log("Authenticating..."); - request.post({ + request({ + method: "POST", url: that.url + 'ZAutomation/api/v1/login', +proxy: 'http://localhost:8888', body: { //JSON.stringify({ "form": true, "login": that.login, @@ -38,16 +125,19 @@ that.log("Authenticating..."); }, headers: { "Accept": "application/json", - "Content-Type": "application/json" + "Content-Type": "application/json", + "User-Agent": that.userAgent }, json: true, - jar: that.jar - }).on('response', function(response){ + jar: true//that.jar + }, function(error, response, body){ if(response.statusCode == 200){ + that.sessionId = body.data.sid; + opts.headers["Cookie"] = "ZWAYSession=" + that.sessionId; that.log("Authenticated. Resubmitting original request..."); - rmethod(opts).on('response', function(response){ + request(opts, function(error, response, body){ if(response.statusCode == 200){ - deferred.resolve(response); + deferred.resolve(body); } else { deferred.reject(response); } @@ -57,7 +147,7 @@ that.log("Authenticating..."); } }); } else if(response.statusCode == 200) { - deferred.resolve(response); + deferred.resolve(body); } else { deferred.reject(response); } @@ -72,229 +162,396 @@ that.log("Authenticating..."); var that = this; var foundAccessories = []; - this.zwayRequest('get', { + this.zwayRequest({ + method: "GET", url: this.url + 'ZAutomation/api/v1/devices' }).then(function(result){ var devices = result.data.devices; var groupedDevices = {}; for(var i = 0; i < devices.length; i++){ - var dentry = devices[i]; - var gdid = dentry.id.replace(/^(.*?)_zwqy_(\d+-\d+)-\d/, '$1_$2'); - var gd = groupedDevices[gdid] || (groupedDevices[gdid] = []); - gd.push(dentry); + var vdev = devices[i]; + var gdid = vdev.id.replace(/^(.*?)_zway_(\d+-\d+)-\d.*/, '$1_$2'); + var gd = groupedDevices[gdid] || (groupedDevices[gdid] = {devices: [], types: {}, primary: undefined}); + gd.devices.push(vdev); + gd.types[ZWayServerPlatform.getVDevTypeKey(vdev)] = gd.devices.length - 1; + gd.types[vdev.deviceType] = gd.devices.length - 1; // also include the deviceType only as a possibility } + //TODO: Make a second pass, re-splitting any devices that don't make sense together for(var gdid in groupedDevices) { if(!groupedDevices.hasOwnProperty(gdid)) continue; - this.log('Got grouped device ' + gdid + ' consiting of devices:'); + + // Debug/log... + that.log('Got grouped device ' + gdid + ' consiting of devices:'); var gd = groupedDevices[gdid]; - for(var j = 0; j < gd.length; j++){ - this.log(gd[j].id); + for(var j = 0; j < gd.devices.length; j++){ + that.log(gd.devices[j].id + " - " + gd.devices[j].deviceType + (gd.devices[j].metrics && gd.devices[j].metrics.probeTitle ? "." + gd.devices[j].metrics.probeTitle : "")); } - var accessory = new ZWayServerAccessory(); + + var accessory = null; + for(var ti = 0; ti < zwshkDeviceClasses.length; ti++){ + if(gd.types[zwshkDeviceClasses[ti].primaryType] !== undefined){ + gd.primary = gd.types[zwshkDeviceClasses[ti].primaryType]; + var pd = gd.devices[gd.primary]; + var name = pd.metrics && pd.metrics.title ? pd.metrics.title : pd.id; + that.log("Using class with primaryType " + zwshkDeviceClasses[ti].primaryType + ", " + name + " (" + pd.id + ") as primary."); + accessory = new ZWayServerAccessory(name, zwshkDeviceClasses[ti], gd, that); + break; + } + } + if(!accessory) that.log("WARN: Didn't find suitable device class!"); + + //var accessory = new ZWayServerAccessory(); foundAccessories.push(accessory); } - //callback(foundAccessories); +foundAccessories = [foundAccessories[0], foundAccessories[1], foundAccessories[2], foundAccessories[3]]; // Limit to a few devices for testing... + callback(foundAccessories); }); } } -function ZWayServerAccessory(log, name, commands) { +function ZWayServerAccessory(name, dclass, devDesc, platform) { // device info this.name = name; - this.commands = commands; - this.log = log; + this.dclass = dclass; + this.devDesc = devDesc; + this.platform = platform; + this.log = platform.log; } -/* -SmartThingsAccessory.prototype = { - command: function(c,value) { - this.log(this.name + " sending command " + c); - var url = this.commands[c]; - if (value != undefined) { - url = this.commands[c] + "&value="+value - } - var that = this; - request.put({ - url: url - }, function(err, response) { - if (err) { - that.log("There was a problem sending command " + c + " to" + that.name); - that.log(url); - } else { - that.log(that.name + " sent command " + c); - } - }) - }, +ZWayServerAccessory.prototype = { - informationCharacteristics: function() { - return [ - { - 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: "SmartThings", - 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 - } - ] - }, - - controlCharacteristics: function(that) { - cTypes = [{ - cType: types.NAME_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: this.name, - supportEvents: true, - supportBonjour: false, - manfDescription: "Name of service", - designedMaxLength: 255 - }] - - if (this.commands['on'] != undefined) { - cTypes.push({ - cType: types.POWER_STATE_CTYPE, - onUpdate: function(value) { - if (value == 0) { - that.command("off") - } else { - that.command("on") - } - }, - perms: ["pw","pr","ev"], - format: "bool", - initialValue: 0, - supportEvents: true, - supportBonjour: false, - manfDescription: "Change the power state", - designedMaxLength: 1 - }) - } - - if (this.commands['on'] != undefined) { - cTypes.push({ - cType: types.BRIGHTNESS_CTYPE, - onUpdate: function(value) { that.command("setLevel", value); }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: 0, - supportEvents: true, - supportBonjour: false, - manfDescription: "Adjust Brightness of Light", - designedMinValue: 0, - designedMaxValue: 100, - designedMinStep: 1, - unit: "%" - }) - } - - if (this.commands['setHue'] != undefined) { - cTypes.push({ - cType: types.HUE_CTYPE, - onUpdate: function(value) { that.command("setHue", value); }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: 0, - supportEvents: true, - supportBonjour: false, - manfDescription: "Adjust Hue of Light", - designedMinValue: 0, - designedMaxValue: 360, - designedMinStep: 1, - unit: "arcdegrees" - }) - } - - if (this.commands['setSaturation'] != undefined) { - cTypes.push({ - cType: types.SATURATION_CTYPE, - onUpdate: function(value) { that.command("setSaturation", value); }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: 0, - supportEvents: true, - supportBonjour: false, - manfDescription: "Adjust Brightness of Light", - designedMinValue: 0, - designedMaxValue: 100, - designedMinStep: 1, - unit: "%" - }) - } - - return cTypes - }, - - sType: function() { - if (this.commands['setLevel'] != undefined) { - return types.LIGHTBULB_STYPE - } else { - return types.SWITCH_STYPE - } - }, - - getServices: function() { - var that = this; - var services = [{ - sType: types.ACCESSORY_INFORMATION_STYPE, - characteristics: this.informationCharacteristics(), + command: function(vdev, command, value) { + this.platform.zwayRequest({ + method: "GET", + url: this.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id + '/command/' + command + (value === undefined ? "" : "/" + value) + }); }, - { - sType: this.sType(), - characteristics: this.controlCharacteristics(that) - }]; - this.log("Loaded services for " + this.name) - return services; - } + + informationCharacteristics: function() { + return [ + { + 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: "Z-Wave.me", + supportEvents: false, + supportBonjour: false, + manfDescription: "Manufacturer", + designedMaxLength: 255 + },{ + cType: types.MODEL_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: "VDev", + supportEvents: false, + supportBonjour: false, + manfDescription: "Model", + designedMaxLength: 255 + },{ + cType: types.SERIAL_NUMBER_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: "VDev-" + this.devDesc.devices[this.devDesc.primary].h, //TODO: Is this valid? + 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 + } + ] + }, + + controlCharacteristics: function(vdev) { + var that = this; + var cTypes = []; + + var cxs = ZWayServerPlatform.getVDevCharacteristicsTypes(vdev); + + if(!cxs || cxs.length <= 0) return cTypes; + + if (cxs.indexOf(types.POWER_STATE_CTYPE) >= 0) { + cTypes.push({ + cType: types.POWER_STATE_CTYPE, + onUpdate: function(value) { + if (value == 0) { + that.command(vdev, "off"); + } else { + that.command(vdev, "on"); + } + }, + perms: ["pw","pr","ev"], + format: "bool", + initialValue: 0, + supportEvents: true, + supportBonjour: false, + manfDescription: "Change the power state", + designedMaxLength: 1 + }); + } + + if (cxs.indexOf(types.BRIGHTNESS_CTYPE) >= 0) { + cTypes.push({ + cType: types.BRIGHTNESS_CTYPE, + onUpdate: function(value) { + that.command(vdev, "exact", value); + }, + perms: ["pw","pr","ev"], + format: "int", + initialValue: 0, + supportEvents: true, + supportBonjour: false, + manfDescription: "Adjust Brightness of Light", + designedMinValue: 0, + designedMaxValue: 100, + designedMinStep: 1, + unit: "%" + }); + } + + if (cxs.indexOf(types.CURRENT_TEMPERATURE_CTYPE) >= 0) { + cTypes.push({ + cType: types.CURRENT_TEMPERATURE_CTYPE, + onUpdate: null, + onRead: function(callback) { + that.platform.zwayRequest({ + method: "GET", + url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id + }).then(function(result){ + callback(result.data.metrics.level); + }); + }, + perms: ["pr","ev"], + format: "int", + initialValue: 20, + supportEvents: false, + supportBonjour: false, + manfDescription: "Current Temperature", + unit: "celsius" + }); + } + + if (cxs.indexOf(types.TARGET_TEMPERATURE_CTYPE) >= 0) { + cTypes.push({ + cType: types.TARGET_TEMPERATURE_CTYPE, + onUpdate: function(value) { + that.command(vdev, "exact", value); + }, + onRead: function(callback) { + that.platform.zwayRequest({ + method: "GET", + url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id + }).then(function(result){ + callback(result.data.metrics.level); + }); + }, + perms: ["pw","pr","ev"], + format: "int", + initialValue: 20, + supportEvents: false, + supportBonjour: false, + manfDescription: "Target Temperature", + designedMinValue: 2, + designedMaxValue: 38, + designedMinStep: 1, + unit: "celsius" + }); + } + + if (cxs.indexOf(types.TEMPERATURE_UNITS_CTYPE) >= 0) { + cTypes.push({ + cType: types.TEMPERATURE_UNITS_CTYPE, + perms: ["pr"], + format: "int", + //TODO: Let this update from the device if it changes after startup. + initialValue: vdev.metrics.scaleTitle.indexOf("F") >= 0 ? 1 : 0, + supportEvents: false, + supportBonjour: false, + manfDescription: "Unit", + }); + } + + if (cxs.indexOf(types.CURRENTHEATINGCOOLING_CTYPE) >= 0) { + cTypes.push({ + cType: types.CURRENTHEATINGCOOLING_CTYPE, + //TODO: Support multifunction thermostats...only heating supported now. + /* + onUpdate: null, + onRead: function(callback) { + that.getCurrentHeatingCooling(function(currentHeatingCooling){ + callback(currentHeatingCooling); + }); + }, + */ + perms: ["pr"], + format: "int", + initialValue: 1, + supportEvents: false, + supportBonjour: false, + manfDescription: "Current Mode", + designedMaxLength: 1, + designedMinValue: 0, + designedMaxValue: 2, + designedMinStep: 1, + }); + } + + if (cxs.indexOf(types.TARGETHEATINGCOOLING_CTYPE) >= 0) { + cTypes.push({ + cType: types.TARGETHEATINGCOOLING_CTYPE, + //TODO: Support multifunction thermostats...only heating supported now. + /* + onUpdate: function(value) { + that.setTargetHeatingCooling(value); + }, + onRead: function(callback) { + that.getTargetHeatingCoooling(function(targetHeatingCooling){ + callback(targetHeatingCooling); + }); + }, + */ + perms: ["pr"], + format: "int", + initialValue: 0, + supportEvents: false, + supportBonjour: false, + manfDescription: "Target Mode", + designedMinValue: 0, + designedMaxValue: 3, + designedMinStep: 1, + }); + } + + if (cxs.indexOf(types.CONTACT_SENSOR_STATE_CTYPE) >= 0) { + cTypes.push({ + cType: types.CONTACT_SENSOR_STATE_CTYPE, + onUpdate: null, + onRead: function(callback) { + that.platform.zwayRequest({ + method: "GET", + url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id + }).then(function(result){ + callback(result.data.metrics.level == "off" ? 0 : 1); + }); + }, + perms: ["pr","ev"], + format: "bool", + initialValue: 0, + supportEvents: false, + supportBonjour: false, + manfDescription: "Contact State", + designedMaxLength: 1 + }); + } + + if (cxs.indexOf(types.CURRENT_DOOR_STATE_CTYPE) >= 0) { + cTypes.push({ + cType: types.CURRENT_DOOR_STATE_CTYPE, + onRead: function(callback) { + that.platform.zwayRequest({ + method: "GET", + url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id + }).then(function(result){ + callback(result.data.metrics.level == "off" ? 0 : 1); + }); + }, + perms: ["pr","ev"], + format: "int", + initialValue: 0, + supportEvents: false, + supportBonjour: false, + manfDescription: "Current Door State", + designedMinValue: 0, + designedMaxValue: 4, + designedMinStep: 1, + designedMaxLength: 1 + }); + } + + return cTypes; + }, + + getServices: function() { + var that = this; + var services = [{ + sType: types.ACCESSORY_INFORMATION_STYPE, + characteristics: this.informationCharacteristics(), + }]; + + // rearrange the array so the primary is first + var vdevs = this.devDesc.devices.concat(); + var p = vdevs.splice(this.devDesc.primary, 1)[0]; + vdevs.unshift(p); + /* + for(var i = 0; i < vdevs.length; i++){ + var sTypes = ZWayServerPlatform.getVDevServiceTypes(vdevs[i]); + if(!sTypes) continue; + for(var j = 0; j < sTypes.length; j++){ + services.push({ + sType: sTypes[j], + characteristics: this.controlCharacteristics(vdevs[i]) + }); + } + } + */ + + var sTypes = ZWayServerPlatform.getVDevServiceTypes(vdevs[0]); + var cTypes = [{ + cType: types.NAME_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: this.name, + supportEvents: true, + supportBonjour: false, + manfDescription: "Name of service", + designedMaxLength: 255 + }]; + if(sTypes) for(var i = 0; i < vdevs.length; i++){ + cTypes = cTypes.concat(this.controlCharacteristics(vdevs[i])); + } + + // Scrub/eliminate duplicate cTypes? This is a lot of guesswork ATM... + var hits = {}; + for (var i = 0; i < cTypes.length; i++){ + if(hits[cTypes[i].cType]) cTypes.splice(i--, 1); // Remember postfix means post-evaluate! + hits[cTypes[i].cType] = true; + } + + services.push({ + sType: sTypes[0], + characteristics: cTypes + }); + //... + + this.log("Loaded services for " + this.name); + return services; + } }; -*/ module.exports.accessory = ZWayServerAccessory; module.exports.platform = ZWayServerPlatform; From 9d7a6768b889bcc1f37ecf1243a86e4c87de3323 Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Sat, 15 Aug 2015 07:21:48 +0200 Subject: [PATCH 06/63] Working at the moment. Support for several device types. Gotta be careful to not throw the bridge out of compliance! --- platforms/ZWayServer.js | 48 +++++++++++++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/platforms/ZWayServer.js b/platforms/ZWayServer.js index c81b093..b6fd29d 100644 --- a/platforms/ZWayServer.js +++ b/platforms/ZWayServer.js @@ -90,7 +90,7 @@ ZWayServerPlatform.getVDevCharacteristicsTypes = function(vdev){ case "sensorMultilevel.Temperature": return [types.CURRENT_TEMPERATURE_CTYPE, types.TEMPERATURE_UNITS_CTYPE]; case "sensorBinary.Door/Window": - return [types.CURRENT_DOOR_STATE_CTYPE]; + return [types.CURRENT_DOOR_STATE_CTYPE, types.TARGET_DOORSTATE_CTYPE, types.OBSTRUCTION_DETECTED_CTYPE]; case "battery.Battery": return [types.BATTERY_LEVEL_CTYPE, types.STATUS_LOW_BATTERY_CTYPE]; } @@ -203,7 +203,7 @@ proxy: 'http://localhost:8888', //var accessory = new ZWayServerAccessory(); foundAccessories.push(accessory); } -foundAccessories = [foundAccessories[0], foundAccessories[1], foundAccessories[2], foundAccessories[3]]; // Limit to a few devices for testing... +foundAccessories = [foundAccessories[0], foundAccessories[1], foundAccessories[2], foundAccessories[3], foundAccessories[4]]; // Limit to a few devices for testing... callback(foundAccessories); }); @@ -455,12 +455,12 @@ ZWayServerAccessory.prototype = { method: "GET", url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id }).then(function(result){ - callback(result.data.metrics.level == "off" ? 0 : 1); + callback(result.data.metrics.level == "off" ? 1 : 0); }); }, perms: ["pr","ev"], format: "bool", - initialValue: 0, + initialValue: 1, supportEvents: false, supportBonjour: false, manfDescription: "Contact State", @@ -476,12 +476,12 @@ ZWayServerAccessory.prototype = { method: "GET", url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id }).then(function(result){ - callback(result.data.metrics.level == "off" ? 0 : 1); + callback(result.data.metrics.level == "off" ? 1 : 0); }); }, perms: ["pr","ev"], format: "int", - initialValue: 0, + initialValue: 1, supportEvents: false, supportBonjour: false, manfDescription: "Current Door State", @@ -492,6 +492,42 @@ ZWayServerAccessory.prototype = { }); } + if (cxs.indexOf(types.TARGET_DOORSTATE_CTYPE) >= 0) { + cTypes.push({ + cType: types.TARGET_DOORSTATE_CTYPE, + onRead: function(callback) { + that.platform.zwayRequest({ + method: "GET", + url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id + }).then(function(result){ + callback(result.data.metrics.level == "off" ? 0 : 1); + }); + }, + perms: ["pr","ev"], //TODO: If we support some non-sensor device that can actually open, add "pw"! + format: "int", + initialValue: 0, + supportEvents: false, + supportBonjour: false, + manfDescription: "Target Door State", + designedMinValue: 0, + designedMaxValue: 1, + designedMinStep: 1, + designedMaxLength: 1 + }); + } + + if (cxs.indexOf(types.OBSTRUCTION_DETECTED_CTYPE) >= 0) { + cTypes.push({ + cType: types.OBSTRUCTION_DETECTED_CTYPE, + perms: ["pr","ev"], + format: "bool", + initialValue: false, + supportEvents: false, + supportBonjour: false, + manfDescription: "Obstruction Detected" + }); + } + return cTypes; }, From f72cb43043bfdec4636c9a8250afc400c28ebc25 Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Mon, 24 Aug 2015 06:39:00 +0200 Subject: [PATCH 07/63] Numerous fixes and improvements. Committing before merging back to master, since the upstream branches have merged into master! --- platforms/ZWayServer.js | 102 ++++++++++++++++++++++++++++++++++------ 1 file changed, 87 insertions(+), 15 deletions(-) diff --git a/platforms/ZWayServer.js b/platforms/ZWayServer.js index b6fd29d..6053632 100644 --- a/platforms/ZWayServer.js +++ b/platforms/ZWayServer.js @@ -53,6 +53,7 @@ function ZWayServerPlatform(log, config){ this.login = config["login"]; this.password = config["password"]; this.name_overrides = config["name_overrides"]; + this.batteryLow = config["battery_low_level"]; this.userAgent = "HomeBridge/-1^0.5"; this.sessionId = ""; this.jar = request.jar(new tough.CookieJar()); @@ -107,15 +108,15 @@ ZWayServerPlatform.prototype = { opts.headers = { "Cookie": "ZWAYSession=" + this.sessionId }; -opts.proxy = 'http://localhost:8888'; +//opts.proxy = 'http://localhost:8888'; request(opts, function(error, response, body){ if(response.statusCode == 401){ -that.log("Authenticating..."); + that.log("Authenticating..."); request({ method: "POST", url: that.url + 'ZAutomation/api/v1/login', -proxy: 'http://localhost:8888', +//proxy: 'http://localhost:8888', body: { //JSON.stringify({ "form": true, "login": that.login, @@ -170,6 +171,7 @@ proxy: 'http://localhost:8888', var groupedDevices = {}; for(var i = 0; i < devices.length; i++){ var vdev = devices[i]; + if(vdev.tags.indexOf("HomeBridge:Skip") >= 0) { that.log("Tag says skip!"); continue; } var gdid = vdev.id.replace(/^(.*?)_zway_(\d+-\d+)-\d.*/, '$1_$2'); var gd = groupedDevices[gdid] || (groupedDevices[gdid] = {devices: [], types: {}, primary: undefined}); gd.devices.push(vdev); @@ -198,12 +200,14 @@ proxy: 'http://localhost:8888', break; } } - if(!accessory) that.log("WARN: Didn't find suitable device class!"); - - //var accessory = new ZWayServerAccessory(); - foundAccessories.push(accessory); + + if(!accessory) + that.log("WARN: Didn't find suitable device class!"); + else + foundAccessories.push(accessory); + } -foundAccessories = [foundAccessories[0], foundAccessories[1], foundAccessories[2], foundAccessories[3], foundAccessories[4]]; // Limit to a few devices for testing... +//foundAccessories = foundAccessories.slice(0, 10); // Limit to a few devices for testing... callback(foundAccessories); }); @@ -224,9 +228,10 @@ function ZWayServerAccessory(name, dclass, devDesc, platform) { ZWayServerAccessory.prototype = { command: function(vdev, command, value) { - this.platform.zwayRequest({ + return this.platform.zwayRequest({ method: "GET", - url: this.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id + '/command/' + command + (value === undefined ? "" : "/" + value) + url: this.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id + '/command/' + command, + qs: (value === undefined ? undefined : value) }); }, @@ -318,7 +323,7 @@ ZWayServerAccessory.prototype = { cTypes.push({ cType: types.BRIGHTNESS_CTYPE, onUpdate: function(value) { - that.command(vdev, "exact", value); + that.command(vdev, "exact", {level: parseInt(value, 10)}); }, perms: ["pw","pr","ev"], format: "int", @@ -359,7 +364,12 @@ ZWayServerAccessory.prototype = { cTypes.push({ cType: types.TARGET_TEMPERATURE_CTYPE, onUpdate: function(value) { - that.command(vdev, "exact", value); + try { + that.command(vdev, "exact", {level: parseFloat(value)}); + } + catch (e) { + that.log(e); + } }, onRead: function(callback) { that.platform.zwayRequest({ @@ -375,8 +385,8 @@ ZWayServerAccessory.prototype = { supportEvents: false, supportBonjour: false, manfDescription: "Target Temperature", - designedMinValue: 2, - designedMaxValue: 38, + designedMinValue: vdev.metrics && vdev.metrics.min !== undefined ? vdev.metrics.min : 5, + designedMaxValue: vdev.metrics && vdev.metrics.max !== undefined ? vdev.metrics.max : 40, designedMinStep: 1, unit: "celsius" }); @@ -528,6 +538,52 @@ ZWayServerAccessory.prototype = { }); } + if (cxs.indexOf(types.BATTERY_LEVEL_CTYPE) >= 0) { + cTypes.push({ + cType: types.BATTERY_LEVEL_CTYPE, + onRead: function(callback) { + that.platform.zwayRequest({ + method: "GET", + url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id + }).then(function(result){ + callback(result.data.metrics.level); + }); + }, + perms: ["pr","ev"], + format: "int", + initialValue: 100, + supportEvents: true, + supportBonjour: false, + manfDescription: "Battery Level", + designedMinValue: 0, + designedMaxValue: 100, + designedMinStep: 1, + unit: "%" + }); + } + + if (cxs.indexOf(types.STATUS_LOW_BATTERY_CTYPE) >= 0) { + cTypes.push({ + cType: types.STATUS_LOW_BATTERY_CTYPE, + onUpdate: null, + onRead: function(callback) { + that.platform.zwayRequest({ + method: "GET", + url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id + }).then(function(result){ + callback(result.data.metrics.level <= that.platform.batteryLow ? 1 : 0); + }); + }, + perms: ["pr","ev"], + format: "bool", + initialValue: 0, + supportEvents: false, + supportBonjour: false, + manfDescription: "Battery is low", + designedMaxLength: 1 + }); + } + return cTypes; }, @@ -575,9 +631,25 @@ ZWayServerAccessory.prototype = { var hits = {}; for (var i = 0; i < cTypes.length; i++){ if(hits[cTypes[i].cType]) cTypes.splice(i--, 1); // Remember postfix means post-evaluate! - hits[cTypes[i].cType] = true; + else hits[cTypes[i].cType] = cTypes[i]; } + // Thermostats MUST include current temperature...so, for the Danfoss/Devolo radiator + // thermostats, we have to fake one... + if (hits[types.TARGET_TEMPERATURE_CTYPE] && !hits[types.CURRENT_TEMPERATURE_CTYPE]) { + // Copy the "target" device to the "current" one, with necessary tweaks... + var tcx = hits[types.TARGET_TEMPERATURE_CTYPE]; + var ccx = {}; + for(var p in tcx){ + if(tcx.hasOwnProperty(p)) ccx[p] = tcx[p]; + } + ccx.cType = types.CURRENT_TEMPERATURE_CTYPE; + ccx.onUpdate = null; + ccx.perms = ["pr"]; + //ccx.onRead = null; // Override this?? + cTypes.push(ccx); + } + services.push({ sType: sTypes[0], characteristics: cTypes From 77fbe5b53c194ef58ef250f3c5361d4c626e3816 Mon Sep 17 00:00:00 2001 From: Nick Farina Date: Mon, 24 Aug 2015 19:23:43 -0700 Subject: [PATCH 08/63] 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."); - } + } }); } } From 11f71e076cd0438823ab19a6161b4352bb3b503b Mon Sep 17 00:00:00 2001 From: Sam Edwards Date: Tue, 25 Aug 2015 10:28:08 -0700 Subject: [PATCH 09/63] Update MiLight module for new API and add improved (or some at all) handling of lamp types other than rgbw. Also remove invalid comments from json file, and support future versions of node-milight-promise --- accessories/MiLight.js | 260 +++++++++++++++-------------------------- config-sample.json | 12 +- package.json | 2 +- 3 files changed, 104 insertions(+), 170 deletions(-) diff --git a/accessories/MiLight.js b/accessories/MiLight.js index 409fcb5..dacd671 100644 --- a/accessories/MiLight.js +++ b/accessories/MiLight.js @@ -1,181 +1,115 @@ -var types = require("HAP-NodeJS/accessories/types.js"); +var Service = require("HAP-NodeJS").Service; +var Characteristic = require("HAP-NodeJS").Characteristic; var Milight = require('node-milight-promise').MilightController; var commands = require('node-milight-promise').commands; +module.exports = { + accessory: MiLight +} + function MiLight(log, config) { - this.log = log; - this.ip_address = config["ip_address"]; - this.port = config["port"]; - this.name = config["name"]; - this.zone = config["zone"]; - this.type = config["type"]; - this.delay = config["delay"]; - this.repeat = config["repeat"]; + this.log = log; + + // config info + this.ip_address = config["ip_address"]; + this.port = config["port"]; + this.name = config["name"]; + this.zone = config["zone"]; + this.type = config["type"]; + this.delay = config["delay"]; + this.repeat = config["repeat"]; } var light = new Milight({ - ip: this.ip_address, - port: this.port, - delayBetweenCommands: this.delay, - commandRepeat: this.repeat + ip: this.ip_address, + port: this.port, + delayBetweenCommands: this.delay, + commandRepeat: this.repeat }); MiLight.prototype = { - setPowerState: function(powerOn) { + setPowerState: function(powerOn, callback) { + if (powerOn) { + light.sendCommands(commands[this.type].on(this.zone)); + this.log("Setting power state to on"); + } + else { + light.sendCommands(commands[this.type].off(this.zone)); + this.log("Setting power state to off"); + } + callback(); + }, - var binaryState = powerOn ? "on" : "off"; - var that = this; + setBrightness: function(level, callback) { + this.log("Setting brightness to %s", level); - if (binaryState === "on") { - this.log("Setting power state of zone " + this.zone + " to " + powerOn); - light.sendCommands(commands[this.type].on(this.zone)); - } else { - this.log("Setting power state of zone " + this.zone + " to " + powerOn); - light.sendCommands(commands[this.type].off(this.zone)); - } + // If this is an rgbw lamp, set the absolute brightness specified + if (this.type == "rgbw") { + light.sendCommands(commands.rgbw.brightness(level)); + } else { + // If this is an rgb or a white lamp, they only support brightness up and down. + // Set brightness up when value is >50 and down otherwise. Not sure how well this works real-world. + if (level >= 50) { + light.sendCommands(commands[this.type].brightUp()); + } else { + light.sendCommands(commands[this.type].brightDown()); + } + } + callback(); + }, - }, + setHue: function(value, callback) { + this.log("Setting hue to %s", value); - setBrightnessLevel: function(value) { + if (this.type == "rgbw") { + if (value == 0) { + light.sendCommands(commands.rgbw.whiteMode(this.zone)); + } else { + light.sendCommands(commands.rgbw.hue(commands.rgbw.hsvToMilightColor(Array(value, 0, 0)))); + } + } else if (this.type == "rgb") { + light.sendCommands(commands.rgb.hue(commands.rgbw.hsvToMilightColor(Array(value, 0, 0)))); + } else if (this.type == "white") { + // Again, white lamps don't support setting an absolue colour temp, so trying to do warmer/cooler step at a time based on colour + if (value >= 180) { + light.sendCommands(commands.white.warmer()); + } else { + light.sendCommands(commands.white.cooler()); + } + } - var that = this; + }, + + identify: function(callback) { + this.log("Identify requested!"); + callback(); // success + }, + + getServices: function() { + var informationService = new Service.AccessoryInformation(); + + informationService + .setCharacteristic(Characteristic.Manufacturer, "MiLight") + .setCharacteristic(Characteristic.Model, this.type) + .setCharacteristic(Characteristic.SerialNumber, "MILIGHT12345"); + + 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)); - this.log("Setting brightness level of zone " + this.zone + " to " + value); - - light.sendCommands(commands[this.type].brightness(value)); - }, - - setHue: function(value) { - - var that = this; - - this.log("Setting hue of zone " + this.zone + " to " + value); - - if (value == "0") { - light.sendCommands(commands.rgbw.whiteMode(this.zone)); - } else { - light.sendCommands(commands.rgbw.hue(commands.rgbw.hsvToMilightColor(Array(value, 0, 0)))); - } - }, - - - getServices: function() { - var that = this; - var services = [{ - 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: "MiLight", - supportEvents: false, - supportBonjour: false, - manfDescription: "Manufacturer", - designedMaxLength: 255 - }, { - cType: types.MODEL_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: this.type, - supportEvents: false, - supportBonjour: false, - manfDescription: "Model", - designedMaxLength: 255 - }, { - cType: types.SERIAL_NUMBER_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: "MILIGHT1234", - 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: false, - 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: false, - supportEvents: false, - supportBonjour: false, - manfDescription: "Turn on the light", - designedMaxLength: 1 - }, { - cType: types.BRIGHTNESS_CTYPE, - onUpdate: function(value) { - that.setBrightnessLevel(value); - }, - perms: ["pw", "pr", "ev"], - format: "bool", - initialValue: 100, - supportEvents: false, - supportBonjour: false, - manfDescription: "Adjust brightness of light", - designedMinValue: 0, - designedMaxValue: 100, - designedMinStep: 1, - unit: "%" - }] - }]; - if (that.type == "rgbw" || that.type == "rgb") { - services[1].characteristics.push({ - cType: types.HUE_CTYPE, - onUpdate: function(value) { - that.setHue(value); - }, - perms: ["pw", "pr", "ev"], - format: "int", - initialValue: 0, - supportEvents: false, - supportBonjour: false, - manfDescription: "Adjust Hue of Light", - designedMinValue: 0, - designedMaxValue: 360, - designedMinStep: 1, - unit: "arcdegrees" - }); - } - return services; - } + if (this.type == "rgbw" || this.type == "rgb") { + lightbulbService + .addCharacteristic(new Characteristic.Hue()) + .on('set', this.setHue.bind(this)); + } + + return [informationService, lightbulbService]; + } }; - -module.exports.accessory = MiLight; diff --git a/config-sample.json b/config-sample.json index adfe40c..ccad7dc 100644 --- a/config-sample.json +++ b/config-sample.json @@ -153,12 +153,12 @@ { "accessory":"MiLight", "name": "Lamp", - "ip_address": "255.255.255.255", // IP Address of the WiFi Bridge, or 255.255.255.255 to broadcast to all - "port": 8899, // Default port 8899 (50000 for v1 or v2 bridge) - "zone": 1, // Zone to address commands to (not used for rgb only bulbs) - "type": "rgbw", // Bulb type (rgbw, rgb, white) - "delay": 35, // Delay between commands sent to the WiFi bridge (default 35) - "repeat": 3 // Number of times each command is repeated for reliability (default 3) + "ip_address": "255.255.255.255", + "port": 8899, + "zone": 1, + "type": "rgbw", + "delay": 35, + "repeat": 3 }, { "accessory": "Tesla", diff --git a/package.json b/package.json index 5604b0f..ac28c5f 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "harmonyhubjs-discover": "git+https://github.com/swissmanu/harmonyhubjs-discover.git", "mdns": "^2.2.4", "node-hue-api": "^1.0.5", - "node-milight-promise": "0.0.2", + "node-milight-promise": "0.0.x", "node-persist": "0.0.x", "request": "2.49.x", "sonos": "0.8.x", From f81e594ff0a8013dd7dd343f27bee73baf7d44f9 Mon Sep 17 00:00:00 2001 From: Sam Edwards Date: Tue, 25 Aug 2015 10:34:45 -0700 Subject: [PATCH 10/63] Update readme with reference to milight under lights section --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d75b54e..4fae78b 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Since Siri supports devices added through HomeKit, this means that with Homebrid * _Siri, turn off the Speakers._ ([Sonos](http://www.sonos.com)) * _Siri, turn on the Dehumidifier._ ([WeMo](http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/)) * _Siri, turn on Away Mode._ ([Xfinity Home](http://www.comcast.com/home-security.html)) - * _Siri, turn on the living room lights._ ([Wink](http://www.wink.com), [SmartThings](http://www.smartthings.com), [X10](http://github.com/edc1591/rest-mochad), [Philips Hue](http://meethue.com)) + * _Siri, turn on the living room lights._ ([Wink](http://www.wink.com), [SmartThings](http://www.smartthings.com), [X10](http://github.com/edc1591/rest-mochad), [Philips Hue](http://meethue.com), [LimitlessLED/MiLight/Easybulb](http://www.limitlessled.com/)) * _Siri, set the movie scene._ ([Logitech Harmony](http://myharmony.com/)) If you would like to support any other devices, please write a shim and create a pull request and I'd be happy to add it to this official list. From 7d5caae96d13fae4ed244536ffece8f8024791a4 Mon Sep 17 00:00:00 2001 From: Sam Edwards Date: Tue, 25 Aug 2015 10:46:57 -0700 Subject: [PATCH 11/63] Add the hue characteristics for all lamps, as we're trying to handle this in some way now --- accessories/MiLight.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/accessories/MiLight.js b/accessories/MiLight.js index dacd671..9ab18f6 100644 --- a/accessories/MiLight.js +++ b/accessories/MiLight.js @@ -104,11 +104,9 @@ MiLight.prototype = { .addCharacteristic(new Characteristic.Brightness()) .on('set', this.setBrightness.bind(this)); - if (this.type == "rgbw" || this.type == "rgb") { - lightbulbService - .addCharacteristic(new Characteristic.Hue()) - .on('set', this.setHue.bind(this)); - } + lightbulbService + .addCharacteristic(new Characteristic.Hue()) + .on('set', this.setHue.bind(this)); return [informationService, lightbulbService]; } From 1050b4e550a8c94806e1edf8e1a487e974e4e5fe Mon Sep 17 00:00:00 2001 From: Sam Edwards Date: Tue, 25 Aug 2015 12:12:58 -0700 Subject: [PATCH 12/63] Add a name to the accessory as well --- accessories/MiLight.js | 1 + 1 file changed, 1 insertion(+) diff --git a/accessories/MiLight.js b/accessories/MiLight.js index 9ab18f6..ffcf774 100644 --- a/accessories/MiLight.js +++ b/accessories/MiLight.js @@ -93,6 +93,7 @@ MiLight.prototype = { .setCharacteristic(Characteristic.Manufacturer, "MiLight") .setCharacteristic(Characteristic.Model, this.type) .setCharacteristic(Characteristic.SerialNumber, "MILIGHT12345"); + .setCharacteristic(Characteristic.Name, this.name); var lightbulbService = new Service.Lightbulb(); From d7d80b8618193b3264e0c9c0cd5592679120e6a3 Mon Sep 17 00:00:00 2001 From: Sam Edwards Date: Tue, 25 Aug 2015 17:46:28 -0700 Subject: [PATCH 13/63] Don't actually add a name characteristic here --- accessories/HomeMatic.js | 0 accessories/MiLight.js | 1 - 2 files changed, 1 deletion(-) mode change 100755 => 100644 accessories/HomeMatic.js diff --git a/accessories/HomeMatic.js b/accessories/HomeMatic.js old mode 100755 new mode 100644 diff --git a/accessories/MiLight.js b/accessories/MiLight.js index ffcf774..9ab18f6 100644 --- a/accessories/MiLight.js +++ b/accessories/MiLight.js @@ -93,7 +93,6 @@ MiLight.prototype = { .setCharacteristic(Characteristic.Manufacturer, "MiLight") .setCharacteristic(Characteristic.Model, this.type) .setCharacteristic(Characteristic.SerialNumber, "MILIGHT12345"); - .setCharacteristic(Characteristic.Name, this.name); var lightbulbService = new Service.Lightbulb(); From b170e81059bf17721ee09793c957d753b3517763 Mon Sep 17 00:00:00 2001 From: EddyK69 Date: Wed, 26 Aug 2015 23:20:42 +0200 Subject: [PATCH 14/63] Implemented: config.json parameter loadscenes & fixed dimmer-bug Added parameter in config.json: 'loadscenes' for enabling/disabling loading scenes Fixed issue with dimmer-range; was 0-100, should be 0-16 --- config-sample.json | 4 +++- platforms/Domoticz.js | 51 ++++++++++++++++++++++++++++--------------- 2 files changed, 36 insertions(+), 19 deletions(-) diff --git a/config-sample.json b/config-sample.json index ccad7dc..c9ba829 100644 --- a/config-sample.json +++ b/config-sample.json @@ -41,7 +41,9 @@ "platform": "Domoticz", "name": "Domoticz", "server": "127.0.0.1", - "port": "8005" + "port": "8080", + "roomid": 0, + "loadscenes": 1 }, { "platform": "PhilipsHue", diff --git a/platforms/Domoticz.js b/platforms/Domoticz.js index d80e6d6..5bb6910 100644 --- a/platforms/Domoticz.js +++ b/platforms/Domoticz.js @@ -8,6 +8,11 @@ // - Added support for Scenes // - Sorting device names // +// 26 August 2015 [EddyK69] +// - Added parameter in config.json: 'loadscenes' for enabling/disabling loading scenes +// - Fixed issue with dimmer-range; was 0-100, should be 0-16 +// +// // Domoticz JSON API required // https://www.domoticz.com/wiki/Domoticz_API/JSON_URL's#Lights_and_switches // @@ -18,7 +23,8 @@ // "name": "Domoticz", // "server": "127.0.0.1", // "port": "8080", -// "roomid": 123 (0=no roomplan) +// "roomid": 123, (0=no roomplan) +// "loadscenes": 1 (0=disable scenes) // } // ], // @@ -47,6 +53,10 @@ function DomoticzPlatform(log, config){ if (typeof config["roomid"] != 'undefined') { this.roomid = config["roomid"]; } + this.loadscenes = 1; + if (typeof config["loadscenes"] != 'undefined') { + this.loadscenes = config["loadscenes"]; + } } function sortByKey(array, key) { @@ -120,24 +130,26 @@ DomoticzPlatform.prototype = { }); } //Get Scenes - asyncCalls++; - request.get({ - url: this.urlForQuery("type=scenes"), - 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); - }) + if (this.loadscenes == 1) { + asyncCalls++; + request.get({ + url: this.urlForQuery("type=scenes"), + 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); + }) + } + callbackLater(); + } else { + that.log("There was a problem connecting to Domoticz."); } - callbackLater(); - } else { - that.log("There was a problem connecting to Domoticz."); - } - }); + }); + } } } @@ -165,6 +177,9 @@ DomoticzAccessory.prototype = { url = this.platform.urlForQuery("type=command¶m=setcolbrightnessvalue&idx=" + this.idx + "&hue=" + value + "&brightness=100" + "&iswhite=false"); } else if (c == "setLevel") { + //Range should be 0-16 instead of 0-100 + //See http://www.domoticz.com/wiki/Domoticz_API/JSON_URL%27s#Set_a_dimmable_light_to_a_certain_level + value = Math.round((value / 100) * 16) url = this.platform.urlForQuery("type=command¶m=switchlight&idx=" + this.idx + "&switchcmd=Set%20Level&level=" + value); } else if (value != undefined) { From 76eaca8f78d33d610322b3d52314343ff497e8f1 Mon Sep 17 00:00:00 2001 From: EddyK69 Date: Thu, 27 Aug 2015 12:49:22 +0200 Subject: [PATCH 15/63] Fixed misc. dimmer issues Fixed issue that 'on/off'-type lights showed as dimmers in HomeKit. Checking now on SwitchType instead of HaveDimmer Fixed issue that 'on-off'-type lights would not react on Siri 'Switch on/off light'; On/Off types are now handled as Lights instead of Switches --- platforms/Domoticz.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/platforms/Domoticz.js b/platforms/Domoticz.js index 5bb6910..de5b466 100644 --- a/platforms/Domoticz.js +++ b/platforms/Domoticz.js @@ -12,6 +12,10 @@ // - Added parameter in config.json: 'loadscenes' for enabling/disabling loading scenes // - Fixed issue with dimmer-range; was 0-100, should be 0-16 // +// 27 August 2015 [EddyK69] +// - Fixed issue that 'on/off'-type lights showed as dimmers in HomeKit. Checking now on SwitchType instead of HaveDimmer +// - Fixed issue that 'on-off'-type lights would not react on Siri 'Switch on/off light'; On/Off types are now handled as Lights instead of Switches +// (Cannot determine if 'on/off'-type device is a Light or a Switch :( ) // // Domoticz JSON API required // https://www.domoticz.com/wiki/Domoticz_API/JSON_URL's#Lights_and_switches @@ -95,7 +99,8 @@ DomoticzPlatform.prototype = { if (json['result'] != undefined) { var sArray=sortByKey(json['result'],"Name"); sArray.map(function(s) { - accessory = new DomoticzAccessory(that.log, that, false, s.idx, s.Name, s.HaveDimmer, s.MaxDimLevel, (s.SubType=="RGB")||(s.SubType=="RGBW")); + var havedimmer = (s.SwitchType == 'Dimmer') + accessory = new DomoticzAccessory(that.log, that, false, s.idx, s.Name, havedimmer, s.MaxDimLevel, (s.SubType=="RGB")||(s.SubType=="RGBW")); foundAccessories.push(accessory); }) } @@ -118,7 +123,8 @@ DomoticzPlatform.prototype = { sArray.map(function(s) { //only accept switches for now if (typeof s.SwitchType != 'undefined') { - accessory = new DomoticzAccessory(that.log, that, false, s.idx, s.Name, s.HaveDimmer, s.MaxDimLevel, (s.SubType=="RGB")||(s.SubType=="RGBW")); + var havedimmer = (s.SwitchType == 'Dimmer') + accessory = new DomoticzAccessory(that.log, that, false, s.idx, s.Name, havedimmer, s.MaxDimLevel, (s.SubType=="RGB")||(s.SubType=="RGBW")); foundAccessories.push(accessory); } }) @@ -333,11 +339,11 @@ DomoticzAccessory.prototype = { }, sType: function() { - if (this.HaveDimmer == true) { + //if (this.HaveDimmer == true) { return types.LIGHTBULB_STYPE - } else { - return types.SWITCH_STYPE - } + //} else { + // return types.SWITCH_STYPE + //} }, getServices: function() { From bf2216209d01a5164778c7c8f38c1b0255cf975b Mon Sep 17 00:00:00 2001 From: Khaos Tian Date: Fri, 28 Aug 2015 21:49:16 -0700 Subject: [PATCH 16/63] [Philips Hue] retry if command failed due to api rate limit. --- platforms/PhilipsHue.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/platforms/PhilipsHue.js b/platforms/PhilipsHue.js index 0988b06..9bceaf0 100644 --- a/platforms/PhilipsHue.js +++ b/platforms/PhilipsHue.js @@ -216,7 +216,13 @@ PhilipsHueAccessory.prototype = { that.log(device.name + ", characteristic: " + characteristic + ", value: " + value + "."); } else { - that.log(err); + if (err.code == "ECONNRESET") { + setTimeout(function() { + that.executeChange(api, device, characteristic, value); + }, 300); + } else { + that.log(err); + } } }); }, From 2c50d76cb2cb2799eb3a5ba83b4754d37d81f4a3 Mon Sep 17 00:00:00 2001 From: Nick Farina Date: Sat, 29 Aug 2015 09:34:56 -0700 Subject: [PATCH 17/63] Bump HAP-NodeJS --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ac28c5f..7ccb806 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#46ba0597eb339983a14d98c53764a58a5516fcd2", + "hap-nodejs": "git+https://github.com/KhaosT/HAP-NodeJS#367c598b748abf2f6d5c6185fc38a8843bc6de70", "harmonyhubjs-client": "^1.1.4", "harmonyhubjs-discover": "git+https://github.com/swissmanu/harmonyhubjs-discover.git", "mdns": "^2.2.4", From d067711974975d6d342868d886216201df11a9df Mon Sep 17 00:00:00 2001 From: Nick Farina Date: Sat, 29 Aug 2015 13:14:55 -0700 Subject: [PATCH 18/63] HAP-NodeJS bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7ccb806..ab5b117 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#367c598b748abf2f6d5c6185fc38a8843bc6de70", + "hap-nodejs": "git+https://github.com/KhaosT/HAP-NodeJS#fff863d7a387636fc612cf27cb859e82d9ee3294", "harmonyhubjs-client": "^1.1.4", "harmonyhubjs-discover": "git+https://github.com/swissmanu/harmonyhubjs-discover.git", "mdns": "^2.2.4", From b56d9346c8c3ddd725ce9b5fcda8340b3784077f Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Sun, 30 Aug 2015 14:24:43 +0200 Subject: [PATCH 19/63] Refactored for new API. Mostly working, but door sensors need further work, battery service still isn't right, and I'm losing the bridge periodically...merging from master to see if I need some bugfixes. --- platforms/ZWayServer.js | 393 ++++++++++++++++++++++++++++++++-------- 1 file changed, 320 insertions(+), 73 deletions(-) diff --git a/platforms/ZWayServer.js b/platforms/ZWayServer.js index 6053632..93ade7b 100644 --- a/platforms/ZWayServer.js +++ b/platforms/ZWayServer.js @@ -1,3 +1,5 @@ +var Service = require("HAP-NodeJS").Service; +var Characteristic = require("HAP-NodeJS").Characteristic; var types = require("HAP-NodeJS/accessories/types.js"); var request = require("request"); var tough = require('tough-cookie'); @@ -79,6 +81,7 @@ ZWayServerPlatform.getVDevServiceTypes = function(vdev){ } } +/* ZWayServerPlatform.getVDevCharacteristicsTypes = function(vdev){ var typeKey = ZWayServerPlatform.getVDevTypeKey(vdev); switch (typeKey) { @@ -96,6 +99,7 @@ ZWayServerPlatform.getVDevCharacteristicsTypes = function(vdev){ return [types.BATTERY_LEVEL_CTYPE, types.STATUS_LOW_BATTERY_CTYPE]; } } +*/ ZWayServerPlatform.prototype = { @@ -108,7 +112,7 @@ ZWayServerPlatform.prototype = { opts.headers = { "Cookie": "ZWAYSession=" + this.sessionId }; -//opts.proxy = 'http://localhost:8888'; +opts.proxy = 'http://localhost:8888'; request(opts, function(error, response, body){ if(response.statusCode == 401){ @@ -116,7 +120,7 @@ ZWayServerPlatform.prototype = { request({ method: "POST", url: that.url + 'ZAutomation/api/v1/login', -//proxy: 'http://localhost:8888', +proxy: 'http://localhost:8888', body: { //JSON.stringify({ "form": true, "login": that.login, @@ -235,6 +239,7 @@ ZWayServerAccessory.prototype = { }); }, + /* informationCharacteristics: function() { return [ { @@ -272,7 +277,7 @@ ZWayServerAccessory.prototype = { onUpdate: null, perms: ["pr"], format: "string", - initialValue: "VDev-" + this.devDesc.devices[this.devDesc.primary].h, //TODO: Is this valid? + initialValue: "", supportEvents: false, supportBonjour: false, manfDescription: "SN", @@ -409,14 +414,14 @@ ZWayServerAccessory.prototype = { cTypes.push({ cType: types.CURRENTHEATINGCOOLING_CTYPE, //TODO: Support multifunction thermostats...only heating supported now. - /* + / * onUpdate: null, onRead: function(callback) { that.getCurrentHeatingCooling(function(currentHeatingCooling){ callback(currentHeatingCooling); }); }, - */ + * / perms: ["pr"], format: "int", initialValue: 1, @@ -434,7 +439,7 @@ ZWayServerAccessory.prototype = { cTypes.push({ cType: types.TARGETHEATINGCOOLING_CTYPE, //TODO: Support multifunction thermostats...only heating supported now. - /* + / * onUpdate: function(value) { that.setTargetHeatingCooling(value); }, @@ -443,7 +448,7 @@ ZWayServerAccessory.prototype = { callback(targetHeatingCooling); }); }, - */ + * / perms: ["pr"], format: "int", initialValue: 0, @@ -550,7 +555,7 @@ ZWayServerAccessory.prototype = { }); }, perms: ["pr","ev"], - format: "int", + format: "uint8", initialValue: 100, supportEvents: true, supportBonjour: false, @@ -575,7 +580,7 @@ ZWayServerAccessory.prototype = { }); }, perms: ["pr","ev"], - format: "bool", + format: "uint8", initialValue: 0, supportEvents: false, supportBonjour: false, @@ -586,75 +591,317 @@ ZWayServerAccessory.prototype = { return cTypes; }, + */ + + getVDevServices: function(vdev){ + var typeKey = ZWayServerPlatform.getVDevTypeKey(vdev); + var services = [], service; + switch (typeKey) { + case "switchBinary": + services.push(new Service.Switch(vdev.metrics.title)); + break; + case "switchMultilevel": + services.push(new Service.Lightbulb(vdev.metrics.title)); + break; + case "thermostat": + services.push(new Service.Thermostat(vdev.metrics.title)); + break; + case "sensorMultilevel.Temperature": + services.push(new Service.TemperatureSensor(vdev.metrics.title)); + break; + case "sensorBinary.Door/Window": + services.push(new Service.Door(vdev.metrics.title)); + break; + case "battery.Battery": + services.push(new Service.BatteryService(vdev.metrics.title)); + break; + } + + var validServices =[]; + for(var i = 0; i < services.length; i++){ + if(this.configureService(services[i], vdev)) + validServices.push(services[i]); + } + + return validServices; + } + , + uuidToTypeKeyMap: null + , + getVDevForCharacteristic: function(cx, vdevPreferred){ + var map = this.uuidToTypeKeyMap; + if(!map){ + this.uuidToTypeKeyMap = map = {}; + map[(new Characteristic.On).UUID] = ["switchBinary","switchMultilevel"]; + map[(new Characteristic.Brightness).UUID] = ["switchMultilevel"]; + map[(new Characteristic.CurrentTemperature).UUID] = ["sensorMultilevel.Temperature","thermostat"]; + map[(new Characteristic.TargetTemperature).UUID] = ["thermostat"]; + map[(new Characteristic.TemperatureDisplayUnits).UUID] = ["sensorMultilevel.Temperature","thermostat"]; //TODO: Always a fixed result + map[(new Characteristic.CurrentHeatingCoolingState).UUID] = ["thermostat"]; //TODO: Always a fixed result + map[(new Characteristic.TargetHeatingCoolingState).UUID] = ["thermostat"]; //TODO: Always a fixed result + map[(new Characteristic.CurrentDoorState).UUID] = ["sensorBinary.Door/Window","sensorBinary"]; + map[(new Characteristic.TargetDoorState).UUID] = ["sensorBinary.Door/Window","sensorBinary"]; //TODO: Always a fixed result + map[(new Characteristic.ObstructionDetected).UUID] = ["sensorBinary.Door/Window","sensorBinary"]; //TODO: Always a fixed result + map[(new Characteristic.BatteryLevel).UUID] = ["battery.Battery"]; + map[(new Characteristic.StatusLowBattery).UUID] = ["battery.Battery"]; + map[(new Characteristic.ChargingState).UUID] = ["battery.Battery"]; //TODO: Always a fixed result + } + + var typekeys = map[cx.UUID]; + if(typekeys === undefined) return null; + + if(vdevPreferred && typekeys.indexOf(ZWayServerPlatform.getVDevTypeKey(vdevPreferred)) >= 0){ + return vdevPreferred; + } + + var candidates = this.devDesc.devices; + for(var i = 0; i < typekeys.length; i++){ + for(var j = 0; j < candidates.length; j++){ + if(ZWayServerPlatform.getVDevTypeKey(candidates[j]) === typekeys[i]) return candidates[j]; + } + } + + return null; + } + , + configureCharacteristic: function(cx, vdev){ + var that = this; + var gdv = function(){ + that.log("Default value for " + vdev.metrics.title + " is " + vdev.metrics.level); + return vdev.metrics.level; + }; + + if(cx instanceof Characteristic.On){ + cx.getDefaultValue = gdv; + cx.on('get', function(callback, context){ + that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + that.platform.zwayRequest({ + method: "GET", + url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id + }).then(function(result){ + that.log("Got value: " + result.data.metrics.level + ", for " + vdev.metrics.title + "."); + var val; + if(result.data.metrics.level === "off"){ + val = false; + } else if(val <= 5) { + val = false; + } else if (val > 5) { + val = true; + } + callback(false, val); + }); + }.bind(this)); + cx.on('set', function(powerOn, callback){ + this.command(vdev, powerOn ? "on" : "off").then(function(result){ + callback(); + }); + }.bind(this)); + return cx; + } + + if(cx instanceof Characteristic.Brightness){ + cx.getDefaultValue = gdv; + cx.on('get', function(callback, context){ + that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + that.platform.zwayRequest({ + method: "GET", + url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id + }).then(function(result){ + that.log("Got value " + result.data.metrics.level + " for " + vdev.metrics.title + "."); + callback(false, result.data.metrics.level); + }); + }.bind(this)); + cx.on('set', function(level, callback){ + this.command(vdev, "exact", {level: parseInt(level, 10)}).then(function(result){ + callback(); + }); + }.bind(this)); + return cx; + } + + if(cx instanceof Characteristic.CurrentTemperature){ + cx.getDefaultValue = gdv; + cx.on('get', function(callback, context){ + that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + that.platform.zwayRequest({ + method: "GET", + url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id + }).then(function(result){ + callback(false, result.data.metrics.level); + }); + }.bind(this)); + cx.minimumValue = vdev.metrics && vdev.metrics.min !== undefined ? vdev.metrics.min : -40; + cx.maximumValue = vdev.metrics && vdev.metrics.max !== undefined ? vdev.metrics.max : 999; + return cx; + } + + if(cx instanceof Characteristic.TargetTemperature){ + cx.getDefaultValue = gdv; + cx.on('get', function(callback, context){ + that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + this.platform.zwayRequest({ + method: "GET", + url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id + }).then(function(result){ + callback(false, result.data.metrics.level); + }); + }.bind(this)); + cx.on('set', function(level, callback){ + this.command(vdev, "exact", {level: parseInt(level, 10)}).then(function(result){ + callback(); + }); + }.bind(this)); + cx.minimumValue = vdev.metrics && vdev.metrics.min !== undefined ? vdev.metrics.min : 5; + cx.maximumValue = vdev.metrics && vdev.metrics.max !== undefined ? vdev.metrics.max : 40; + return cx; + } + + if(cx instanceof Characteristic.TemperatureDisplayUnits){ + //TODO: Always in °C for now. + cx.getDefaultValue = function(){ return Characteristic.TemperatureDisplayUnits.CELCIUS; }; + cx.on('get', function(callback, context){ + that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + callback(false, Characteristic.TemperatureDisplayUnits.CELCIUS); + }); + cx.writable = false; + return cx; + } + + if(cx instanceof Characteristic.CurrentHeatingCoolingState){ + //TODO: Always HEAT for now, we don't have an example to work with that supports another function. + cx.getDefaultValue = function(){ return Characteristic.CurrentHeatingCoolingState.HEAT; }; + cx.on('get', function(callback, context){ + that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + callback(false, Characteristic.CurrentHeatingCoolingState.HEAT); + }); + return cx; + } + + if(cx instanceof Characteristic.TargetHeatingCoolingState){ + //TODO: Always HEAT for now, we don't have an example to work with that supports another function. + cx.getDefaultValue = function(){ return Characteristic.TargetHeatingCoolingState.HEAT; }; + cx.on('get', function(callback, context){ + that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + callback(false, Characteristic.TargetHeatingCoolingState.HEAT); + }); + cx.writable = false; + return cx; + } + + if(cx instanceof Characteristic.CurrentDoorState){ + cx.getDefaultValue = function(){ + return vdev.metrics.level == "off" ? Characteristic.CurrentDoorState.CLOSED : Characteristic.CurrentDoorState.OPEN; + }; + cx.on('get', function(callback, context){ + that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + this.platform.zwayRequest({ + method: "GET", + url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id + }).then(function(result){ + callback(false, result.data.metrics.level == "off" ? Characteristic.CurrentDoorState.CLOSED : Characteristic.CurrentDoorState.OPEN); + }); + }.bind(this)); + } + + if(cx instanceof Characteristic.TargetDoorState){ + //TODO: We only support this for Door sensors now, so it's a fixed value. + cx.getDefaultValue = function(){ return Characteristic.TargetDoorState.CLOSED; }; + cx.on('get', function(callback, context){ + that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + callback(false, Characteristic.TargetDoorState.CLOSED); + }); + //cx.readable = false; + cx.writable = false; + } + + if(cx instanceof Characteristic.ObstructionDetected){ + //TODO: We only support this for Door sensors now, so it's a fixed value. + cx.getDefaultValue = function(){ return false; }; + cx.on('get', function(callback, context){ + that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + callback(false, false); + }); + //cx.readable = false; + cx.writable = false; + } + + if(cx instanceof Characteristic.BatteryLevel){ + cx.getDefaultValue = gdv; + cx.on('get', function(callback, context){ + that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + that.platform.zwayRequest({ + method: "GET", + url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id + }).then(function(result){ + callback(false, result.data.metrics.level); + }); + }.bind(this)); + } + + if(cx instanceof Characteristic.StatusLowBattery){ + cx.getDefaultValue = function(){ return Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; }; + cx.on('get', function(callback, context){ + that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + that.platform.zwayRequest({ + method: "GET", + url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id + }).then(function(result){ + callback(false, result.data.metrics.level <= that.platform.batteryLow ? Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL); + }); + }.bind(this)); + } + + if(cx instanceof Characteristic.ChargingState){ + //TODO: No known chargeable devices(?), so always return false. + cx.getDefaultValue = function(){ return Characteristic.ChargingState.NOT_CHARGING; }; + cx.on('get', function(callback, context){ + that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + callback(false, Characteristic.ChargingState.NOT_CHARGING); + }); + //cx.readable = false; + cx.writable = false; + } + + } + , + configureService: function(service, vdev){ + var success = true; + for(var i = 0; i < service.characteristics.length; i++){ + var cx = service.characteristics[i]; + var vdev = this.getVDevForCharacteristic(cx, vdev); + if(!vdev){ + success = false; + this.log("ERROR! Failed to configure required characteristic \"" + service.characteristics[i].displayName + "\"!"); + } + cx = this.configureCharacteristic(cx, vdev); + } + for(var i = 0; i < service.optionalCharacteristics.length; i++){ + var cx = service.optionalCharacteristics[i]; + var vdev = this.getVDevForCharacteristic(cx); + if(!vdev) continue; + cx = this.configureCharacteristic(cx, vdev); + } + return success; + } + , getServices: function() { var that = this; - var services = [{ - sType: types.ACCESSORY_INFORMATION_STYPE, - characteristics: this.informationCharacteristics(), - }]; + + var informationService = new Service.AccessoryInformation(); - // rearrange the array so the primary is first - var vdevs = this.devDesc.devices.concat(); - var p = vdevs.splice(this.devDesc.primary, 1)[0]; - vdevs.unshift(p); - /* - for(var i = 0; i < vdevs.length; i++){ - var sTypes = ZWayServerPlatform.getVDevServiceTypes(vdevs[i]); - if(!sTypes) continue; - for(var j = 0; j < sTypes.length; j++){ - services.push({ - sType: sTypes[j], - characteristics: this.controlCharacteristics(vdevs[i]) - }); - } - } - */ - - var sTypes = ZWayServerPlatform.getVDevServiceTypes(vdevs[0]); - var cTypes = [{ - cType: types.NAME_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: this.name, - supportEvents: true, - supportBonjour: false, - manfDescription: "Name of service", - designedMaxLength: 255 - }]; - if(sTypes) for(var i = 0; i < vdevs.length; i++){ - cTypes = cTypes.concat(this.controlCharacteristics(vdevs[i])); - } - - // Scrub/eliminate duplicate cTypes? This is a lot of guesswork ATM... - var hits = {}; - for (var i = 0; i < cTypes.length; i++){ - if(hits[cTypes[i].cType]) cTypes.splice(i--, 1); // Remember postfix means post-evaluate! - else hits[cTypes[i].cType] = cTypes[i]; - } - - // Thermostats MUST include current temperature...so, for the Danfoss/Devolo radiator - // thermostats, we have to fake one... - if (hits[types.TARGET_TEMPERATURE_CTYPE] && !hits[types.CURRENT_TEMPERATURE_CTYPE]) { - // Copy the "target" device to the "current" one, with necessary tweaks... - var tcx = hits[types.TARGET_TEMPERATURE_CTYPE]; - var ccx = {}; - for(var p in tcx){ - if(tcx.hasOwnProperty(p)) ccx[p] = tcx[p]; - } - ccx.cType = types.CURRENT_TEMPERATURE_CTYPE; - ccx.onUpdate = null; - ccx.perms = ["pr"]; - //ccx.onRead = null; // Override this?? - cTypes.push(ccx); - } + informationService + .setCharacteristic(Characteristic.Name, this.name) + .setCharacteristic(Characteristic.Manufacturer, "Z-Wave.me") + .setCharacteristic(Characteristic.Model, "Virtual Device (VDev version 1)") + .setCharacteristic(Characteristic.SerialNumber, "VDev-" + this.devDesc.devices[this.devDesc.primary].h) //FIXME: Is this valid?); - services.push({ - sType: sTypes[0], - characteristics: cTypes - }); - //... + var services = [informationService]; + + services = services.concat(this.getVDevServices(this.devDesc.devices[this.devDesc.primary])); + + if(this.devDesc.types["battery.Battery"]) + services = services.concat(this.getVDevServices(this.devDesc.devices[this.devDesc.types["battery.Battery"]])); this.log("Loaded services for " + this.name); return services; From 47f000ecff978c7301441114fdaafad3ad143340 Mon Sep 17 00:00:00 2001 From: Sam Edwards Date: Mon, 31 Aug 2015 15:40:02 -0700 Subject: [PATCH 20/63] [MiLight] Sent on command before brightness and hue to ensure we're targeting the right lamp, and implement night mode at brightness <= 2, and implement maximum brightness command for white lamps --- accessories/MiLight.js | 59 ++++++++++++++++++++++++++++-------------- 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/accessories/MiLight.js b/accessories/MiLight.js index 9ab18f6..85b09d6 100644 --- a/accessories/MiLight.js +++ b/accessories/MiLight.js @@ -29,12 +29,11 @@ var light = new Milight({ MiLight.prototype = { - setPowerState: function(powerOn, callback) { + setPowerState: function(powerOn, callback) { if (powerOn) { light.sendCommands(commands[this.type].on(this.zone)); this.log("Setting power state to on"); - } - else { + } else { light.sendCommands(commands[this.type].off(this.zone)); this.log("Setting power state to off"); } @@ -42,18 +41,35 @@ MiLight.prototype = { }, setBrightness: function(level, callback) { - this.log("Setting brightness to %s", level); + if (level <= 2 && (this.type == "rgbw" || this.type == "white")) { + // If setting brightness to 2 or lower, instead set night mode for lamps that support it + this.log("Setting night mode", level); - // If this is an rgbw lamp, set the absolute brightness specified - if (this.type == "rgbw") { - light.sendCommands(commands.rgbw.brightness(level)); + light.sendCommands(commands[this.type].off(this.zone)); + // Not sure if this timing is going to work or not? It's supposed to be 100ms after the off command + light.sendCommands(commands[this.type].nightMode(this.zone)); } else { - // If this is an rgb or a white lamp, they only support brightness up and down. - // Set brightness up when value is >50 and down otherwise. Not sure how well this works real-world. - if (level >= 50) { - light.sendCommands(commands[this.type].brightUp()); + this.log("Setting brightness to %s", level); + + // Send on command to ensure we're addressing the right bulb + light.sendCommands(commands[this.type].on(this.zone)); + + // If this is an rgbw lamp, set the absolute brightness specified + if (this.type == "rgbw") { + light.sendCommands(commands.rgbw.brightness(level)); } else { - light.sendCommands(commands[this.type].brightDown()); + // If this is an rgb or a white lamp, they only support brightness up and down. + // Set brightness up when value is >50 and down otherwise. Not sure how well this works real-world. + if (level >= 50) { + if (this.type == "white" && level == 100) { + // But the white lamps do have a "maximum brightness" command + light.sendCommands(commands.white.maxBright(this.zone)); + } else { + light.sendCommands(commands[this.type].brightUp()); + } + } else { + light.sendCommands(commands[this.type].brightDown()); + } } } callback(); @@ -62,6 +78,9 @@ MiLight.prototype = { setHue: function(value, callback) { this.log("Setting hue to %s", value); + // Send on command to ensure we're addressing the right bulb + light.sendCommands(commands[this.type].on(this.zone)); + if (this.type == "rgbw") { if (value == 0) { light.sendCommands(commands.rgbw.whiteMode(this.zone)); @@ -69,7 +88,7 @@ MiLight.prototype = { light.sendCommands(commands.rgbw.hue(commands.rgbw.hsvToMilightColor(Array(value, 0, 0)))); } } else if (this.type == "rgb") { - light.sendCommands(commands.rgb.hue(commands.rgbw.hsvToMilightColor(Array(value, 0, 0)))); + light.sendCommands(commands.rgb.hue(commands.rgbw.hsvToMilightColor(Array(value, 0, 0)))); } else if (this.type == "white") { // Again, white lamps don't support setting an absolue colour temp, so trying to do warmer/cooler step at a time based on colour if (value >= 180) { @@ -80,26 +99,26 @@ MiLight.prototype = { } }, - + identify: function(callback) { this.log("Identify requested!"); callback(); // success }, - + getServices: function() { var informationService = new Service.AccessoryInformation(); - + informationService .setCharacteristic(Characteristic.Manufacturer, "MiLight") .setCharacteristic(Characteristic.Model, this.type) .setCharacteristic(Characteristic.SerialNumber, "MILIGHT12345"); - + 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)); @@ -107,7 +126,7 @@ MiLight.prototype = { lightbulbService .addCharacteristic(new Characteristic.Hue()) .on('set', this.setHue.bind(this)); - + return [informationService, lightbulbService]; } }; From 01d2c21aac1045d789a6fc3e7d23e02dfc2eed28 Mon Sep 17 00:00:00 2001 From: Nick Farina Date: Sat, 29 Aug 2015 22:28:52 -0700 Subject: [PATCH 21/63] New iControl accessory, supports Xfinity Home. - No more mysterious "dsig" param --- accessories/XfinityHome.js | 284 ------------------------------------- accessories/iControl.js | 130 +++++++++++++++++ config-sample.json | 10 +- package.json | 5 +- 4 files changed, 138 insertions(+), 291 deletions(-) delete mode 100644 accessories/XfinityHome.js create mode 100644 accessories/iControl.js diff --git a/accessories/XfinityHome.js b/accessories/XfinityHome.js deleted file mode 100644 index cafdbd1..0000000 --- a/accessories/XfinityHome.js +++ /dev/null @@ -1,284 +0,0 @@ -var types = require("HAP-NodeJS/accessories/types.js"); -var request = require("request"); -var xmldoc = require("xmldoc"); - -function XfinityHomeAccessory(log, config) { - this.log = log; - this.name = config["name"]; - this.email = config["email"]; - this.password = config["password"]; - this.dsig = config["dsig"]; - this.pinCode = config["pin"]; -} - -XfinityHomeAccessory.prototype = { - - armWithType: function(armed, type) { - this.log("Arming with type " + type + " = " + armed + "..."); - this.targetArmed = armed; - this.targetArmType = type; - this.getLoginToken(); - }, - - getLoginToken: function() { - this.log("Retrieving login token..."); - - var that = this; - - request.post({ - url: "https://login.comcast.net/api/login", - form: { - appkey:"iControl", - dsig: this.dsig, - u: this.email, - p: this.password - } - }, function(err, response, body) { - - if (!err && response.statusCode == 200) { - - var doc = new xmldoc.XmlDocument(body); - that.loginToken = doc.valueWithPath("LoginToken"); - that.refreshLoginCookie(); - } - else { - that.log("Error '"+err+"' getting login token: " + body); - } - }); - }, - - refreshLoginCookie: function() { - this.log("Refreshing login cookie..."); - - var that = this; - - request.post({ - url: "https://www.xfinityhomesecurity.com/rest/icontrol/login", - form: { - token: this.loginToken - } - }, function(err, response, body) { - - if (!err && response.statusCode == 200) { - - // extract our "site" from the login response - var json = JSON.parse(body); - that.siteHref = json["login"]["site"]["href"]; - - // manual cookie handling - that.loginCookie = response.headers["set-cookie"]; - - that.getInstances(); - } - else { - that.log("Error '"+err+"' refreshing login cookie: " + body); - } - }); - }, - - getInstances: function() { - this.log("Getting instances for site " + this.siteHref + "..."); - - this.panelHref = null; - var that = this; - - request.get({ - url: "https://www.xfinityhomesecurity.com/"+that.siteHref+"/network/instances", - headers: { Cookie: this.loginCookie }, - json: true - }, function(err, response, json) { - - if (!err && response.statusCode == 200) { - - // extract our "instance" from the response. look for the first "panel" - var instances = json["instances"]["instance"]; - for (var i=0; i= 200 && response.statusCode < 300) { - that.log("Arm response: " + response); - } - else { - that.log("Error '"+err+"' performing arm request: " + body); - } - }); - }, - - 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: "Comcast", - 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.SWITCH_STYPE, - characteristics: [{ - cType: types.NAME_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: "Away Mode", - supportEvents: false, - supportBonjour: false, - manfDescription: "Away Mode service", - designedMaxLength: 255 - },{ - cType: types.POWER_STATE_CTYPE, - onUpdate: function(value) { that.armWithType(value, "away"); }, - perms: ["pw","pr","ev"], - format: "bool", - initialValue: false, - supportEvents: false, - supportBonjour: false, - manfDescription: "Turn on the Away alarm", - designedMaxLength: 1 - }] - },{ - sType: types.SWITCH_STYPE, - characteristics: [{ - cType: types.NAME_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: "Night Mode", - supportEvents: false, - supportBonjour: false, - manfDescription: "Night Mode service", - designedMaxLength: 255 - },{ - cType: types.POWER_STATE_CTYPE, - onUpdate: function(value) { that.armWithType(value, "night"); }, - perms: ["pw","pr","ev"], - format: "bool", - initialValue: false, - supportEvents: false, - supportBonjour: false, - manfDescription: "Turn on the Night alarm", - designedMaxLength: 1 - }] - },{ - sType: types.SWITCH_STYPE, - characteristics: [{ - cType: types.NAME_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: "Stay Mode", - supportEvents: false, - supportBonjour: false, - manfDescription: "Stay Mode service", - designedMaxLength: 255 - },{ - cType: types.POWER_STATE_CTYPE, - onUpdate: function(value) { that.armWithType(value, "stay"); }, - perms: ["pw","pr","ev"], - format: "bool", - initialValue: false, - supportEvents: false, - supportBonjour: false, - manfDescription: "Turn on the Stay alarm", - designedMaxLength: 1 - }] - }]; - } -}; - -// Enable cookie handling and append our expected headers -request = request.defaults({ - headers: { - "X-appkey": "comcastTokenKey", - "X-ClientInfo": "5.2.51", - "X-format": "json" - } -}); - -module.exports.accessory = XfinityHomeAccessory; diff --git a/accessories/iControl.js b/accessories/iControl.js new file mode 100644 index 0000000..d948867 --- /dev/null +++ b/accessories/iControl.js @@ -0,0 +1,130 @@ +var iControl = require('node-icontrol').iControl; +var Service = require('HAP-NodeJS').Service; +var Characteristic = require('HAP-NodeJS').Characteristic; + +module.exports = { + accessory: iControlAccessory +} + +/** + * Provides a Security System accessory for an iControl-based security system like Xfinity Home. + */ + +function iControlAccessory(log, config) { + this.log = log; + + this.iControl = new iControl({ + system: iControl.Systems[config.system], + email: config.email, + password: config.password, + pinCode: config.pin + }); + + this.iControl.on('change', this._handleChange.bind(this)); + this.iControl.on('error', this._handleError.bind(this)); + + this.log("Logging into iControl..."); + this.iControl.login(); + + this._securitySystem = new Service.SecuritySystem("Security System"); + + this._securitySystem + .getCharacteristic(Characteristic.SecuritySystemTargetState) + .on('get', this._getTargetState.bind(this)) + .on('set', this._setTargetState.bind(this)); + + this._securitySystem + .getCharacteristic(Characteristic.SecuritySystemCurrentState) + .on('get', this._getCurrentState.bind(this)); +} + +iControlAccessory.prototype._getTargetState = function(callback) { + this.iControl.getArmState(function(err, armState) { + if (err) return callback(err); + + var currentState = this._getHomeKitStateFromArmState(armState); + callback(null, currentState); + + }.bind(this)); +} + +iControlAccessory.prototype._getCurrentState = function(callback) { + this.iControl.getArmState(function(err, armState) { + if (err) return callback(err); + + var currentState = this._getHomeKitStateFromArmState(armState); + callback(null, currentState); + + }.bind(this)); +} + +iControlAccessory.prototype._setTargetState = function(targetState, callback, context) { + if (context == "internal") return callback(null); // we set this state ourself, no need to react to it + + var armState = this._getArmStateFromHomeKitState(targetState); + this.log("Setting target state to %s", armState); + + this.iControl.setArmState(armState, function(err) { + if (err) return callback(err); + + this.log("Successfully set target state to %s", armState); + + // also update current state + this._securitySystem + .getCharacteristic(Characteristic.SecuritySystemCurrentState) + .setValue(targetState); + + callback(null); // success! + + }.bind(this)); +} + +iControlAccessory.prototype._handleChange = function(armState) { + this.log("Arm state changed to %s", armState); + + var homeKitState = this._getHomeKitStateFromArmState(armState); + + this._securitySystem + .getCharacteristic(Characteristic.SecuritySystemCurrentState) + .setValue(homeKitState); + + this._securitySystem + .getCharacteristic(Characteristic.SecuritySystemTargetState) + .setValue(homeKitState, null, "internal"); // these characteristics happen to share underlying values +} + +iControlAccessory.prototype._handleError = function(err) { + this.log(err.message); +} + +iControlAccessory.prototype.getServices = function() { + return [this._securitySystem]; +} + +iControlAccessory.prototype._getHomeKitStateFromArmState = function(armState) { + switch (armState) { + case "disarmed": return Characteristic.SecuritySystemCurrentState.DISARMED; + case "away": return Characteristic.SecuritySystemCurrentState.AWAY_ARM; + case "night": return Characteristic.SecuritySystemCurrentState.NIGHT_ARM; + case "stay": return Characteristic.SecuritySystemCurrentState.STAY_ARM; + } +} + +iControlAccessory.prototype._getArmStateFromHomeKitState = function(homeKitState) { + switch (homeKitState) { + case Characteristic.SecuritySystemCurrentState.DISARMED: return "disarmed"; + case Characteristic.SecuritySystemCurrentState.AWAY_ARM: return "away"; + case Characteristic.SecuritySystemCurrentState.NIGHT_ARM: return "night"; + case Characteristic.SecuritySystemCurrentState.STAY_ARM: return "stay"; + } +} + + +/** + * TESTING + */ + +if (require.main === module) { + var config = JSON.parse(require('fs').readFileSync("config.json")).accessories[0]; + var accessory = new iControlAccessory(console.log, config); +} diff --git a/config-sample.json b/config-sample.json index c9ba829..4245bd5 100644 --- a/config-sample.json +++ b/config-sample.json @@ -104,15 +104,15 @@ "password" : "your-carwings-password" }, { - "accessory": "XfinityHome", + "accessory": "iControl", "name": "Xfinity Home", - "description": "This shim supports the 'Xfinity Home' security system. Unfortunately I don't know how to generate the 'dsig' property, so you'll need to figure yours out by running the Xfinity Home app on your iOS device while connected to a proxy server like Charles. If you didn't understand any of that, sorry! I welcome any suggestions for how to figure out dsig automatically.", - "email": "your-comcast-email@example.com", + "description": "This shim supports iControl-based security systems like Xfinity Home.", + "system": "XFINITY_HOME", + "email": "your-comcast-email", "password": "your-comcast-password", - "dsig": "your-digital-signature", "pin": "your-security-system-pin-code" }, - { + { "accessory": "HomeMatic", "name": "Light", "description": "Control HomeMatic devices (The XMP-API addon for the CCU is required)", diff --git a/package.json b/package.json index ab5b117..7eb761f 100644 --- a/package.json +++ b/package.json @@ -20,17 +20,18 @@ "harmonyhubjs-discover": "git+https://github.com/swissmanu/harmonyhubjs-discover.git", "mdns": "^2.2.4", "node-hue-api": "^1.0.5", + "node-icontrol": "^0.1.0", "node-milight-promise": "0.0.x", "node-persist": "0.0.x", "request": "2.49.x", "sonos": "0.8.x", "telldus-live": "0.2.x", + "teslams": "1.0.1", "unofficial-nest-api": "git+https://github.com/hachidorii/unofficial_nodejs_nest.git#d8d48edc952b049ff6320ef99afa7b2f04cdee98", "wemo": "0.2.x", "wink-js": "0.0.5", "xml2js": "0.4.x", "xmldoc": "0.1.x", - "yamaha-nodejs": "0.4.x", - "teslams": "1.0.1" + "yamaha-nodejs": "0.4.x" } } From 30a705e79f0e9de2245f310080809c5bc164ca99 Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Wed, 2 Sep 2015 06:27:13 +0200 Subject: [PATCH 22/63] Fixes, cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Got rid of old code I clearly don’t need anymore. Switched `this.log`s to `debug`s. Fix for picking wrong vDev for thermostat current temp. Added configuration for `Name` characteristic…which I guess became required. --- package.json | 3 +- platforms/ZWayServer.js | 449 +++++----------------------------------- 2 files changed, 51 insertions(+), 401 deletions(-) diff --git a/package.json b/package.json index ca43ef5..69784c8 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "xml2js": "0.4.x", "xmldoc": "0.1.x", "yamaha-nodejs": "0.4.x", - "teslams": "1.0.1" + "teslams": "1.0.1", + "debug": "^2.2.0" } } diff --git a/platforms/ZWayServer.js b/platforms/ZWayServer.js index 93ade7b..f595408 100644 --- a/platforms/ZWayServer.js +++ b/platforms/ZWayServer.js @@ -1,3 +1,4 @@ +var debug = require('debug')('ZWayServer'); var Service = require("HAP-NodeJS").Service; var Characteristic = require("HAP-NodeJS").Characteristic; var types = require("HAP-NodeJS/accessories/types.js"); @@ -81,26 +82,6 @@ ZWayServerPlatform.getVDevServiceTypes = function(vdev){ } } -/* -ZWayServerPlatform.getVDevCharacteristicsTypes = function(vdev){ - var typeKey = ZWayServerPlatform.getVDevTypeKey(vdev); - switch (typeKey) { - case "switchBinary": - return [types.POWER_STATE_CTYPE]; - case "switchMultilevel": - return [types.POWER_STATE_CTYPE, types.BRIGHTNESS_CTYPE]; - case "thermostat": - return [types.TARGET_TEMPERATURE_CTYPE, types.TEMPERATURE_UNITS_CTYPE, types.CURRENTHEATINGCOOLING_CTYPE, types.TARGETHEATINGCOOLING_CTYPE]; - case "sensorMultilevel.Temperature": - return [types.CURRENT_TEMPERATURE_CTYPE, types.TEMPERATURE_UNITS_CTYPE]; - case "sensorBinary.Door/Window": - return [types.CURRENT_DOOR_STATE_CTYPE, types.TARGET_DOORSTATE_CTYPE, types.OBSTRUCTION_DETECTED_CTYPE]; - case "battery.Battery": - return [types.BATTERY_LEVEL_CTYPE, types.STATUS_LOW_BATTERY_CTYPE]; - } -} -*/ - ZWayServerPlatform.prototype = { zwayRequest: function(opts){ @@ -116,7 +97,7 @@ opts.proxy = 'http://localhost:8888'; request(opts, function(error, response, body){ if(response.statusCode == 401){ - that.log("Authenticating..."); + debug("Authenticating..."); request({ method: "POST", url: that.url + 'ZAutomation/api/v1/login', @@ -139,7 +120,7 @@ proxy: 'http://localhost:8888', if(response.statusCode == 200){ that.sessionId = body.data.sid; opts.headers["Cookie"] = "ZWAYSession=" + that.sessionId; - that.log("Authenticated. Resubmitting original request..."); + debug("Authenticated. Resubmitting original request..."); request(opts, function(error, response, body){ if(response.statusCode == 200){ deferred.resolve(body); @@ -162,7 +143,7 @@ proxy: 'http://localhost:8888', , accessories: function(callback) { - this.log("Fetching Z-Way devices..."); + debug("Fetching Z-Way devices..."); var that = this; var foundAccessories = []; @@ -175,7 +156,7 @@ proxy: 'http://localhost:8888', var groupedDevices = {}; for(var i = 0; i < devices.length; i++){ var vdev = devices[i]; - if(vdev.tags.indexOf("HomeBridge:Skip") >= 0) { that.log("Tag says skip!"); continue; } + if(vdev.tags.indexOf("HomeBridge:Skip") >= 0) { debug("Tag says skip!"); continue; } var gdid = vdev.id.replace(/^(.*?)_zway_(\d+-\d+)-\d.*/, '$1_$2'); var gd = groupedDevices[gdid] || (groupedDevices[gdid] = {devices: [], types: {}, primary: undefined}); gd.devices.push(vdev); @@ -187,10 +168,10 @@ proxy: 'http://localhost:8888', if(!groupedDevices.hasOwnProperty(gdid)) continue; // Debug/log... - that.log('Got grouped device ' + gdid + ' consiting of devices:'); + debug('Got grouped device ' + gdid + ' consiting of devices:'); var gd = groupedDevices[gdid]; for(var j = 0; j < gd.devices.length; j++){ - that.log(gd.devices[j].id + " - " + gd.devices[j].deviceType + (gd.devices[j].metrics && gd.devices[j].metrics.probeTitle ? "." + gd.devices[j].metrics.probeTitle : "")); + debug(gd.devices[j].id + " - " + gd.devices[j].deviceType + (gd.devices[j].metrics && gd.devices[j].metrics.probeTitle ? "." + gd.devices[j].metrics.probeTitle : "")); } var accessory = null; @@ -199,14 +180,14 @@ proxy: 'http://localhost:8888', gd.primary = gd.types[zwshkDeviceClasses[ti].primaryType]; var pd = gd.devices[gd.primary]; var name = pd.metrics && pd.metrics.title ? pd.metrics.title : pd.id; - that.log("Using class with primaryType " + zwshkDeviceClasses[ti].primaryType + ", " + name + " (" + pd.id + ") as primary."); + debug("Using class with primaryType " + zwshkDeviceClasses[ti].primaryType + ", " + name + " (" + pd.id + ") as primary."); accessory = new ZWayServerAccessory(name, zwshkDeviceClasses[ti], gd, that); break; } } if(!accessory) - that.log("WARN: Didn't find suitable device class!"); + debug("WARN: Didn't find suitable device class!"); else foundAccessories.push(accessory); @@ -238,360 +219,6 @@ ZWayServerAccessory.prototype = { qs: (value === undefined ? undefined : value) }); }, - - /* - informationCharacteristics: function() { - return [ - { - 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: "Z-Wave.me", - supportEvents: false, - supportBonjour: false, - manfDescription: "Manufacturer", - designedMaxLength: 255 - },{ - cType: types.MODEL_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: "VDev", - supportEvents: false, - supportBonjour: false, - manfDescription: "Model", - designedMaxLength: 255 - },{ - cType: types.SERIAL_NUMBER_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: "", - 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 - } - ] - }, - - controlCharacteristics: function(vdev) { - var that = this; - var cTypes = []; - - var cxs = ZWayServerPlatform.getVDevCharacteristicsTypes(vdev); - - if(!cxs || cxs.length <= 0) return cTypes; - - if (cxs.indexOf(types.POWER_STATE_CTYPE) >= 0) { - cTypes.push({ - cType: types.POWER_STATE_CTYPE, - onUpdate: function(value) { - if (value == 0) { - that.command(vdev, "off"); - } else { - that.command(vdev, "on"); - } - }, - perms: ["pw","pr","ev"], - format: "bool", - initialValue: 0, - supportEvents: true, - supportBonjour: false, - manfDescription: "Change the power state", - designedMaxLength: 1 - }); - } - - if (cxs.indexOf(types.BRIGHTNESS_CTYPE) >= 0) { - cTypes.push({ - cType: types.BRIGHTNESS_CTYPE, - onUpdate: function(value) { - that.command(vdev, "exact", {level: parseInt(value, 10)}); - }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: 0, - supportEvents: true, - supportBonjour: false, - manfDescription: "Adjust Brightness of Light", - designedMinValue: 0, - designedMaxValue: 100, - designedMinStep: 1, - unit: "%" - }); - } - - if (cxs.indexOf(types.CURRENT_TEMPERATURE_CTYPE) >= 0) { - cTypes.push({ - cType: types.CURRENT_TEMPERATURE_CTYPE, - onUpdate: null, - onRead: function(callback) { - that.platform.zwayRequest({ - method: "GET", - url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id - }).then(function(result){ - callback(result.data.metrics.level); - }); - }, - perms: ["pr","ev"], - format: "int", - initialValue: 20, - supportEvents: false, - supportBonjour: false, - manfDescription: "Current Temperature", - unit: "celsius" - }); - } - - if (cxs.indexOf(types.TARGET_TEMPERATURE_CTYPE) >= 0) { - cTypes.push({ - cType: types.TARGET_TEMPERATURE_CTYPE, - onUpdate: function(value) { - try { - that.command(vdev, "exact", {level: parseFloat(value)}); - } - catch (e) { - that.log(e); - } - }, - onRead: function(callback) { - that.platform.zwayRequest({ - method: "GET", - url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id - }).then(function(result){ - callback(result.data.metrics.level); - }); - }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: 20, - supportEvents: false, - supportBonjour: false, - manfDescription: "Target Temperature", - designedMinValue: vdev.metrics && vdev.metrics.min !== undefined ? vdev.metrics.min : 5, - designedMaxValue: vdev.metrics && vdev.metrics.max !== undefined ? vdev.metrics.max : 40, - designedMinStep: 1, - unit: "celsius" - }); - } - - if (cxs.indexOf(types.TEMPERATURE_UNITS_CTYPE) >= 0) { - cTypes.push({ - cType: types.TEMPERATURE_UNITS_CTYPE, - perms: ["pr"], - format: "int", - //TODO: Let this update from the device if it changes after startup. - initialValue: vdev.metrics.scaleTitle.indexOf("F") >= 0 ? 1 : 0, - supportEvents: false, - supportBonjour: false, - manfDescription: "Unit", - }); - } - - if (cxs.indexOf(types.CURRENTHEATINGCOOLING_CTYPE) >= 0) { - cTypes.push({ - cType: types.CURRENTHEATINGCOOLING_CTYPE, - //TODO: Support multifunction thermostats...only heating supported now. - / * - onUpdate: null, - onRead: function(callback) { - that.getCurrentHeatingCooling(function(currentHeatingCooling){ - callback(currentHeatingCooling); - }); - }, - * / - perms: ["pr"], - format: "int", - initialValue: 1, - supportEvents: false, - supportBonjour: false, - manfDescription: "Current Mode", - designedMaxLength: 1, - designedMinValue: 0, - designedMaxValue: 2, - designedMinStep: 1, - }); - } - - if (cxs.indexOf(types.TARGETHEATINGCOOLING_CTYPE) >= 0) { - cTypes.push({ - cType: types.TARGETHEATINGCOOLING_CTYPE, - //TODO: Support multifunction thermostats...only heating supported now. - / * - onUpdate: function(value) { - that.setTargetHeatingCooling(value); - }, - onRead: function(callback) { - that.getTargetHeatingCoooling(function(targetHeatingCooling){ - callback(targetHeatingCooling); - }); - }, - * / - perms: ["pr"], - format: "int", - initialValue: 0, - supportEvents: false, - supportBonjour: false, - manfDescription: "Target Mode", - designedMinValue: 0, - designedMaxValue: 3, - designedMinStep: 1, - }); - } - - if (cxs.indexOf(types.CONTACT_SENSOR_STATE_CTYPE) >= 0) { - cTypes.push({ - cType: types.CONTACT_SENSOR_STATE_CTYPE, - onUpdate: null, - onRead: function(callback) { - that.platform.zwayRequest({ - method: "GET", - url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id - }).then(function(result){ - callback(result.data.metrics.level == "off" ? 1 : 0); - }); - }, - perms: ["pr","ev"], - format: "bool", - initialValue: 1, - supportEvents: false, - supportBonjour: false, - manfDescription: "Contact State", - designedMaxLength: 1 - }); - } - - if (cxs.indexOf(types.CURRENT_DOOR_STATE_CTYPE) >= 0) { - cTypes.push({ - cType: types.CURRENT_DOOR_STATE_CTYPE, - onRead: function(callback) { - that.platform.zwayRequest({ - method: "GET", - url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id - }).then(function(result){ - callback(result.data.metrics.level == "off" ? 1 : 0); - }); - }, - perms: ["pr","ev"], - format: "int", - initialValue: 1, - supportEvents: false, - supportBonjour: false, - manfDescription: "Current Door State", - designedMinValue: 0, - designedMaxValue: 4, - designedMinStep: 1, - designedMaxLength: 1 - }); - } - - if (cxs.indexOf(types.TARGET_DOORSTATE_CTYPE) >= 0) { - cTypes.push({ - cType: types.TARGET_DOORSTATE_CTYPE, - onRead: function(callback) { - that.platform.zwayRequest({ - method: "GET", - url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id - }).then(function(result){ - callback(result.data.metrics.level == "off" ? 0 : 1); - }); - }, - perms: ["pr","ev"], //TODO: If we support some non-sensor device that can actually open, add "pw"! - format: "int", - initialValue: 0, - supportEvents: false, - supportBonjour: false, - manfDescription: "Target Door State", - designedMinValue: 0, - designedMaxValue: 1, - designedMinStep: 1, - designedMaxLength: 1 - }); - } - - if (cxs.indexOf(types.OBSTRUCTION_DETECTED_CTYPE) >= 0) { - cTypes.push({ - cType: types.OBSTRUCTION_DETECTED_CTYPE, - perms: ["pr","ev"], - format: "bool", - initialValue: false, - supportEvents: false, - supportBonjour: false, - manfDescription: "Obstruction Detected" - }); - } - - if (cxs.indexOf(types.BATTERY_LEVEL_CTYPE) >= 0) { - cTypes.push({ - cType: types.BATTERY_LEVEL_CTYPE, - onRead: function(callback) { - that.platform.zwayRequest({ - method: "GET", - url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id - }).then(function(result){ - callback(result.data.metrics.level); - }); - }, - perms: ["pr","ev"], - format: "uint8", - initialValue: 100, - supportEvents: true, - supportBonjour: false, - manfDescription: "Battery Level", - designedMinValue: 0, - designedMaxValue: 100, - designedMinStep: 1, - unit: "%" - }); - } - - if (cxs.indexOf(types.STATUS_LOW_BATTERY_CTYPE) >= 0) { - cTypes.push({ - cType: types.STATUS_LOW_BATTERY_CTYPE, - onUpdate: null, - onRead: function(callback) { - that.platform.zwayRequest({ - method: "GET", - url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id - }).then(function(result){ - callback(result.data.metrics.level <= that.platform.batteryLow ? 1 : 0); - }); - }, - perms: ["pr","ev"], - format: "uint8", - initialValue: 0, - supportEvents: false, - supportBonjour: false, - manfDescription: "Battery is low", - designedMaxLength: 1 - }); - } - - return cTypes; - }, - */ getVDevServices: function(vdev){ var typeKey = ZWayServerPlatform.getVDevTypeKey(vdev); @@ -647,6 +274,12 @@ ZWayServerAccessory.prototype = { map[(new Characteristic.ChargingState).UUID] = ["battery.Battery"]; //TODO: Always a fixed result } + if(cx instanceof Characteristic.Name) return vdevPreferred; + + // Special case!: If cx is a CurrentTemperature, ignore the preferred device...we want the sensor if available! + if(cx instanceof Characteristic.CurrentTemperature) vdevPreferred = null; + // + var typekeys = map[cx.UUID]; if(typekeys === undefined) return null; @@ -668,19 +301,29 @@ ZWayServerAccessory.prototype = { var that = this; var gdv = function(){ - that.log("Default value for " + vdev.metrics.title + " is " + vdev.metrics.level); + debug("Default value for " + vdev.metrics.title + " is " + vdev.metrics.level); return vdev.metrics.level; }; + if(cx instanceof Characteristic.Name){ + cx.getDefaultValue = function(){ return this.name; }; + cx.on('get', function(callback, context){ + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + callback(false, that.name); + }); + cx.writable = false; + return cx; + } + if(cx instanceof Characteristic.On){ cx.getDefaultValue = gdv; cx.on('get', function(callback, context){ - that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); that.platform.zwayRequest({ method: "GET", url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id }).then(function(result){ - that.log("Got value: " + result.data.metrics.level + ", for " + vdev.metrics.title + "."); + debug("Got value: " + result.data.metrics.level + ", for " + vdev.metrics.title + "."); var val; if(result.data.metrics.level === "off"){ val = false; @@ -703,12 +346,12 @@ ZWayServerAccessory.prototype = { if(cx instanceof Characteristic.Brightness){ cx.getDefaultValue = gdv; cx.on('get', function(callback, context){ - that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); that.platform.zwayRequest({ method: "GET", url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id }).then(function(result){ - that.log("Got value " + result.data.metrics.level + " for " + vdev.metrics.title + "."); + debug("Got value: " + result.data.metrics.level + ", for " + vdev.metrics.title + "."); callback(false, result.data.metrics.level); }); }.bind(this)); @@ -723,11 +366,12 @@ ZWayServerAccessory.prototype = { if(cx instanceof Characteristic.CurrentTemperature){ cx.getDefaultValue = gdv; cx.on('get', function(callback, context){ - that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); that.platform.zwayRequest({ method: "GET", url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id }).then(function(result){ + debug("Got value: " + result.data.metrics.level + ", for " + vdev.metrics.title + "."); callback(false, result.data.metrics.level); }); }.bind(this)); @@ -739,7 +383,7 @@ ZWayServerAccessory.prototype = { if(cx instanceof Characteristic.TargetTemperature){ cx.getDefaultValue = gdv; cx.on('get', function(callback, context){ - that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); this.platform.zwayRequest({ method: "GET", url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id @@ -749,6 +393,7 @@ ZWayServerAccessory.prototype = { }.bind(this)); cx.on('set', function(level, callback){ this.command(vdev, "exact", {level: parseInt(level, 10)}).then(function(result){ + debug("Got value: " + result.data.metrics.level + ", for " + vdev.metrics.title + "."); callback(); }); }.bind(this)); @@ -761,7 +406,7 @@ ZWayServerAccessory.prototype = { //TODO: Always in °C for now. cx.getDefaultValue = function(){ return Characteristic.TemperatureDisplayUnits.CELCIUS; }; cx.on('get', function(callback, context){ - that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); callback(false, Characteristic.TemperatureDisplayUnits.CELCIUS); }); cx.writable = false; @@ -772,7 +417,7 @@ ZWayServerAccessory.prototype = { //TODO: Always HEAT for now, we don't have an example to work with that supports another function. cx.getDefaultValue = function(){ return Characteristic.CurrentHeatingCoolingState.HEAT; }; cx.on('get', function(callback, context){ - that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); callback(false, Characteristic.CurrentHeatingCoolingState.HEAT); }); return cx; @@ -782,7 +427,7 @@ ZWayServerAccessory.prototype = { //TODO: Always HEAT for now, we don't have an example to work with that supports another function. cx.getDefaultValue = function(){ return Characteristic.TargetHeatingCoolingState.HEAT; }; cx.on('get', function(callback, context){ - that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); callback(false, Characteristic.TargetHeatingCoolingState.HEAT); }); cx.writable = false; @@ -794,11 +439,12 @@ ZWayServerAccessory.prototype = { return vdev.metrics.level == "off" ? Characteristic.CurrentDoorState.CLOSED : Characteristic.CurrentDoorState.OPEN; }; cx.on('get', function(callback, context){ - that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); this.platform.zwayRequest({ method: "GET", url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id }).then(function(result){ + debug("Got value: " + result.data.metrics.level + ", for " + vdev.metrics.title + "."); callback(false, result.data.metrics.level == "off" ? Characteristic.CurrentDoorState.CLOSED : Characteristic.CurrentDoorState.OPEN); }); }.bind(this)); @@ -808,7 +454,7 @@ ZWayServerAccessory.prototype = { //TODO: We only support this for Door sensors now, so it's a fixed value. cx.getDefaultValue = function(){ return Characteristic.TargetDoorState.CLOSED; }; cx.on('get', function(callback, context){ - that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); callback(false, Characteristic.TargetDoorState.CLOSED); }); //cx.readable = false; @@ -819,7 +465,7 @@ ZWayServerAccessory.prototype = { //TODO: We only support this for Door sensors now, so it's a fixed value. cx.getDefaultValue = function(){ return false; }; cx.on('get', function(callback, context){ - that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); callback(false, false); }); //cx.readable = false; @@ -829,11 +475,12 @@ ZWayServerAccessory.prototype = { if(cx instanceof Characteristic.BatteryLevel){ cx.getDefaultValue = gdv; cx.on('get', function(callback, context){ - that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); that.platform.zwayRequest({ method: "GET", url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id }).then(function(result){ + debug("Got value: " + result.data.metrics.level + ", for " + vdev.metrics.title + "."); callback(false, result.data.metrics.level); }); }.bind(this)); @@ -842,11 +489,12 @@ ZWayServerAccessory.prototype = { if(cx instanceof Characteristic.StatusLowBattery){ cx.getDefaultValue = function(){ return Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; }; cx.on('get', function(callback, context){ - that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); that.platform.zwayRequest({ method: "GET", url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id }).then(function(result){ + debug("Got value: " + result.data.metrics.level + ", for " + vdev.metrics.title + "."); callback(false, result.data.metrics.level <= that.platform.batteryLow ? Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL); }); }.bind(this)); @@ -856,7 +504,7 @@ ZWayServerAccessory.prototype = { //TODO: No known chargeable devices(?), so always return false. cx.getDefaultValue = function(){ return Characteristic.ChargingState.NOT_CHARGING; }; cx.on('get', function(callback, context){ - that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); callback(false, Characteristic.ChargingState.NOT_CHARGING); }); //cx.readable = false; @@ -872,7 +520,7 @@ ZWayServerAccessory.prototype = { var vdev = this.getVDevForCharacteristic(cx, vdev); if(!vdev){ success = false; - this.log("ERROR! Failed to configure required characteristic \"" + service.characteristics[i].displayName + "\"!"); + debug("ERROR! Failed to configure required characteristic \"" + service.characteristics[i].displayName + "\"!"); } cx = this.configureCharacteristic(cx, vdev); } @@ -881,6 +529,7 @@ ZWayServerAccessory.prototype = { var vdev = this.getVDevForCharacteristic(cx); if(!vdev) continue; cx = this.configureCharacteristic(cx, vdev); + if(cx) service.addCharacteristic(cx); } return success; } @@ -903,7 +552,7 @@ ZWayServerAccessory.prototype = { if(this.devDesc.types["battery.Battery"]) services = services.concat(this.getVDevServices(this.devDesc.devices[this.devDesc.types["battery.Battery"]])); - this.log("Loaded services for " + this.name); + debug("Loaded services for " + this.name); return services; } }; From 86e17a392256ef68122e20b60e6784b5a9efcdba Mon Sep 17 00:00:00 2001 From: Nick Farina Date: Wed, 2 Sep 2015 07:20:36 -0700 Subject: [PATCH 23/63] Bump node-icontrol with fixes --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7eb761f..da0b266 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "harmonyhubjs-discover": "git+https://github.com/swissmanu/harmonyhubjs-discover.git", "mdns": "^2.2.4", "node-hue-api": "^1.0.5", - "node-icontrol": "^0.1.0", + "node-icontrol": "^0.1.2", "node-milight-promise": "0.0.x", "node-persist": "0.0.x", "request": "2.49.x", From a3cbf5a380a7ce00404f7e3265fa9e046c5adcb8 Mon Sep 17 00:00:00 2001 From: Nick Farina Date: Wed, 2 Sep 2015 18:51:23 -0700 Subject: [PATCH 24/63] Another node-icontrol bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index da0b266..e7fd564 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "harmonyhubjs-discover": "git+https://github.com/swissmanu/harmonyhubjs-discover.git", "mdns": "^2.2.4", "node-hue-api": "^1.0.5", - "node-icontrol": "^0.1.2", + "node-icontrol": "^0.1.3", "node-milight-promise": "0.0.x", "node-persist": "0.0.x", "request": "2.49.x", From a677edb2cf4c44d39fde54623f410711d1260897 Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Fri, 4 Sep 2015 06:18:23 +0200 Subject: [PATCH 25/63] Trying for a stable baseline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switched Door sensors back to GarageDoorOpener services for now, and disabled battery service…lets see if we can get this stabilized. --- platforms/ZWayServer.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/platforms/ZWayServer.js b/platforms/ZWayServer.js index f595408..9a515ee 100644 --- a/platforms/ZWayServer.js +++ b/platforms/ZWayServer.js @@ -237,7 +237,7 @@ ZWayServerAccessory.prototype = { services.push(new Service.TemperatureSensor(vdev.metrics.title)); break; case "sensorBinary.Door/Window": - services.push(new Service.Door(vdev.metrics.title)); + services.push(new Service.GarageDoorOpener(vdev.metrics.title)); break; case "battery.Battery": services.push(new Service.BatteryService(vdev.metrics.title)); @@ -388,6 +388,7 @@ ZWayServerAccessory.prototype = { method: "GET", url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id }).then(function(result){ + debug("Got value: " + result.data.metrics.level + ", for " + vdev.metrics.title + "."); callback(false, result.data.metrics.level); }); }.bind(this)); @@ -549,8 +550,9 @@ ZWayServerAccessory.prototype = { services = services.concat(this.getVDevServices(this.devDesc.devices[this.devDesc.primary])); - if(this.devDesc.types["battery.Battery"]) - services = services.concat(this.getVDevServices(this.devDesc.devices[this.devDesc.types["battery.Battery"]])); + if(this.devDesc.types["battery.Battery"]){ + //services = services.concat(this.getVDevServices(this.devDesc.devices[this.devDesc.types["battery.Battery"]])); + } debug("Loaded services for " + this.name); return services; From bad0ba0c3b87b4fb0570516b182aad051a566f9c Mon Sep 17 00:00:00 2001 From: Snowdd1 Date: Fri, 4 Sep 2015 15:45:19 +0200 Subject: [PATCH 26/63] initial test version --- accessories/knxlamp.js | 180 +++++++++++++++++++++++++++++++++++ platforms/KNX.js | 206 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 386 insertions(+) create mode 100644 accessories/knxlamp.js create mode 100644 platforms/KNX.js diff --git a/accessories/knxlamp.js b/accessories/knxlamp.js new file mode 100644 index 0000000..337904c --- /dev/null +++ b/accessories/knxlamp.js @@ -0,0 +1,180 @@ +/* + * This is a demo KNX lamp accessory shim. + * It can switch a light on and off, and optionally set a brightness if configured to do so + * + */ +var Service = require("HAP-NodeJS").Service; +var Characteristic = require("HAP-NodeJS").Characteristic; +var knxd = require("eibd"); + + + +function KNXlampAccessory(log, config) { + this.log = log; + + + // knx information on object + this.group_address = config.group_address; + this.listen_addresses = config.listen_addresses; // supposed to be undefined, an array of strings, or single string + this.can_dim = config.can_dim; //supposed to be true or false + this.brightness_group_address = config.brightness_group_address; + this.brightness_listen_addresses = config.brightness_listen_addresses; + this.knxd_ip = config.knxd_ip ; // eg 127.0.0.1 if running on localhost + this.knxd_port = config.knxd_port || 6720; // eg 6720 default knxd port + +} + + +module.exports = { + accessory: KNXlampAccessory + }; + + +KNXlampAccessory.prototype = { + + + knxwrite: function(callback, groupAddress, dpt, value) { + // this.log("DEBUG in knxwrite"); + var knxdConnection = new knxd.Connection(); + // this.log("DEBUG in knxwrite: created empty connection, trying to connect socket to "+this.knxd_ip+":"+this.knxd_port); + knxdConnection.socketRemote({ host: this.knxd_ip, port: this.knxd_port }, function() { + var dest = knxd.str2addr(groupAddress); + // this.log("DEBUG got dest="+dest); + knxdConnection.openTGroup(dest, 1, function(err) { + if (err) { + this.log("[ERROR] knxwrite:openTGroup: " + err); + callback(err); + } else { + // this.log("DEBUG opened TGroup "); + var msg = knxd.createMessage('write', dpt, parseFloat(value)); + knxdConnection.sendAPDU(msg, function(err) { + if (err) { + this.log("[ERROR] knxwrite:sendAPDU: " + err); + callback(err); + } else { + // this.log("knx data sent"); + callback(); + } + }.bind(this)); + } + }.bind(this)); + }.bind(this)); + }, + + // issues a read request on the knx bus + // DOES NOT WAIT for an answer. Please register the address with a callback using registerGA() function + knxread: function(groupAddress){ + // this.log("DEBUG in knxread"); + var knxdConnection = new knxd.Connection(); + // this.log("DEBUG in knxread: created empty connection, trying to connect socket to "+this.knxd_ip+":"+this.knxd_port); + knxdConnection.socketRemote({ host: this.knxd_ip, port: this.knxd_port }, function() { + var dest = knxd.str2addr(groupAddress); + // this.log("DEBUG got dest="+dest); + knxdConnection.openTGroup(dest, 1, function(err) { + if (err) { + this.log("[ERROR] knxread:openTGroup: " + err); + } else { + // this.log("DEBUG knxread: opened TGroup "); + var msg = knxd.createMessage('read', 'DPT1', 0); + knxdConnection.sendAPDU(msg, function(err) { + if (err) { + this.log("[ERROR] knxread:sendAPDU: " + err); + } else { + this.log("knx request sent"); + } + }.bind(this)); + } + }.bind(this)); + }.bind(this)); + }, + + knxregister: function(addresses, characteristic) { + console.log("knx registering " + addresses); + knxd.registerGA(addresses, function(value){ + // parameters do not match + this.log("Getting value from bus:"+value); + characteristic.setValue(value, undefined, 'fromKNXBus'); + }.bind(this)); + }, + + setPowerState: function(value, callback, context) { + if (context === 'fromKNXBus') { + this.log("event ping pong, exit!"); + if (callback) { + callback(); + } + } else { + console.log("Setting power to %s", value); + var numericValue = 0; + if (value) { + numericValue = 1; // need 0 or 1, not true or something + } + this.knxwrite(callback, this.group_address,'DPT1',numericValue); + } + + }, + + + setBrightness: function(value, callback, context) { + if (context === 'fromKNXBus') { + this.log("event ping pong, exit!"); + if (callback) { + callback(); + } + } else { + this.log("Setting brightness to %s", value); + var numericValue = 0; + if (value) { + numericValue = 255*value/100; // convert 1..100 to 1..255 for KNX bus + } + this.knxwrite(callback, this.brightness_group_address,'DPT5',numericValue); + } + }, + + + + identify: function(callback) { + this.log("Identify requested!"); + callback(); // success + }, + + 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, "Opensource Community") + .setCharacteristic(Characteristic.Model, "KNX Light Switch with or without dimmer") + .setCharacteristic(Characteristic.SerialNumber, "Version 1"); + + var lightbulbService = new Service.Lightbulb(); + + var onCharacteristic = lightbulbService + .getCharacteristic(Characteristic.On) + .on('set', this.setPowerState.bind(this)); + onCharacteristic.supportsEventNotification=true; + // register with value update service + this.addresses = [this.group_address]; + this.log("DEBUG1 this.addresses = "+this.addresses); + this.log("DEBUG2 this.listen_addresses = "+this.listen_addresses); + this.addresses = this.addresses.concat(this.listen_addresses || []); // do not join anything if empty (do not add undefined) + this.log("DEBUG3 this.addresses = "+this.addresses); + this.knxregister(this.addresses, onCharacteristic); + this.knxread(this.group_address); // issue a read request on the bus, maybe the device answers to that! + + if (this.can_dim) { + var brightnessCharacteristic = lightbulbService + .addCharacteristic(new Characteristic.Brightness()) + .on('set', this.setBrightness.bind(this)); + // register with value update service + this.brightness_addresses = [this.brightness_group_address]; + this.brightness_addresses.concat(this.brightness_listen_addresses || []); // do not join anything if empty (do not add undefined) + this.knxregister(this.brightness_addresses, brightnessCharacteristic); + this.knxread(this.brightness_group_address); // issue a read request on the bus, maybe the device answers to that! + } + knxd.startMonitor({ host: this.knxd_ip, port: this.knxd_port }); + return [informationService, lightbulbService]; + } +}; diff --git a/platforms/KNX.js b/platforms/KNX.js new file mode 100644 index 0000000..0b4f765 --- /dev/null +++ b/platforms/KNX.js @@ -0,0 +1,206 @@ +/** Sample platform outline + * based on Sonos platform + */ +'use strict'; +var types = require("HAP-NodeJS/accessories/types.js"); +//var hardware = require('myHardwareSupport'); //require any additional hardware packages +var Connection = require('eibd').connection; + +function KNXPlatform(log, config){ + this.log = log; + this.config = config; +// this.property1 = config.property1; +// this.property2 = config.property2; + + + // initiate connection to bus for listening ==> done with first shim + +}; + +MyPlatform.prototype = { + accessories: function(callback) { + this.log("Fetching myPlatform devices."); + var that = this; + + + // iterate through all devices the platform my offer + // for each device, create an accessory + + // read accessories from file !!!!! + var foundAccessories = this.config.accessories; + + + //create array of accessories + var myAccessories = []; + + for (var int = 0; int < foundAccessories.length; int++) { + + // instantiate and push to array + if (foundAccessories[i].accessory-type === "knxlamp") { + + myAccessories.push(new require('../accessories/knxlamp.js').accessory(this.log,foundAccessories[i])); + } else { + // do something else + this.log("unkown accessory type found") + } //etc. + + }; + // if done, return the array to callback function + callback(myAccessories); + } +}; + + +// the signature of the constructor has to be adopted to the accessory you need in your platform! These are the first lines from the sonos platform +function myAccessoryType1(log, config, device, description /* add or remove parms as you need*/ ) { + + this.log = log; + this.config = config; + this.device = device; + this.description = description; + // more initialization if required + +} + +myAccessoryType1.prototype = { + // see https shim wiki page for details. Accessory definition is discussed there. +} + +// more + + +/** + * The buscallbacks module is to expose a simple function to listen on the bus and register callbacks for value changes + * of registered addresses. + * + * Usage: +* You can start the monitoring process at any time + startMonitor({host: name-ip, port: port-num }); + +* You can add addresses to the subscriptions using + +registerGA(groupAddress, callback) + +* groupAddress has to be an groupAddress in common knx notation string '1/2/3' +* the callback has to be a +* var f = function(value) { handle value update;} +* so you can do a +* registerGA('1/2/3', function(value){ +* console.log('1/2/3 got a hit with '+value); +* }); +* but of course it is meant to be used programmatically, not literally, otherwise it has no advantage +* +* You can also use arrays of addresses if your callback is supposed to listen to many addresses: + +registerGA(groupAddresses[], callback) + +* as in +* registerGA(['1/2/3','1/0/0'], function(value){ +* console.log('1/2/3 or 1/0/0 got a hit with '+value); +* }); +* if you are having central addresses like "all lights off" or additional response objects +* +* +* callbacks can have a signature of +* function(value, src, dest, type) but do not have to support these parameters (order matters) +* src = physical address such as '1.1.20' +* dest = groupAddress hit (you subscribed to that address, remember?), as '1/2/3' +* type = Data point type, as 'DPT1' +* +* +*/ + + + +// array of registered addresses and their callbacks +var subscriptions = []; +// check variable to avoid running two listeners +var running; + +function groupsocketlisten(opts, callback) { + var conn = Connection(); + conn.socketRemote(opts, function() { + conn.openGroupSocket(0, callback); + }); +} + + +var registerSingleGA = function registerSingleGA (groupAddress, callback) { + subscriptions.push({address: groupAddress, callback: callback }); +} + +/* + * public busMonitor.startMonitor() + * starts listening for telegrams on KNX bus + * + */ +var startMonitor = function startMonitor(opts) { // using { host: name-ip, port: port-num } options object + if (!running) { + running = true; + } else { + return null; + } + + groupsocketlisten(opts, function(parser) { + //console.log("knxfunctions.read: in callback parser"); + parser.on('write', function(src, dest, type, val){ + // search the registered group addresses + for (var i = 0; i < subscriptions.length; i++) { + // iterate through all registered addresses + if (subscriptions[i].address === dest) { + // found one, notify + //console.log('HIT: Write from '+src+' to '+dest+': '+val+' ['+type+']'); + subscriptions[i].callback(val, src, dest, type); + } + } + }); + + parser.on('response', function(src, dest, type, val) { + // search the registered group addresses + for (var i = 0; i < subscriptions.length; i++) { + // iterate through all registered addresses + if (subscriptions[i].address === dest) { + // found one, notify + //console.log('HIT: Response from '+src+' to '+dest+': '+val+' ['+type+']'); + subscriptions[i].callback(val, src, dest, type); + } + } + + }); + + //dont care about reads here +// parser.on('read', function(src, dest) { +// console.log('Read from '+src+' to '+dest); +// }); + //console.log("knxfunctions.read: in callback parser at end"); + }); // groupsocketlisten parser +}; //startMonitor + + +/* + * public registerGA(groupAdresses[], callback(value)) + * parameters + * callback: function(value, src, dest, type) called when a value is sent on the bus + * groupAddresses: (Array of) string(s) for group addresses + * + * + * + */ +var registerGA = function (groupAddresses, callback) { + // check if the groupAddresses is an array + if (groupAddresses.constructor.toString().indexOf("Array") > -1) { + // handle multiple addresses + for (var i = 0; i < groupAddresses.length; i++) { + registerSingleGA (groupAddresses[i], callback); + } + } else { + // it's only one + registerSingleGA (groupAddresses, callback); + } +}; + + + +module.exports.platform = myPlatform; +module.exports.registerGA = registerGA; +module.exports.startMonitor = startMonitor; \ No newline at end of file From 1a98a6c9ac2feb35d8b9656ab3478df101ad08cd Mon Sep 17 00:00:00 2001 From: Snowdd1 Date: Fri, 4 Sep 2015 16:35:11 +0200 Subject: [PATCH 27/63] got it straight does not use PR#15 for node-eibd any more. --- accessories/knxlamp.js | 10 ++++++++-- platforms/KNX.js | 24 +++++++++++++++--------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/accessories/knxlamp.js b/accessories/knxlamp.js index 337904c..738164c 100644 --- a/accessories/knxlamp.js +++ b/accessories/knxlamp.js @@ -6,6 +6,8 @@ var Service = require("HAP-NodeJS").Service; var Characteristic = require("HAP-NodeJS").Characteristic; var knxd = require("eibd"); +var knxd_registerGA = require('../platforms/KNX.js').registerGA; +var knxd_startMonitor = require('../platforms/KNX.js').startMonitor; @@ -21,6 +23,10 @@ function KNXlampAccessory(log, config) { this.brightness_listen_addresses = config.brightness_listen_addresses; this.knxd_ip = config.knxd_ip ; // eg 127.0.0.1 if running on localhost this.knxd_port = config.knxd_port || 6720; // eg 6720 default knxd port + if (config.name) { + this.name = config.name; + } + log("Accessory constructor called"); } @@ -90,7 +96,7 @@ KNXlampAccessory.prototype = { knxregister: function(addresses, characteristic) { console.log("knx registering " + addresses); - knxd.registerGA(addresses, function(value){ + knxd_registerGA(addresses, function(value){ // parameters do not match this.log("Getting value from bus:"+value); characteristic.setValue(value, undefined, 'fromKNXBus'); @@ -174,7 +180,7 @@ KNXlampAccessory.prototype = { this.knxregister(this.brightness_addresses, brightnessCharacteristic); this.knxread(this.brightness_group_address); // issue a read request on the bus, maybe the device answers to that! } - knxd.startMonitor({ host: this.knxd_ip, port: this.knxd_port }); + knxd_startMonitor({ host: this.knxd_ip, port: this.knxd_port }); return [informationService, lightbulbService]; } }; diff --git a/platforms/KNX.js b/platforms/KNX.js index 0b4f765..0b5c659 100644 --- a/platforms/KNX.js +++ b/platforms/KNX.js @@ -4,7 +4,7 @@ 'use strict'; var types = require("HAP-NodeJS/accessories/types.js"); //var hardware = require('myHardwareSupport'); //require any additional hardware packages -var Connection = require('eibd').connection; +var knxd = require('eibd'); function KNXPlatform(log, config){ this.log = log; @@ -17,9 +17,9 @@ function KNXPlatform(log, config){ }; -MyPlatform.prototype = { +KNXPlatform.prototype = { accessories: function(callback) { - this.log("Fetching myPlatform devices."); + this.log("Fetching KNX devices."); var that = this; @@ -34,11 +34,16 @@ MyPlatform.prototype = { var myAccessories = []; for (var int = 0; int < foundAccessories.length; int++) { - + this.log("parsing acc " + int + " of " + foundAccessories.length); // instantiate and push to array - if (foundAccessories[i].accessory-type === "knxlamp") { - - myAccessories.push(new require('../accessories/knxlamp.js').accessory(this.log,foundAccessories[i])); + if (foundAccessories[int].accessory_type === "knxlamp") { + this.log("push new lamp with "+foundAccessories[int].name); + foundAccessories[int].knxd_ip = this.config.knxd_ip; + foundAccessories[int].knxd_port = this.config.knxd_port; + var accConstructor = require('./../accessories/knxlamp.js'); + var acc = new accConstructor.accessory(this.log,foundAccessories[int]); + this.log("created "+acc.name+" accessory"); + myAccessories.push(acc); } else { // do something else this.log("unkown accessory type found") @@ -46,6 +51,7 @@ MyPlatform.prototype = { }; // if done, return the array to callback function + this.log("returning "+myAccessories.length+" accessories"); callback(myAccessories); } }; @@ -118,7 +124,7 @@ var subscriptions = []; var running; function groupsocketlisten(opts, callback) { - var conn = Connection(); + var conn = knxd.Connection(); conn.socketRemote(opts, function() { conn.openGroupSocket(0, callback); }); @@ -201,6 +207,6 @@ var registerGA = function (groupAddresses, callback) { -module.exports.platform = myPlatform; +module.exports.platform = KNXPlatform; module.exports.registerGA = registerGA; module.exports.startMonitor = startMonitor; \ No newline at end of file From 116dd1b3159d06ba6a006f0734cfb1c0458bea48 Mon Sep 17 00:00:00 2001 From: Snowdd1 Date: Fri, 4 Sep 2015 17:39:46 +0200 Subject: [PATCH 28/63] still WIP with Thermostat stub. Somehow stops working after a few lines of console.log --- accessories/knxlamp.js | 4 +- accessories/knxthermostat.js | 178 +++++++++++++++++++++++++++++++++++ platforms/KNX.js | 35 ++++--- 3 files changed, 203 insertions(+), 14 deletions(-) create mode 100644 accessories/knxthermostat.js diff --git a/accessories/knxlamp.js b/accessories/knxlamp.js index 738164c..7f06529 100644 --- a/accessories/knxlamp.js +++ b/accessories/knxlamp.js @@ -1,6 +1,6 @@ /* - * This is a demo KNX lamp accessory shim. - * It can switch a light on and off, and optionally set a brightness if configured to do so + * This is a demo KNX thermostat accessory shim. + * It can * */ var Service = require("HAP-NodeJS").Service; diff --git a/accessories/knxthermostat.js b/accessories/knxthermostat.js new file mode 100644 index 0000000..e0f408a --- /dev/null +++ b/accessories/knxthermostat.js @@ -0,0 +1,178 @@ +/* + * This is a demo KNX lamp accessory shim. + * It can switch a light on and off, and optionally set a brightness if configured to do so + * + */ +var Service = require("HAP-NodeJS").Service; +var Characteristic = require("HAP-NodeJS").Characteristic; +var knxd = require("eibd"); +var knxd_registerGA = require('../platforms/KNX.js').registerGA; +var knxd_startMonitor = require('../platforms/KNX.js').startMonitor; + + + +function KNXthermoAccessory(log, config) { + this.log = log; + this.config=config; + + // knx information on object + this.curr_temp_address = config.curr_temp_address; + this.curr_temp_listen_addresses = config.curr_temp_listen_addresses; // supposed to be undefined, an array of strings, or single string + this.target_temp_address = config.target_temp_address; + this.knxd_ip = config.knxd_ip ; // eg 127.0.0.1 if running on localhost + this.knxd_port = config.knxd_port || 6720; // eg 6720 default knxd port + if (config.name) { + this.name = config.name; + } + log("Accessory constructor called"); + +} + + +module.exports = { + accessory: KNXthermoAccessory + }; + + +KNXthermoAccessory.prototype = { + + + knxwrite: function(callback, groupAddress, dpt, value) { + // this.log("DEBUG in knxwrite"); + var knxdConnection = new knxd.Connection(); + // this.log("DEBUG in knxwrite: created empty connection, trying to connect socket to "+this.knxd_ip+":"+this.knxd_port); + knxdConnection.socketRemote({ host: this.knxd_ip, port: this.knxd_port }, function() { + var dest = knxd.str2addr(groupAddress); + // this.log("DEBUG got dest="+dest); + knxdConnection.openTGroup(dest, 1, function(err) { + if (err) { + this.log("[ERROR] knxwrite:openTGroup: " + err); + callback(err); + } else { + // this.log("DEBUG opened TGroup "); + var msg = knxd.createMessage('write', dpt, parseFloat(value)); + knxdConnection.sendAPDU(msg, function(err) { + if (err) { + this.log("[ERROR] knxwrite:sendAPDU: " + err); + callback(err); + } else { + this.log("knx data sent"); + callback(); + } + }.bind(this)); + } + }.bind(this)); + }.bind(this)); + }, + + // issues a read request on the knx bus + // DOES NOT WAIT for an answer. Please register the address with a callback using registerGA() function + knxread: function(groupAddress){ + // this.log("DEBUG in knxread"); + var knxdConnection = new knxd.Connection(); + // this.log("DEBUG in knxread: created empty connection, trying to connect socket to "+this.knxd_ip+":"+this.knxd_port); + knxdConnection.socketRemote({ host: this.knxd_ip, port: this.knxd_port }, function() { + var dest = knxd.str2addr(groupAddress); + // this.log("DEBUG got dest="+dest); + knxdConnection.openTGroup(dest, 1, function(err) { + if (err) { + this.log("[ERROR] knxread:openTGroup: " + err); + } else { + // this.log("DEBUG knxread: opened TGroup "); + var msg = knxd.createMessage('read', 'DPT1', 0); + knxdConnection.sendAPDU(msg, function(err) { + if (err) { + this.log("[ERROR] knxread:sendAPDU: " + err); + } else { + this.log("knx request sent"); + } + }.bind(this)); + } + }.bind(this)); + }.bind(this)); + }, + + knxregister: function(addresses, characteristic) { + console.log("knx registering " + addresses); + knxd_registerGA(addresses, function(value){ + // parameters do not match + this.log("Getting value from bus:"+value); + characteristic.setValue(value, undefined, 'fromKNXBus'); + }.bind(this)); + }, + + + + identify: function(callback) { + this.log("Identify requested!"); + callback(); // success + }, + + 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, "Opensource Community") + .setCharacteristic(Characteristic.Model, "KNX Thermostat") + .setCharacteristic(Characteristic.SerialNumber, "Version 1"); + + var myService = new Service.Thermostat(); + +// +// // Required Characteristics +// this.addCharacteristic(Characteristic.CurrentHeatingCoolingState); +// this.addCharacteristic(Characteristic.TargetHeatingCoolingState); +// this.addCharacteristic(Characteristic.CurrentTemperature); //check +// this.addCharacteristic(Characteristic.TargetTemperature); // +// this.addCharacteristic(Characteristic.TemperatureDisplayUnits); +// +// // Optional Characteristics +// this.addOptionalCharacteristic(Characteristic.CurrentRelativeHumidity); +// this.addOptionalCharacteristic(Characteristic.TargetRelativeHumidity); +// this.addOptionalCharacteristic(Characteristic.CoolingThresholdTemperature); +// this.addOptionalCharacteristic(Characteristic.HeatingThresholdTemperature); +// this.addOptionalCharacteristic(Characteristic.Name); + + + var CurrentTemperatureCharacteristic = myService + .getCharacteristic(Characteristic.CurrentTemperature) + // .on('set', this.setPowerState.bind(this)); + CurrentTemperatureCharacteristic.supportsEventNotification=true; + // register with value update service + this.addresses1 = [this.curr_temp_address]; + this.addresses1 = this.addresses1.concat(this.curr_temp_listen_addresses || []); // do not join anything if empty (do not add undefined) + this.knxregister(this.addresses1, CurrentTemperatureCharacteristic); + this.knxread(this.curr_temp_address); // issue a read request on the bus, maybe the device answers to that! + + var TargetTemperatureCharacteristic = myService + .getCharacteristic(Characteristic.TargetTemperature) + .on('set', function(value, callback, context) { + if (context === 'fromKNXBus') { + this.log("event ping pong, exit!"); + if (callback) { + callback(); + } + } else { + console.log("Setting temperature to %s", value); + var numericValue = 0.0; + if (value) { + numericValue = 0+value; // need to be numeric + } + this.knxwrite(callback, this.target_temp_address,'DPT9',numericValue); + } + }.bind(this)); + TargetTemperatureCharacteristic.supportsEventNotification=true; + // register with value update service + this.addresses2 = [this.target_temp_address]; + this.addresses2 = this.addresses2.concat(this.target_temp_listen_addresses || []); // do not join anything if empty (do not add undefined) + this.knxregister(this.addresses2, TargetTemperatureCharacteristic); + this.knxread(this.target_temp_address); // issue a read request on the bus, maybe the device answers to that! + + + knxd_startMonitor({ host: this.knxd_ip, port: this.knxd_port }); + return [informationService, myService]; + } +}; diff --git a/platforms/KNX.js b/platforms/KNX.js index 0b5c659..841c03d 100644 --- a/platforms/KNX.js +++ b/platforms/KNX.js @@ -36,18 +36,29 @@ KNXPlatform.prototype = { for (var int = 0; int < foundAccessories.length; int++) { this.log("parsing acc " + int + " of " + foundAccessories.length); // instantiate and push to array - if (foundAccessories[int].accessory_type === "knxlamp") { - this.log("push new lamp with "+foundAccessories[int].name); - foundAccessories[int].knxd_ip = this.config.knxd_ip; - foundAccessories[int].knxd_port = this.config.knxd_port; - var accConstructor = require('./../accessories/knxlamp.js'); - var acc = new accConstructor.accessory(this.log,foundAccessories[int]); - this.log("created "+acc.name+" accessory"); - myAccessories.push(acc); - } else { - // do something else - this.log("unkown accessory type found") - } //etc. + switch (foundAccessories[int].accessory_type) { + case "knxlamp": + this.log("push new lamp with "+foundAccessories[int].name); + foundAccessories[int].knxd_ip = this.config.knxd_ip; + foundAccessories[int].knxd_port = this.config.knxd_port; + var accConstructor = require('./../accessories/knxlamp.js'); + var acc = new accConstructor.accessory(this.log,foundAccessories[int]); + this.log("created "+acc.name+" accessory"); + myAccessories.push(acc); + break; + case "knxthermostat": + this.log("push new thermostat with "+foundAccessories[int].name); + foundAccessories[int].knxd_ip = this.config.knxd_ip; + foundAccessories[int].knxd_port = this.config.knxd_port; + var accConstructor = require('./../accessories/knxthermostat.js'); + var acc = new accConstructor.accessory(this.log,foundAccessories[int]); + this.log("created "+acc.name+" accessory"); + myAccessories.push(acc); + break; + default: + // do something else + this.log("unkown accessory type found") + } }; // if done, return the array to callback function From fe4cd285d096f6d8d24d5e1bc487df90461f87c6 Mon Sep 17 00:00:00 2001 From: Nick Farina Date: Fri, 4 Sep 2015 10:05:37 -0700 Subject: [PATCH 29/63] Use once() to guard multiple callbacks Mentioned in #95 --- app.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app.js b/app.js index 3f0c275..8a92123 100644 --- a/app.js +++ b/app.js @@ -8,6 +8,7 @@ var Accessory = require('HAP-NodeJS').Accessory; var Service = require('HAP-NodeJS').Service; var Characteristic = require('HAP-NodeJS').Characteristic; var accessoryLoader = require('HAP-NodeJS').AccessoryLoader; +var once = require('HAP-NodeJS/lib/util/once').once; console.log("Starting HomeBridge server..."); @@ -118,7 +119,7 @@ function loadPlatforms() { function loadPlatformAccessories(platformInstance, log) { asyncCalls++; - platformInstance.accessories(function(foundAccessories){ + platformInstance.accessories(once(function(foundAccessories){ asyncCalls--; // loop through accessories adding them to the list and registering them @@ -137,7 +138,7 @@ function loadPlatformAccessories(platformInstance, log) { // were we the last callback? if (asyncCalls === 0 && !asyncWait) publish(); - }); + })); } function createAccessory(accessoryInstance, displayName) { From ea1c1f6fcec6c496ea16ca8fd02ad28989433950 Mon Sep 17 00:00:00 2001 From: Snowdd1 Date: Sat, 5 Sep 2015 19:08:28 +0200 Subject: [PATCH 30/63] Working Version Refactored version. KNX.js is used as platform, while knxdevice is called for each accessory to add. --- accessories/knxdevice.js | 562 +++++++++++++++++++++++++++++++++++ accessories/knxlamp.js | 186 ------------ accessories/knxthermostat.js | 178 ----------- platforms/KNX.js | 276 +++++++++-------- 4 files changed, 699 insertions(+), 503 deletions(-) create mode 100644 accessories/knxdevice.js delete mode 100644 accessories/knxlamp.js delete mode 100644 accessories/knxthermostat.js diff --git a/accessories/knxdevice.js b/accessories/knxdevice.js new file mode 100644 index 0000000..c07e2ac --- /dev/null +++ b/accessories/knxdevice.js @@ -0,0 +1,562 @@ +/* + * This is a KNX universal accessory shim. + * + * + */ +var Service = require("HAP-NodeJS").Service; +var Characteristic = require("HAP-NodeJS").Characteristic; +var knxd = require("eibd"); +var knxd_registerGA = require('../platforms/KNX.js').registerGA; +var knxd_startMonitor = require('../platforms/KNX.js').startMonitor; + + + +function KNXDevice(log, config) { + this.log = log; + // everything in one object, do not copy individually + this.config = config; + log("Accessory constructor called"); + if (config.name) { + this.name = config.name; + } + if (config.knxd_ip){ + this.knxd_ip = config.knxd_ip; + } else { + throw new Error("MISSING KNXD IP"); + } + if (config.knxd_port){ + this.knxd_port = config.knxd_port; + } else { + throw new Error("MISSING KNXD PORT"); + } + +} + + +//debugging helper only +//inspects an object and prints its properties (also inherited properties) +var iterate = function nextIteration(myObject, path){ + // this function iterates over all properties of an object and print them to the console + // when finding objects it goes one level deeper + var name; + if (!path){ + console.log("---iterating--------------------") + } + for (name in myObject) { + if (typeof myObject[name] !== 'function') { + if (typeof myObject[name] !== 'object' ) { + console.log((path || "") + name + ': ' + myObject[name]); + } else { + nextIteration(myObject[name], path ? path + name + "." : name + "."); + } + } else { + console.log((path || "") + name + ': (function)' ); + } + } + if (!path) { + console.log("================================"); + } +}; + + +module.exports = { + accessory: KNXDevice +}; + + +KNXDevice.prototype = { + + // all purpose / all types write function + knxwrite: function(callback, groupAddress, dpt, value) { + // this.log("DEBUG in knxwrite"); + var knxdConnection = new knxd.Connection(); + // this.log("DEBUG in knxwrite: created empty connection, trying to connect socket to "+this.knxd_ip+":"+this.knxd_port); + knxdConnection.socketRemote({ host: this.knxd_ip, port: this.knxd_port }, function() { + var dest = knxd.str2addr(groupAddress); + // this.log("DEBUG got dest="+dest); + knxdConnection.openTGroup(dest, 1, function(err) { + if (err) { + this.log("[ERROR] knxwrite:openTGroup: " + err); + callback(err); + } else { + // this.log("DEBUG opened TGroup "); + var msg = knxd.createMessage('write', dpt, parseFloat(value)); + knxdConnection.sendAPDU(msg, function(err) { + if (err) { + this.log("[ERROR] knxwrite:sendAPDU: " + err); + callback(err); + } else { + // this.log("knx data sent"); + callback(); + } + }.bind(this)); + } + }.bind(this)); + }.bind(this)); + }, + + // issues an all purpose read request on the knx bus + // DOES NOT WAIT for an answer. Please register the address with a callback using registerGA() function + knxread: function(groupAddress){ + // this.log("DEBUG in knxread"); + if (!groupAddress) { + return null; + } + var knxdConnection = new knxd.Connection(); + // this.log("DEBUG in knxread: created empty connection, trying to connect socket to "+this.knxd_ip+":"+this.knxd_port); + knxdConnection.socketRemote({ host: this.knxd_ip, port: this.knxd_port }, function() { + var dest = knxd.str2addr(groupAddress); + // this.log("DEBUG got dest="+dest); + knxdConnection.openTGroup(dest, 1, function(err) { + if (err) { + this.log("[ERROR] knxread:openTGroup: " + err); + } else { + // this.log("DEBUG knxread: opened TGroup "); + var msg = knxd.createMessage('read', 'DPT1', 0); + knxdConnection.sendAPDU(msg, function(err) { + if (err) { + this.log("[ERROR] knxread:sendAPDU: " + err); + } else { + this.log("knx request sent for "+groupAddress); + } + }.bind(this)); + } + }.bind(this)); + }.bind(this)); + }, + + // issuing multiple read requests at once + knxreadarray: function (groupAddresses) { + if (groupAddresses.constructor.toString().indexOf("Array") > -1) { + // handle multiple addresses + for (var i = 0; i < groupAddresses.length; i++) { + if (groupAddresses[i]) { // do not bind empty addresses + this.knxread (groupAddresses[i]); + } + } + } else { + // it's only one + this.knxread (groupAddresses); + } + }, + + // special types + knxwrite_percent: function(callback, groupAddress, value) { + var numericValue = 0; + if (value && value>=0 && value <= 100) { + numericValue = 255*value/100; // convert 1..100 to 1..255 for KNX bus + } else { + this.log("[ERROR] Percentage value ot of bounds "); + numericValue = 0; + } + this.knxwrite(callback, groupAddress,'DPT5',numericValue); + }, + + + // need to spit registers into types + + // boolean: get 0 or 1 from the bus, write boolean + knxregister_bool: function(addresses, characteristic) { + this.log("knx registering BOOLEAN " + addresses); + knxd_registerGA(addresses, function(val, src, dest, type){ + this.log("Received value from bus:"+val+ " for " +dest+ " from "+src+" of type"+type + " for " + characteristic.displayName); +// iterate(characteristic); + characteristic.setValue(val ? 1 : 0, undefined, 'fromKNXBus'); + }.bind(this)); + }, + + // percentage: get 0..255 from the bus, write 0..100 to characteristic + knxregister_percent: function(addresses, characteristic) { + this.log("knx registering PERCENT " + addresses); + knxd_registerGA(addresses, function(val, src, dest, type){ + this.log("Received value from bus:"+val+ " for " +dest+ " from "+src+" of type"+type+ " for " + characteristic.displayName); + if (type !== "DPT5") { + this.log("[ERROR] Received value cannot be a percentage value"); + } else { + characteristic.setValue(Math.round(val/255*100), undefined, 'fromKNXBus'); + } + }.bind(this)); + }, + + // float + knxregister_float: function(addresses, characteristic) { + this.log("knx registering FLOAT " + addresses); + knxd_registerGA(addresses, function(val, src, dest, type){ + this.log("Received value from bus:"+val+ " for " +dest+ " from "+src+" of type"+type+ " for " + characteristic.displayName); + characteristic.setValue(val, undefined, 'fromKNXBus'); + }.bind(this)); + }, + + // what about HVAC heating cooling types? + knxregister_HVAC: function(addresses, characteristic) { + this.log("knx registering HVAC " + addresses); + knxd_registerGA(addresses, function(val, src, dest, type){ + this.log("Received value from bus:"+val+ " for " +dest+ " from "+src+" of type"+type+ " for " + characteristic.displayName); + var HAPvalue = 0; + switch (val){ + case 0: + HAPvalue = 3; + break; + case 1: + HAPvalue = 3; + break; + case 2: + HAPvalue = 3; + break; + case 3: + HAPvalue = 3; + break; + case 4: + HAPvalue = 0; + break; + default: + HAPvalue = 0; + } + characteristic.setValue(HAPvalue, undefined, 'fromKNXBus'); + }.bind(this)); + }, + // to do! KNX: DPT 20.102 = One Byte like DPT5 +// 0 = Auto +// 1 = Comfort +// 2 = Standby +// 3 = Night +// 4 = Freezing/Heat Protection +// 5 – 255 = not allowed” + // The value property of TargetHeatingCoolingState must be one of the following: +// Characteristic.TargetHeatingCoolingState.OFF = 0; +// Characteristic.TargetHeatingCoolingState.HEAT = 1; +// Characteristic.TargetHeatingCoolingState.COOL = 2; +// Characteristic.TargetHeatingCoolingState.AUTO = 3; + + + // undefined, has to match! + knxregister: function(addresses, characteristic) { + this.log("knx registering " + addresses); + knxd_registerGA(addresses, function(val, src, dest, type){ + this.log("Received value from bus:"+val+ " for " +dest+ " from "+src+" of type"+type+ " for " + characteristic.displayName); + characteristic.setValue(val, undefined, 'fromKNXBus'); + }.bind(this)); + }, + + /* + * set methods used for creating callbacks, such as + * var Characteristic = myService.addCharacteristic(new Characteristic.Brightness()) + * .on('set', function(value, callback, context) { + * this.setPercentage(value, callback, context, this.config[index].Set) + * }.bind(this)); + * + */ + setBooleanState: function(value, callback, context, gaddress) { + if (context === 'fromKNXBus') { + this.log(gaddress + " event ping pong, exit!"); + if (callback) { + callback(); + } + } else { + var numericValue = 0; + if (value) { + numericValue = 1; // need 0 or 1, not true or something + } + this.log("Setting "+gaddress+" Boolean to %s", numericValue); + this.knxwrite(callback, gaddress,'DPT1',numericValue); + } + + }, + + + setPercentage: function(value, callback, context, gaddress) { + if (context === 'fromKNXBus') { + this.log("event ping pong, exit!"); + if (callback) { + callback(); + } + } else { + var numericValue = 0; + if (value) { + numericValue = Math.round(255*value/100); // convert 1..100 to 1..255 for KNX bus + } + this.log("Setting "+gaddress+" percentage to %s (%s)", value, numericValue); + this.knxwrite(callback, gaddress,'DPT5',numericValue); + } + }, + + setFloat: function(value, callback, context, gaddress) { + if (context === 'fromKNXBus') { + this.log(gaddress + " event ping pong, exit!"); + if (callback) { + callback(); + } + } else { + var numericValue = 0; + if (value) { + numericValue = value; // need 0 or 1, not true or something + } + this.log("Setting "+gaddress+" Float to %s", numericValue); + this.knxwrite(callback, gaddress,'DPT9',numericValue); + } + }, + + setHVACState: function(value, callback, context, gaddress) { + if (context === 'fromKNXBus') { + this.log(gaddress + " event ping pong, exit!"); + if (callback) { + callback(); + } + } else { + var numericValue = 0; + switch (value){ + case 0: + KNXvalue = 4; + break; + case 1: + KNXvalue = 1; + break; + case 2: + KNXvalue = 1; + break; + case 3: + KNXvalue = 1; + break; + default: + KNXvalue = 1; + } + + this.log("Setting "+gaddress+" HVAC to %s", KNXvalue); + this.knxwrite(callback, gaddress,'DPT5',KNXvalue); + } + + }, + + + identify: function(callback) { + this.log("Identify requested!"); + callback(); // success + }, + + + /* + * function getXXXXXXXService(config) + * + * returns a configured service object to the caller (accessory/device) + * + * the config section is supposed to look like that for the complete device + * "devices": [ + { + "accessory_type": "knxdevice", + "name": "Living Room North Lamp", + "services": [ + { + "type": "Light", + "name": "Living Room North Lamp", + "On": { + "Set": "1/1/6", + "Listen": [ + "1/1/63" + ] + }, + "Brightness": { + "Set": "1/1/62", + "Listen": [ + "1/1/64" + ] + } + } + ] + }, + + * for each service it is supposed to have a + * type + * name + * and service specific objects + */ + + bindCharacteristic: function(myService, characteristicType, valueType, config) { + var myCharacteristic = myService.getCharacteristic(characteristicType); + if (myCharacteristic === undefined) { + throw new Error("unknown characteristics cannot be bound"); + } + if (config.Set) { + // can write + switch (valueType) { + case "Bool": + myCharacteristic.on('set', function(value, callback, context) { +// this.log("ITERATE DEBUG"); +// iterate(config); + this.setBooleanState(value, callback, context, config.Set); + }.bind(this)); + break; + case "Percent": + myCharacteristic.on('set', function(value, callback, context) { + this.setPercentage(value, callback, context, config.Set); + }.bind(this)); + break; + case "Float": + myCharacteristic.on('set', function(value, callback, context) { + this.setFloat(value, callback, context, config.Set); + }.bind(this)); + break; + case "HVAC": + myCharacteristic.on('set', function(value, callback, context) { + this.setHVACState(value, callback, context, config.Set); + }.bind(this)); + break; + default: + this.log("[ERROR] unknown type passed"); + throw new Error("[ERROR] unknown type passed"); + } + } + if ([config.Set].concat(config.Listen || []).length>0) { + this.log("Binding LISTEN"); + // can read + switch (valueType) { + case "Bool": + this.knxregister_bool([config.Set].concat(config.Listen || []), myCharacteristic); + break; + case "Percent": + this.knxregister_percent([config.Set].concat(config.Listen || []), myCharacteristic); + break; + case "Float": + this.knxregister_float([config.Set].concat(config.Listen || []), myCharacteristic); + break; + case "HVAC": + this.knxregister_HVAC([config.Set].concat(config.Listen || []), myCharacteristic); + break; + default: + this.log("[ERROR] unknown type passed"); + throw new Error("[ERROR] unknown type passed"); + } + this.log("Issuing read requests on the KNX bus..."); + this.knxreadarray([config.Set].concat(config.Listen || [])); + } + return myCharacteristic; // for chaining or whatsoever + }, + + getLightbulbService: function(config) { + // some sanity checks + //this.config = config; + + if (config.type !== "Lightbulb") { + this.log("[ERROR] Lightbulb Service for non 'Lightbulb' service called"); + return undefined; + } + if (!config.name) { + this.log("[ERROR] Lightbulb Service without 'name' property called"); + return undefined; + } + var myService = new Service.Lightbulb(config.name,config.name); + // On (and Off) + if (config.On) { + this.log("Lightbulb on/off characteristic enabled"); + this.bindCharacteristic(myService, Characteristic.On, "Bool", config.On); + } // On characteristic + // Brightness if available + if (config.Brightness) { + this.log("Lightbulb Brightness characteristic enabled"); + myService.addCharacteristic(Characteristic.Brightness); // it's an optional + this.bindCharacteristic(myService, Characteristic.Brightness, "Percent", config.Brightness); + } + // Hue and Saturation could be added here if available in KNX lamps + //iterate(myService); + return myService; + }, + + getThermostatService: function(config) { + + +// // Required Characteristics +// this.addCharacteristic(Characteristic.CurrentHeatingCoolingState); +// this.addCharacteristic(Characteristic.TargetHeatingCoolingState); +// this.addCharacteristic(Characteristic.CurrentTemperature); //check +// this.addCharacteristic(Characteristic.TargetTemperature); // +// this.addCharacteristic(Characteristic.TemperatureDisplayUnits); + // +// // Optional Characteristics +// this.addOptionalCharacteristic(Characteristic.CurrentRelativeHumidity); +// this.addOptionalCharacteristic(Characteristic.TargetRelativeHumidity); +// this.addOptionalCharacteristic(Characteristic.CoolingThresholdTemperature); +// this.addOptionalCharacteristic(Characteristic.HeatingThresholdTemperature); + + + // some sanity checks + + + if (config.type !== "Thermostat") { + this.log("[ERROR] Thermostat Service for non 'Thermostat' service called"); + return undefined; + } + if (!config.name) { + this.log("[ERROR] Thermostat Service without 'name' property called"); + return undefined; + } + var myService = new Service.Thermostat(config.name,config.name); + // CurrentTemperature) + if (config.CurrentTemperature) { + this.log("Thermostat CurrentTemperature characteristic enabled"); + this.bindCharacteristic(myService, Characteristic.CurrentTemperature, "Float", config.CurrentTemperature); + } + // TargetTemperature if available + if (config.TargetTemperature) { + this.log("Thermostat TargetTemperature characteristic enabled"); + this.bindCharacteristic(myService, Characteristic.TargetTemperature, "Float", config.TargetTemperature); + } + // HVAC missing yet + if (config.CurrentHeatingCoolingState) { + this.log("Thermostat CurrentHeatingCoolingState characteristic enabled"); + this.bindCharacteristic(myService, Characteristic.CurrentHeatingCoolingState, "HVAC", config.CurrentHeatingCoolingState); + } + return myService; + }, + + + + /* assemble the device ***************************************************************************************************/ + + + 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 accessoryServices = []; + + var informationService = new Service.AccessoryInformation(); + + informationService + .setCharacteristic(Characteristic.Manufacturer, "Opensource Community") + .setCharacteristic(Characteristic.Model, "KNX Universal Device") + .setCharacteristic(Characteristic.SerialNumber, "Version 1.1"); + + accessoryServices.push(informationService); + + iterate(this.config); +// throw new Error("STOP"); + if (!this.config.services){ + this.log("No services found in accessory?!") + } + var currServices = this.config.services; + this.log("Preparing Services: " + currServices.length) + // go through the config thing and look for services + for (var int = 0; int < currServices.length; int++) { + var configService = currServices[int]; + // services need to have type and name properties + if (!configService.type && !configService.name) { + this.log("[ERROR] must specify 'type' and 'name' properties for each service in config.json. KNX platform section fault "); + throw new Error("Must specify 'type' and 'name' properties for each service in config.json"); + } + switch (configService.type) { + case "Lightbulb": + accessoryServices.push(this.getLightbulbService(configService)); + break; + case "Thermostat": + accessoryServices.push(this.getThermostatService(configService)); + break; + default: + this.log("[ERROR] unknown 'type' property for service "+ configService.name + " in config.json. KNX platform section fault "); + //throw new Error("[ERROR] unknown 'type' property for service "+ configService.name + " in config.json. KNX platform section fault "); + } + } + // start listening for events on the bus (if not started yet - will prevent itself) + knxd_startMonitor({ host: this.knxd_ip, port: this.knxd_port }); + return accessoryServices; + } +}; diff --git a/accessories/knxlamp.js b/accessories/knxlamp.js deleted file mode 100644 index 7f06529..0000000 --- a/accessories/knxlamp.js +++ /dev/null @@ -1,186 +0,0 @@ -/* - * This is a demo KNX thermostat accessory shim. - * It can - * - */ -var Service = require("HAP-NodeJS").Service; -var Characteristic = require("HAP-NodeJS").Characteristic; -var knxd = require("eibd"); -var knxd_registerGA = require('../platforms/KNX.js').registerGA; -var knxd_startMonitor = require('../platforms/KNX.js').startMonitor; - - - -function KNXlampAccessory(log, config) { - this.log = log; - - - // knx information on object - this.group_address = config.group_address; - this.listen_addresses = config.listen_addresses; // supposed to be undefined, an array of strings, or single string - this.can_dim = config.can_dim; //supposed to be true or false - this.brightness_group_address = config.brightness_group_address; - this.brightness_listen_addresses = config.brightness_listen_addresses; - this.knxd_ip = config.knxd_ip ; // eg 127.0.0.1 if running on localhost - this.knxd_port = config.knxd_port || 6720; // eg 6720 default knxd port - if (config.name) { - this.name = config.name; - } - log("Accessory constructor called"); - -} - - -module.exports = { - accessory: KNXlampAccessory - }; - - -KNXlampAccessory.prototype = { - - - knxwrite: function(callback, groupAddress, dpt, value) { - // this.log("DEBUG in knxwrite"); - var knxdConnection = new knxd.Connection(); - // this.log("DEBUG in knxwrite: created empty connection, trying to connect socket to "+this.knxd_ip+":"+this.knxd_port); - knxdConnection.socketRemote({ host: this.knxd_ip, port: this.knxd_port }, function() { - var dest = knxd.str2addr(groupAddress); - // this.log("DEBUG got dest="+dest); - knxdConnection.openTGroup(dest, 1, function(err) { - if (err) { - this.log("[ERROR] knxwrite:openTGroup: " + err); - callback(err); - } else { - // this.log("DEBUG opened TGroup "); - var msg = knxd.createMessage('write', dpt, parseFloat(value)); - knxdConnection.sendAPDU(msg, function(err) { - if (err) { - this.log("[ERROR] knxwrite:sendAPDU: " + err); - callback(err); - } else { - // this.log("knx data sent"); - callback(); - } - }.bind(this)); - } - }.bind(this)); - }.bind(this)); - }, - - // issues a read request on the knx bus - // DOES NOT WAIT for an answer. Please register the address with a callback using registerGA() function - knxread: function(groupAddress){ - // this.log("DEBUG in knxread"); - var knxdConnection = new knxd.Connection(); - // this.log("DEBUG in knxread: created empty connection, trying to connect socket to "+this.knxd_ip+":"+this.knxd_port); - knxdConnection.socketRemote({ host: this.knxd_ip, port: this.knxd_port }, function() { - var dest = knxd.str2addr(groupAddress); - // this.log("DEBUG got dest="+dest); - knxdConnection.openTGroup(dest, 1, function(err) { - if (err) { - this.log("[ERROR] knxread:openTGroup: " + err); - } else { - // this.log("DEBUG knxread: opened TGroup "); - var msg = knxd.createMessage('read', 'DPT1', 0); - knxdConnection.sendAPDU(msg, function(err) { - if (err) { - this.log("[ERROR] knxread:sendAPDU: " + err); - } else { - this.log("knx request sent"); - } - }.bind(this)); - } - }.bind(this)); - }.bind(this)); - }, - - knxregister: function(addresses, characteristic) { - console.log("knx registering " + addresses); - knxd_registerGA(addresses, function(value){ - // parameters do not match - this.log("Getting value from bus:"+value); - characteristic.setValue(value, undefined, 'fromKNXBus'); - }.bind(this)); - }, - - setPowerState: function(value, callback, context) { - if (context === 'fromKNXBus') { - this.log("event ping pong, exit!"); - if (callback) { - callback(); - } - } else { - console.log("Setting power to %s", value); - var numericValue = 0; - if (value) { - numericValue = 1; // need 0 or 1, not true or something - } - this.knxwrite(callback, this.group_address,'DPT1',numericValue); - } - - }, - - - setBrightness: function(value, callback, context) { - if (context === 'fromKNXBus') { - this.log("event ping pong, exit!"); - if (callback) { - callback(); - } - } else { - this.log("Setting brightness to %s", value); - var numericValue = 0; - if (value) { - numericValue = 255*value/100; // convert 1..100 to 1..255 for KNX bus - } - this.knxwrite(callback, this.brightness_group_address,'DPT5',numericValue); - } - }, - - - - identify: function(callback) { - this.log("Identify requested!"); - callback(); // success - }, - - 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, "Opensource Community") - .setCharacteristic(Characteristic.Model, "KNX Light Switch with or without dimmer") - .setCharacteristic(Characteristic.SerialNumber, "Version 1"); - - var lightbulbService = new Service.Lightbulb(); - - var onCharacteristic = lightbulbService - .getCharacteristic(Characteristic.On) - .on('set', this.setPowerState.bind(this)); - onCharacteristic.supportsEventNotification=true; - // register with value update service - this.addresses = [this.group_address]; - this.log("DEBUG1 this.addresses = "+this.addresses); - this.log("DEBUG2 this.listen_addresses = "+this.listen_addresses); - this.addresses = this.addresses.concat(this.listen_addresses || []); // do not join anything if empty (do not add undefined) - this.log("DEBUG3 this.addresses = "+this.addresses); - this.knxregister(this.addresses, onCharacteristic); - this.knxread(this.group_address); // issue a read request on the bus, maybe the device answers to that! - - if (this.can_dim) { - var brightnessCharacteristic = lightbulbService - .addCharacteristic(new Characteristic.Brightness()) - .on('set', this.setBrightness.bind(this)); - // register with value update service - this.brightness_addresses = [this.brightness_group_address]; - this.brightness_addresses.concat(this.brightness_listen_addresses || []); // do not join anything if empty (do not add undefined) - this.knxregister(this.brightness_addresses, brightnessCharacteristic); - this.knxread(this.brightness_group_address); // issue a read request on the bus, maybe the device answers to that! - } - knxd_startMonitor({ host: this.knxd_ip, port: this.knxd_port }); - return [informationService, lightbulbService]; - } -}; diff --git a/accessories/knxthermostat.js b/accessories/knxthermostat.js deleted file mode 100644 index e0f408a..0000000 --- a/accessories/knxthermostat.js +++ /dev/null @@ -1,178 +0,0 @@ -/* - * This is a demo KNX lamp accessory shim. - * It can switch a light on and off, and optionally set a brightness if configured to do so - * - */ -var Service = require("HAP-NodeJS").Service; -var Characteristic = require("HAP-NodeJS").Characteristic; -var knxd = require("eibd"); -var knxd_registerGA = require('../platforms/KNX.js').registerGA; -var knxd_startMonitor = require('../platforms/KNX.js').startMonitor; - - - -function KNXthermoAccessory(log, config) { - this.log = log; - this.config=config; - - // knx information on object - this.curr_temp_address = config.curr_temp_address; - this.curr_temp_listen_addresses = config.curr_temp_listen_addresses; // supposed to be undefined, an array of strings, or single string - this.target_temp_address = config.target_temp_address; - this.knxd_ip = config.knxd_ip ; // eg 127.0.0.1 if running on localhost - this.knxd_port = config.knxd_port || 6720; // eg 6720 default knxd port - if (config.name) { - this.name = config.name; - } - log("Accessory constructor called"); - -} - - -module.exports = { - accessory: KNXthermoAccessory - }; - - -KNXthermoAccessory.prototype = { - - - knxwrite: function(callback, groupAddress, dpt, value) { - // this.log("DEBUG in knxwrite"); - var knxdConnection = new knxd.Connection(); - // this.log("DEBUG in knxwrite: created empty connection, trying to connect socket to "+this.knxd_ip+":"+this.knxd_port); - knxdConnection.socketRemote({ host: this.knxd_ip, port: this.knxd_port }, function() { - var dest = knxd.str2addr(groupAddress); - // this.log("DEBUG got dest="+dest); - knxdConnection.openTGroup(dest, 1, function(err) { - if (err) { - this.log("[ERROR] knxwrite:openTGroup: " + err); - callback(err); - } else { - // this.log("DEBUG opened TGroup "); - var msg = knxd.createMessage('write', dpt, parseFloat(value)); - knxdConnection.sendAPDU(msg, function(err) { - if (err) { - this.log("[ERROR] knxwrite:sendAPDU: " + err); - callback(err); - } else { - this.log("knx data sent"); - callback(); - } - }.bind(this)); - } - }.bind(this)); - }.bind(this)); - }, - - // issues a read request on the knx bus - // DOES NOT WAIT for an answer. Please register the address with a callback using registerGA() function - knxread: function(groupAddress){ - // this.log("DEBUG in knxread"); - var knxdConnection = new knxd.Connection(); - // this.log("DEBUG in knxread: created empty connection, trying to connect socket to "+this.knxd_ip+":"+this.knxd_port); - knxdConnection.socketRemote({ host: this.knxd_ip, port: this.knxd_port }, function() { - var dest = knxd.str2addr(groupAddress); - // this.log("DEBUG got dest="+dest); - knxdConnection.openTGroup(dest, 1, function(err) { - if (err) { - this.log("[ERROR] knxread:openTGroup: " + err); - } else { - // this.log("DEBUG knxread: opened TGroup "); - var msg = knxd.createMessage('read', 'DPT1', 0); - knxdConnection.sendAPDU(msg, function(err) { - if (err) { - this.log("[ERROR] knxread:sendAPDU: " + err); - } else { - this.log("knx request sent"); - } - }.bind(this)); - } - }.bind(this)); - }.bind(this)); - }, - - knxregister: function(addresses, characteristic) { - console.log("knx registering " + addresses); - knxd_registerGA(addresses, function(value){ - // parameters do not match - this.log("Getting value from bus:"+value); - characteristic.setValue(value, undefined, 'fromKNXBus'); - }.bind(this)); - }, - - - - identify: function(callback) { - this.log("Identify requested!"); - callback(); // success - }, - - 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, "Opensource Community") - .setCharacteristic(Characteristic.Model, "KNX Thermostat") - .setCharacteristic(Characteristic.SerialNumber, "Version 1"); - - var myService = new Service.Thermostat(); - -// -// // Required Characteristics -// this.addCharacteristic(Characteristic.CurrentHeatingCoolingState); -// this.addCharacteristic(Characteristic.TargetHeatingCoolingState); -// this.addCharacteristic(Characteristic.CurrentTemperature); //check -// this.addCharacteristic(Characteristic.TargetTemperature); // -// this.addCharacteristic(Characteristic.TemperatureDisplayUnits); -// -// // Optional Characteristics -// this.addOptionalCharacteristic(Characteristic.CurrentRelativeHumidity); -// this.addOptionalCharacteristic(Characteristic.TargetRelativeHumidity); -// this.addOptionalCharacteristic(Characteristic.CoolingThresholdTemperature); -// this.addOptionalCharacteristic(Characteristic.HeatingThresholdTemperature); -// this.addOptionalCharacteristic(Characteristic.Name); - - - var CurrentTemperatureCharacteristic = myService - .getCharacteristic(Characteristic.CurrentTemperature) - // .on('set', this.setPowerState.bind(this)); - CurrentTemperatureCharacteristic.supportsEventNotification=true; - // register with value update service - this.addresses1 = [this.curr_temp_address]; - this.addresses1 = this.addresses1.concat(this.curr_temp_listen_addresses || []); // do not join anything if empty (do not add undefined) - this.knxregister(this.addresses1, CurrentTemperatureCharacteristic); - this.knxread(this.curr_temp_address); // issue a read request on the bus, maybe the device answers to that! - - var TargetTemperatureCharacteristic = myService - .getCharacteristic(Characteristic.TargetTemperature) - .on('set', function(value, callback, context) { - if (context === 'fromKNXBus') { - this.log("event ping pong, exit!"); - if (callback) { - callback(); - } - } else { - console.log("Setting temperature to %s", value); - var numericValue = 0.0; - if (value) { - numericValue = 0+value; // need to be numeric - } - this.knxwrite(callback, this.target_temp_address,'DPT9',numericValue); - } - }.bind(this)); - TargetTemperatureCharacteristic.supportsEventNotification=true; - // register with value update service - this.addresses2 = [this.target_temp_address]; - this.addresses2 = this.addresses2.concat(this.target_temp_listen_addresses || []); // do not join anything if empty (do not add undefined) - this.knxregister(this.addresses2, TargetTemperatureCharacteristic); - this.knxread(this.target_temp_address); // issue a read request on the bus, maybe the device answers to that! - - - knxd_startMonitor({ host: this.knxd_ip, port: this.knxd_port }); - return [informationService, myService]; - } -}; diff --git a/platforms/KNX.js b/platforms/KNX.js index 841c03d..7070137 100644 --- a/platforms/KNX.js +++ b/platforms/KNX.js @@ -7,83 +7,75 @@ var types = require("HAP-NodeJS/accessories/types.js"); var knxd = require('eibd'); function KNXPlatform(log, config){ - this.log = log; - this.config = config; -// this.property1 = config.property1; -// this.property2 = config.property2; - - - // initiate connection to bus for listening ==> done with first shim - + this.log = log; + this.config = config; +// this.property1 = config.property1; +// this.property2 = config.property2; + + + // initiate connection to bus for listening ==> done with first shim + }; KNXPlatform.prototype = { - accessories: function(callback) { - this.log("Fetching KNX devices."); - var that = this; - - - // iterate through all devices the platform my offer - // for each device, create an accessory - - // read accessories from file !!!!! - var foundAccessories = this.config.accessories; - - - //create array of accessories - var myAccessories = []; - - for (var int = 0; int < foundAccessories.length; int++) { - this.log("parsing acc " + int + " of " + foundAccessories.length); - // instantiate and push to array - switch (foundAccessories[int].accessory_type) { - case "knxlamp": - this.log("push new lamp with "+foundAccessories[int].name); - foundAccessories[int].knxd_ip = this.config.knxd_ip; - foundAccessories[int].knxd_port = this.config.knxd_port; - var accConstructor = require('./../accessories/knxlamp.js'); - var acc = new accConstructor.accessory(this.log,foundAccessories[int]); - this.log("created "+acc.name+" accessory"); - myAccessories.push(acc); - break; - case "knxthermostat": - this.log("push new thermostat with "+foundAccessories[int].name); - foundAccessories[int].knxd_ip = this.config.knxd_ip; - foundAccessories[int].knxd_port = this.config.knxd_port; - var accConstructor = require('./../accessories/knxthermostat.js'); - var acc = new accConstructor.accessory(this.log,foundAccessories[int]); - this.log("created "+acc.name+" accessory"); - myAccessories.push(acc); - break; - default: - // do something else - this.log("unkown accessory type found") - } - - }; - // if done, return the array to callback function - this.log("returning "+myAccessories.length+" accessories"); - callback(myAccessories); - } + accessories: function(callback) { + this.log("Fetching KNX devices."); + var that = this; + + + // iterate through all devices the platform my offer + // for each device, create an accessory + + // read accessories from file !!!!! + var foundAccessories = this.config.accessories; + + + //create array of accessories + var myAccessories = []; + + for (var int = 0; int < foundAccessories.length; int++) { + this.log("parsing acc " + int + " of " + foundAccessories.length); + // instantiate and push to array + switch (foundAccessories[int].accessory_type) { + case "knxdevice": + this.log("push new universal device "+foundAccessories[int].name); + // push knxd connection setting to each device from platform + foundAccessories[int].knxd_ip = this.config.knxd_ip; + foundAccessories[int].knxd_port = this.config.knxd_port; + var accConstructor = require('./../accessories/knxdevice.js'); + var acc = new accConstructor.accessory(this.log,foundAccessories[int]); + this.log("created "+acc.name+" universal accessory"); + myAccessories.push(acc); + break; + case "knxlamp": + this.log("push new lamp with "+foundAccessories[int].name ); + foundAccessories[int].knxd_ip = this.config.knxd_ip; + foundAccessories[int].knxd_port = this.config.knxd_port; + var accConstructor = require('./../accessories/knxlamp.js'); + var acc = new accConstructor.accessory(this.log,foundAccessories[int]); + this.log("created "+acc.name+" accessory"); + myAccessories.push(acc); + break; + case "knxthermostat": + this.log("push new thermostat with "+foundAccessories[int].name); + foundAccessories[int].knxd_ip = this.config.knxd_ip; + foundAccessories[int].knxd_port = this.config.knxd_port; + var accConstructor = require('./../accessories/knxthermostat.js'); + var acc = new accConstructor.accessory(this.log,foundAccessories[int]); + this.log("created "+acc.name+" accessory"); + myAccessories.push(acc); + break; + default: + // do something else + this.log("unkown accessory type found") + } + + }; + // if done, return the array to callback function + this.log("returning "+myAccessories.length+" accessories"); + callback(myAccessories); + } }; - - -// the signature of the constructor has to be adopted to the accessory you need in your platform! These are the first lines from the sonos platform -function myAccessoryType1(log, config, device, description /* add or remove parms as you need*/ ) { - - this.log = log; - this.config = config; - this.device = device; - this.description = description; - // more initialization if required - -} - -myAccessoryType1.prototype = { - // see https shim wiki page for details. Accessory definition is discussed there. -} - -// more /** @@ -91,47 +83,47 @@ myAccessoryType1.prototype = { * of registered addresses. * * Usage: -* You can start the monitoring process at any time + * You can start the monitoring process at any time startMonitor({host: name-ip, port: port-num }); - -* You can add addresses to the subscriptions using - + + * You can add addresses to the subscriptions using + registerGA(groupAddress, callback) - -* groupAddress has to be an groupAddress in common knx notation string '1/2/3' -* the callback has to be a -* var f = function(value) { handle value update;} -* so you can do a -* registerGA('1/2/3', function(value){ -* console.log('1/2/3 got a hit with '+value); -* }); -* but of course it is meant to be used programmatically, not literally, otherwise it has no advantage -* -* You can also use arrays of addresses if your callback is supposed to listen to many addresses: + + * groupAddress has to be an groupAddress in common knx notation string '1/2/3' + * the callback has to be a + * var f = function(value) { handle value update;} + * so you can do a + * registerGA('1/2/3', function(value){ + * console.log('1/2/3 got a hit with '+value); + * }); + * but of course it is meant to be used programmatically, not literally, otherwise it has no advantage + * + * You can also use arrays of addresses if your callback is supposed to listen to many addresses: registerGA(groupAddresses[], callback) -* as in -* registerGA(['1/2/3','1/0/0'], function(value){ -* console.log('1/2/3 or 1/0/0 got a hit with '+value); -* }); -* if you are having central addresses like "all lights off" or additional response objects -* -* -* callbacks can have a signature of -* function(value, src, dest, type) but do not have to support these parameters (order matters) -* src = physical address such as '1.1.20' -* dest = groupAddress hit (you subscribed to that address, remember?), as '1/2/3' -* type = Data point type, as 'DPT1' -* -* -*/ + * as in + * registerGA(['1/2/3','1/0/0'], function(value){ + * console.log('1/2/3 or 1/0/0 got a hit with '+value); + * }); + * if you are having central addresses like "all lights off" or additional response objects + * + * + * callbacks can have a signature of + * function(value, src, dest, type) but do not have to support these parameters (order matters) + * src = physical address such as '1.1.20' + * dest = groupAddress hit (you subscribed to that address, remember?), as '1/2/3' + * type = Data point type, as 'DPT1' + * + * + */ -// array of registered addresses and their callbacks +//array of registered addresses and their callbacks var subscriptions = []; -// check variable to avoid running two listeners +//check variable to avoid running two listeners var running; function groupsocketlisten(opts, callback) { @@ -145,7 +137,7 @@ function groupsocketlisten(opts, callback) { var registerSingleGA = function registerSingleGA (groupAddress, callback) { subscriptions.push({address: groupAddress, callback: callback }); } - + /* * public busMonitor.startMonitor() * starts listening for telegrams on KNX bus @@ -155,42 +147,45 @@ var startMonitor = function startMonitor(opts) { // using { host: name-ip, port if (!running) { running = true; } else { + console.log("<< knxd socket listener already running >>"); return null; } - - groupsocketlisten(opts, function(parser) { - //console.log("knxfunctions.read: in callback parser"); - parser.on('write', function(src, dest, type, val){ - // search the registered group addresses - for (var i = 0; i < subscriptions.length; i++) { - // iterate through all registered addresses - if (subscriptions[i].address === dest) { - // found one, notify - //console.log('HIT: Write from '+src+' to '+dest+': '+val+' ['+type+']'); - subscriptions[i].callback(val, src, dest, type); - } - } - }); + console.log(">>> knxd groupsocketlisten starting <<<"); + groupsocketlisten(opts, function(parser) { + //console.log("knxfunctions.read: in callback parser"); + parser.on('write', function(src, dest, type, val){ + // search the registered group addresses + //console.log('recv: Write from '+src+' to '+dest+': '+val+' ['+type+'], listeners:' + subscriptions.length); + for (var i = 0; i < subscriptions.length; i++) { + // iterate through all registered addresses + if (subscriptions[i].address === dest) { + // found one, notify + console.log('HIT: Write from '+src+' to '+dest+': '+val+' ['+type+']'); + subscriptions[i].callback(val, src, dest, type); + } + } + }); - parser.on('response', function(src, dest, type, val) { - // search the registered group addresses - for (var i = 0; i < subscriptions.length; i++) { - // iterate through all registered addresses - if (subscriptions[i].address === dest) { - // found one, notify - //console.log('HIT: Response from '+src+' to '+dest+': '+val+' ['+type+']'); - subscriptions[i].callback(val, src, dest, type); - } - } - - }); - - //dont care about reads here -// parser.on('read', function(src, dest) { -// console.log('Read from '+src+' to '+dest); -// }); + parser.on('response', function(src, dest, type, val) { + // search the registered group addresses +// console.log('recv: resp from '+src+' to '+dest+': '+val+' ['+type+']'); + for (var i = 0; i < subscriptions.length; i++) { + // iterate through all registered addresses + if (subscriptions[i].address === dest) { + // found one, notify +// console.log('HIT: Response from '+src+' to '+dest+': '+val+' ['+type+']'); + subscriptions[i].callback(val, src, dest, type); + } + } + + }); + + //dont care about reads here +// parser.on('read', function(src, dest) { +// console.log('Read from '+src+' to '+dest); +// }); //console.log("knxfunctions.read: in callback parser at end"); - }); // groupsocketlisten parser + }); // groupsocketlisten parser }; //startMonitor @@ -208,12 +203,15 @@ var registerGA = function (groupAddresses, callback) { if (groupAddresses.constructor.toString().indexOf("Array") > -1) { // handle multiple addresses for (var i = 0; i < groupAddresses.length; i++) { - registerSingleGA (groupAddresses[i], callback); + if (groupAddresses[i]) { // do not bind empty addresses + registerSingleGA (groupAddresses[i], callback); + } } } else { // it's only one registerSingleGA (groupAddresses, callback); } +// console.log("listeners now: " + subscriptions.length); }; From 64635833d6f68ba5743b5b8f90090147bf52ac3b Mon Sep 17 00:00:00 2001 From: Snowdd1 Date: Sat, 5 Sep 2015 20:11:06 +0200 Subject: [PATCH 31/63] Sample config file config-sample-knx.jsnon --- config-sample-knx.json | 88 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 config-sample-knx.json diff --git a/config-sample-knx.json b/config-sample-knx.json new file mode 100644 index 0000000..8e7d11e --- /dev/null +++ b/config-sample-knx.json @@ -0,0 +1,88 @@ +{ + "bridge": { + "name": "Homebridge", + "username": "CC:22:3D:E3:CE:30", + "port": 51826, + "pin": "031-45-154" + }, + "description": "This is an example configuration file for KNX platform shim", + "platforms": + [ + { + "platform": "KNX", + "name": "KNX", + "knxd_ip": "192.168.178.205", + "knxd_port": 6720, + "accessories": + [ + { + "accessory_type": "knxdevice", + "name": "Living Room North Lamp", + "services": + [ + { + "type": "Lightbulb", + "name": "Living Room North Lamp", + "On": { + "Set": "1/1/6", + "Listen": [ + "1/1/63" + ] + }, + "Brightness": { + "Set": "1/1/62", + "Listen": [ + "1/1/64" + ] + } + } + ] + }, + { + "accessory_type": "knxdevice", + "name": "Office", + "services": + [ + { + "type": "Lightbulb", + "name": "Office Lamp", + "On": { + "Set": "1/3/5" + } + }, + { + "type": "Thermostat", + "name": "Raumtemperatur", + "CurrentTemperature": { + "Listen": "3/3/44" + }, + "TargetTemperature": { + "Set": "3/3/94" + }, + "CurrentHeatingCoolingState": { + "Listen": "3/3/64" + } + }, + { + "type": "WindowCovering", + "name": "Rollo", + "Target": { + "Set": "address", + "Listen": "adresses" + }, + "Current": { + "Set": "address", + "Listen": "adresses" + }, + "PositionState": { + "Listen": "adresses" + } + } + ] + } + ] + } + ], + "accessories": [], + "prototyping": {} +} \ No newline at end of file From e2ef8fc0b6d71274477af7adde55c4f4da5f3055 Mon Sep 17 00:00:00 2001 From: Snowdd1 Date: Sat, 5 Sep 2015 21:31:48 +0200 Subject: [PATCH 32/63] package.json - removed subtypes to be passed by default. - package.json to inbclude "eibd" --- accessories/knxdevice.js | 37 ++++--------------------------------- package.json | 1 + 2 files changed, 5 insertions(+), 33 deletions(-) diff --git a/accessories/knxdevice.js b/accessories/knxdevice.js index c07e2ac..ffcb50e 100644 --- a/accessories/knxdevice.js +++ b/accessories/knxdevice.js @@ -339,35 +339,6 @@ KNXDevice.prototype = { * * returns a configured service object to the caller (accessory/device) * - * the config section is supposed to look like that for the complete device - * "devices": [ - { - "accessory_type": "knxdevice", - "name": "Living Room North Lamp", - "services": [ - { - "type": "Light", - "name": "Living Room North Lamp", - "On": { - "Set": "1/1/6", - "Listen": [ - "1/1/63" - ] - }, - "Brightness": { - "Set": "1/1/62", - "Listen": [ - "1/1/64" - ] - } - } - ] - }, - - * for each service it is supposed to have a - * type - * name - * and service specific objects */ bindCharacteristic: function(myService, characteristicType, valueType, config) { @@ -406,7 +377,7 @@ KNXDevice.prototype = { } } if ([config.Set].concat(config.Listen || []).length>0) { - this.log("Binding LISTEN"); + //this.log("Binding LISTEN"); // can read switch (valueType) { case "Bool": @@ -443,7 +414,7 @@ KNXDevice.prototype = { this.log("[ERROR] Lightbulb Service without 'name' property called"); return undefined; } - var myService = new Service.Lightbulb(config.name,config.name); + var myService = new Service.Lightbulb() //(config.name,config.name); // On (and Off) if (config.On) { this.log("Lightbulb on/off characteristic enabled"); @@ -477,7 +448,7 @@ KNXDevice.prototype = { // this.addOptionalCharacteristic(Characteristic.HeatingThresholdTemperature); - // some sanity checks + // some sanity checks if (config.type !== "Thermostat") { @@ -488,7 +459,7 @@ KNXDevice.prototype = { this.log("[ERROR] Thermostat Service without 'name' property called"); return undefined; } - var myService = new Service.Thermostat(config.name,config.name); + var myService = new Service.Thermostat() //(config.name,config.name); // CurrentTemperature) if (config.CurrentTemperature) { this.log("Thermostat CurrentTemperature characteristic enabled"); diff --git a/package.json b/package.json index e7fd564..9ee336f 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "carwingsjs": "0.0.x", "color": "0.10.x", "elkington": "kevinohara80/elkington", + "eibd": "0.3.1", "hap-nodejs": "git+https://github.com/KhaosT/HAP-NodeJS#fff863d7a387636fc612cf27cb859e82d9ee3294", "harmonyhubjs-client": "^1.1.4", "harmonyhubjs-discover": "git+https://github.com/swissmanu/harmonyhubjs-discover.git", From 816728c0cf1957518a343118d5f658c1101b5128 Mon Sep 17 00:00:00 2001 From: Nick Farina Date: Sat, 5 Sep 2015 12:42:21 -0700 Subject: [PATCH 33/63] Upgrade WeMo accessory - Use new Service API - Experimental preliminary support for WeMo "Garage Door Openers" (via WeMo Maker). No Sensor support yet. --- accessories/WeMo.js | 261 +++++++++++++++++++------------------------- 1 file changed, 110 insertions(+), 151 deletions(-) diff --git a/accessories/WeMo.js b/accessories/WeMo.js index 273e4b3..d8e713d 100644 --- a/accessories/WeMo.js +++ b/accessories/WeMo.js @@ -1,166 +1,125 @@ -var types = require("HAP-NodeJS/accessories/types.js"); +var Service = require("HAP-NodeJS").Service; +var Characteristic = require("HAP-NodeJS").Characteristic; var wemo = require('wemo'); -// extend our search timeout from 5 seconds to 60 -wemo.SearchTimeout = 60000; -wemo.timeout = wemo.SearchTimeout // workaround for a bug in wemo.js v0.0.4 +module.exports = { + accessory: WeMoAccessory +} function WeMoAccessory(log, config) { this.log = log; - this.name = config["name"]; - this.wemoName = config["wemo_name"]; - this.device = null; + this.service = config["service"] || "Switch"; + this.wemoName = config["wemo_name"] || config["name"]; // fallback to "name" if you didn't specify an exact "wemo_name" + this.device = null; // instance of WeMo, for controlling the discovered device this.log("Searching for WeMo device with exact name '" + this.wemoName + "'..."); this.search(); } -WeMoAccessory.prototype = { - - search: function() { - var that = this; - - wemo.Search(this.wemoName, function(err, device) { - if (!err && device) { - that.log("Found '"+that.wemoName+"' device at " + device.ip); - that.device = new wemo(device.ip, device.port); - } - else { - that.log("Error finding device '" + that.wemoName + "': " + err); - that.log("Continuing search for WeMo device with exact name '" + that.wemoName + "'..."); - that.search(); - } - }); - }, - - setPowerState: function(powerOn) { - - if (!this.device) { - this.log("No '"+this.wemoName+"' device found (yet?)"); - return; +WeMoAccessory.prototype.search = function() { + wemo.Search(this.wemoName, function(err, device) { + if (!err && device) { + this.log("Found '"+this.wemoName+"' device at " + device.ip); + this.device = new wemo(device.ip, device.port); } - - var binaryState = powerOn ? 1 : 0; - var that = this; - - this.log("Setting power state on the '"+this.wemoName+"' to " + binaryState); - - this.device.setBinaryState(binaryState, function(err, result) { - if (!err) { - that.log("Successfully set power state on the '"+that.wemoName+"' to " + binaryState); - } - else { - that.log("Error setting power state on the '"+that.wemoName+"'") - } - }); - }, - - getPowerState: function(callback) { - - if (!this.device) { - this.log("No '"+this.wemoName+"' device found (yet?)"); - return; + else { + this.log("Error finding device '" + this.wemoName + "': " + err); + this.log("Continuing search for WeMo device with exact name '" + this.wemoName + "'..."); + this.search(); } + }.bind(this)); +} - var that = this; +WeMoAccessory.prototype.getPowerOn = function(callback) { - this.log("checking power state for: " + this.wemoName); - this.device.getBinaryState(function(err, result) { - if (!err) { - var binaryState = parseInt(result) - that.log("power state for " + that.wemoName + " is: " + binaryState) - callback(binaryState > 0 ? 1 : 0); - } - else { - that.log(err) - } - }); - }, - - 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: "WeMo", - 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.SWITCH_STYPE, - characteristics: [{ - cType: types.NAME_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: this.name, - supportEvents: false, - supportBonjour: false, - manfDescription: "Name of service", - designedMaxLength: 255 - },{ - cType: types.POWER_STATE_CTYPE, - onUpdate: function(value) { that.setPowerState(value); }, - onRead: function(callback) { - that.getPowerState(function(powerState){ - callback(powerState); - }); - }, - perms: ["pw","pr","ev"], - format: "bool", - initialValue: false, - supportEvents: false, - supportBonjour: false, - manfDescription: "Change the power state of the WeMo", - designedMaxLength: 1 - }] - }]; + if (!this.device) { + this.log("No '%s' device found (yet?)", this.wemoName); + callback(new Error("Device not found")); + return; } -}; -module.exports.accessory = WeMoAccessory; + this.log("Getting power state on the '%s'...", this.wemoName); + + this.device.getBinaryState(function(err, result) { + if (!err) { + var binaryState = parseInt(result); + var powerOn = binaryState > 0; + this.log("Power state for the '%s' is %s", this.wemoName, binaryState); + callback(null, powerOn); + } + else { + this.log("Error getting power state on the '%s': %s", this.wemoName, err.message); + callback(err); + } + }.bind(this)); +} + +WeMoAccessory.prototype.setPowerOn = function(powerOn, callback) { + + if (!this.device) { + this.log("No '%s' device found (yet?)", this.wemoName); + callback(new Error("Device not found")); + return; + } + + var binaryState = powerOn ? 1 : 0; // wemo langauge + this.log("Setting power state on the '%s' to %s", this.wemoName, binaryState); + + this.device.setBinaryState(binaryState, function(err, result) { + if (!err) { + this.log("Successfully set power state on the '%s' to %s", this.wemoName, binaryState); + callback(null); + } + else { + this.log("Error setting power state to %s on the '%s'", binaryState, this.wemoName); + callback(err); + } + }.bind(this)); +} + +WeMoAccessory.prototype.setTargetDoorState = function(targetDoorState, callback) { + + if (!this.device) { + this.log("No '%s' device found (yet?)", this.wemoName); + callback(new Error("Device not found")); + return; + } + + this.log("Activating WeMo switch '%s'", this.wemoName); + + this.device.setBinaryState(1, function(err, result) { + if (!err) { + this.log("Successfully activated WeMo switch '%s'", this.wemoName); + callback(null); + } + else { + this.log("Error activating WeMo switch '%s'", this.wemoName); + callback(err); + } + }.bind(this)); +} + +WeMoAccessory.prototype.getServices = function() { + + if (this.service == "Switch") { + var switchService = new Service.Switch("Switch"); + + switchService + .getCharacteristic(Characteristic.On) + .on('get', this.getPowerOn.bind(this)) + .on('set', this.setPowerOn.bind(this)); + + return [switchService]; + } + else if (this.service == "GarageDoor") { + var garageDoorService = new Service.GarageDoorOpener("Garage Door Opener"); + + garageDoorService + .getCharacteristic(Characteristic.TargetDoorState) + .on('set', this.setTargetDoorState.bind(this)); + + return [garageDoorService]; + } + else { + throw new Error("Unknown service type '%s'", this.service); + } +} From 6c6b5bf85f0e4612ae745ab0943003b6cd835cfe Mon Sep 17 00:00:00 2001 From: Nick Farina Date: Sat, 5 Sep 2015 12:47:56 -0700 Subject: [PATCH 34/63] Pull in iControl fix --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e7fd564..883ac7b 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "harmonyhubjs-discover": "git+https://github.com/swissmanu/harmonyhubjs-discover.git", "mdns": "^2.2.4", "node-hue-api": "^1.0.5", - "node-icontrol": "^0.1.3", + "node-icontrol": "^0.1.4", "node-milight-promise": "0.0.x", "node-persist": "0.0.x", "request": "2.49.x", From 3cc1f381a393e1e960a4a013415f14a558e4356d Mon Sep 17 00:00:00 2001 From: Nick Farina Date: Sat, 5 Sep 2015 14:04:31 -0700 Subject: [PATCH 35/63] Fix for WeMo switch names --- accessories/WeMo.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/accessories/WeMo.js b/accessories/WeMo.js index d8e713d..26ef822 100644 --- a/accessories/WeMo.js +++ b/accessories/WeMo.js @@ -8,8 +8,9 @@ module.exports = { function WeMoAccessory(log, config) { this.log = log; + this.name = config["name"]; this.service = config["service"] || "Switch"; - this.wemoName = config["wemo_name"] || config["name"]; // fallback to "name" if you didn't specify an exact "wemo_name" + this.wemoName = config["wemo_name"] || this.name; // fallback to "name" if you didn't specify an exact "wemo_name" this.device = null; // instance of WeMo, for controlling the discovered device this.log("Searching for WeMo device with exact name '" + this.wemoName + "'..."); this.search(); @@ -33,7 +34,7 @@ WeMoAccessory.prototype.getPowerOn = function(callback) { if (!this.device) { this.log("No '%s' device found (yet?)", this.wemoName); - callback(new Error("Device not found")); + callback(new Error("Device not found"), false); return; } @@ -101,7 +102,7 @@ WeMoAccessory.prototype.setTargetDoorState = function(targetDoorState, callback) WeMoAccessory.prototype.getServices = function() { if (this.service == "Switch") { - var switchService = new Service.Switch("Switch"); + var switchService = new Service.Switch(this.name); switchService .getCharacteristic(Characteristic.On) @@ -115,7 +116,9 @@ WeMoAccessory.prototype.getServices = function() { garageDoorService .getCharacteristic(Characteristic.TargetDoorState) - .on('set', this.setTargetDoorState.bind(this)); + .on('set', this.setTargetDoorState.bind(this)) + .supportsEventNotification = false; + return [garageDoorService]; } From 94ef18c94d60e88bdd99a6a7563853a5cca5f657 Mon Sep 17 00:00:00 2001 From: Nick Farina Date: Sat, 5 Sep 2015 15:43:11 -0700 Subject: [PATCH 36/63] Bump HAP-NodeJS with fixes for WeMo --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 883ac7b..8da308c 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#fff863d7a387636fc612cf27cb859e82d9ee3294", + "hap-nodejs": "git+https://github.com/KhaosT/HAP-NodeJS#6bf0f9eaaa2d87db8d1768114c61f4acbb095c41", "harmonyhubjs-client": "^1.1.4", "harmonyhubjs-discover": "git+https://github.com/swissmanu/harmonyhubjs-discover.git", "mdns": "^2.2.4", From 70b5a9142a55c7e35da3c9ca3506903b387824b4 Mon Sep 17 00:00:00 2001 From: Kai Date: Sun, 6 Sep 2015 04:31:05 +0200 Subject: [PATCH 37/63] adding resolver chain workaround for raspberry pi adding the workaround from https://github.com/agnat/node_mdns/issues/130 to fix #140 --- platforms/YamahaAVR.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/platforms/YamahaAVR.js b/platforms/YamahaAVR.js index f0d10c1..f554fa0 100644 --- a/platforms/YamahaAVR.js +++ b/platforms/YamahaAVR.js @@ -1,13 +1,19 @@ var types = require("HAP-NodeJS/accessories/types.js"); var Yamaha = require('yamaha-nodejs'); var mdns = require('mdns'); +//workaround for raspberry pi +var sequence = [ + mdns.rst.DNSServiceResolve(), + 'DNSServiceGetAddrInfo' in mdns.dns_sd ? mdns.rst.DNSServiceGetAddrInfo() : mdns.rst.getaddrinfo({families:[4]}), + mdns.rst.makeAddressesUnique() +]; function YamahaAVRPlatform(log, config){ this.log = log; this.config = config; this.playVolume = config["play_volume"]; this.setMainInputTo = config["setMainInputTo"]; - this.browser = mdns.createBrowser(mdns.tcp('http')); + this.browser = mdns.createBrowser(mdns.tcp('http'), {resolverSequence: sequence}); } YamahaAVRPlatform.prototype = { From 6f5e6b6a0b63b2faf8fa2aea6f364dcf2f0434f4 Mon Sep 17 00:00:00 2001 From: Snowdd1 Date: Sun, 6 Sep 2015 09:37:03 +0200 Subject: [PATCH 38/63] More devices for KNX bus Now supports - Lightbulb - Thermostat - TemperatureSensor (as of iOS 8.4.1) - LockMechanism --- accessories/knxdevice.js | 143 +++++++++++++++++++++++++++++++++++---- 1 file changed, 130 insertions(+), 13 deletions(-) diff --git a/accessories/knxdevice.js b/accessories/knxdevice.js index ffcb50e..efdbd4e 100644 --- a/accessories/knxdevice.js +++ b/accessories/knxdevice.js @@ -9,6 +9,7 @@ var knxd = require("eibd"); var knxd_registerGA = require('../platforms/KNX.js').registerGA; var knxd_startMonitor = require('../platforms/KNX.js').startMonitor; +var milliTimeout = 300; // used to block responses while swiping function KNXDevice(log, config) { @@ -164,7 +165,14 @@ KNXDevice.prototype = { characteristic.setValue(val ? 1 : 0, undefined, 'fromKNXBus'); }.bind(this)); }, - + knxregister_boolReverse: function(addresses, characteristic) { + this.log("knx registering BOOLEAN " + addresses); + knxd_registerGA(addresses, function(val, src, dest, type){ + this.log("Received value from bus:"+val+ " for " +dest+ " from "+src+" of type"+type + " for " + characteristic.displayName); +// iterate(characteristic); + characteristic.setValue(val ? 0 : 1, undefined, 'fromKNXBus'); + }.bind(this)); + }, // percentage: get 0..255 from the bus, write 0..100 to characteristic knxregister_percent: function(addresses, characteristic) { this.log("knx registering PERCENT " + addresses); @@ -173,7 +181,16 @@ KNXDevice.prototype = { if (type !== "DPT5") { this.log("[ERROR] Received value cannot be a percentage value"); } else { - characteristic.setValue(Math.round(val/255*100), undefined, 'fromKNXBus'); + if (!characteristic.timeout) { + if (characteristic.timeout < Date.now()) { + characteristic.setValue(Math.round(val/255*100), undefined, 'fromKNXBus'); + } else { + this.log("Blackout time"); + } + } else { + characteristic.setValue(Math.round(val/255*100), undefined, 'fromKNXBus'); + } // todo get the boolean logic right into one OR expresssion + } }.bind(this)); }, @@ -183,7 +200,13 @@ KNXDevice.prototype = { this.log("knx registering FLOAT " + addresses); knxd_registerGA(addresses, function(val, src, dest, type){ this.log("Received value from bus:"+val+ " for " +dest+ " from "+src+" of type"+type+ " for " + characteristic.displayName); - characteristic.setValue(val, undefined, 'fromKNXBus'); + var hk_value = Math.round(val*10)/10; + if (hk_value>=characteristic.minimumValue && hk_value<=characteristic.maximumValue) { + characteristic.setValue(hk_value, undefined, 'fromKNXBus'); // 1 decoimal for HomeKit + } else { + this.log("Value %s out of bounds %s...%s ",hk_value, characteristic.minimumValue, characteristic.maximumValue); + } + }.bind(this)); }, @@ -195,16 +218,16 @@ KNXDevice.prototype = { var HAPvalue = 0; switch (val){ case 0: - HAPvalue = 3; + HAPvalue = 1; break; case 1: - HAPvalue = 3; + HAPvalue = 1; break; case 2: - HAPvalue = 3; + HAPvalue = 1; break; case 3: - HAPvalue = 3; + HAPvalue = 1; break; case 4: HAPvalue = 0; @@ -262,7 +285,22 @@ KNXDevice.prototype = { } }, + setBooleanReverseState: function(value, callback, context, gaddress) { + if (context === 'fromKNXBus') { + this.log(gaddress + " event ping pong, exit!"); + if (callback) { + callback(); + } + } else { + var numericValue = 0; + if (!value) { + numericValue = 1; // need 0 or 1, not true or something + } + this.log("Setting "+gaddress+" Boolean to %s", numericValue); + this.knxwrite(callback, gaddress,'DPT1',numericValue); + } + }, setPercentage: function(value, callback, context, gaddress) { if (context === 'fromKNXBus') { @@ -289,7 +327,7 @@ KNXDevice.prototype = { } else { var numericValue = 0; if (value) { - numericValue = value; // need 0 or 1, not true or something + numericValue = value; // homekit expects precision of 1 decimal } this.log("Setting "+gaddress+" Float to %s", numericValue); this.knxwrite(callback, gaddress,'DPT9',numericValue); @@ -351,14 +389,18 @@ KNXDevice.prototype = { switch (valueType) { case "Bool": myCharacteristic.on('set', function(value, callback, context) { -// this.log("ITERATE DEBUG"); -// iterate(config); this.setBooleanState(value, callback, context, config.Set); }.bind(this)); break; + case "BoolReverse": + myCharacteristic.on('set', function(value, callback, context) { + this.setBooleanReverseState(value, callback, context, config.Set); + }.bind(this)); + break; case "Percent": myCharacteristic.on('set', function(value, callback, context) { this.setPercentage(value, callback, context, config.Set); + myCharacteristic.timeout = Date.now()+milliTimeout; }.bind(this)); break; case "Float": @@ -382,6 +424,9 @@ KNXDevice.prototype = { switch (valueType) { case "Bool": this.knxregister_bool([config.Set].concat(config.Listen || []), myCharacteristic); + break; + case "BoolReverse": + this.knxregister_boolReverse([config.Set].concat(config.Listen || []), myCharacteristic); break; case "Percent": this.knxregister_percent([config.Set].concat(config.Listen || []), myCharacteristic); @@ -414,7 +459,7 @@ KNXDevice.prototype = { this.log("[ERROR] Lightbulb Service without 'name' property called"); return undefined; } - var myService = new Service.Lightbulb() //(config.name,config.name); + var myService = new Service.Lightbulb(config.name,config.name); // On (and Off) if (config.On) { this.log("Lightbulb on/off characteristic enabled"); @@ -430,6 +475,45 @@ KNXDevice.prototype = { //iterate(myService); return myService; }, + + getLockMechanismService: function(config) { + // some sanity checks + //this.config = config; +// Characteristic.LockCurrentState.UNSECURED = 0; +// Characteristic.LockCurrentState.SECURED = 1; + + if (config.type !== "LockMechanism") { + this.log("[ERROR] LockMechanism Service for non 'LockMechanism' service called"); + return undefined; + } + if (!config.name) { + this.log("[ERROR] LockMechanism Service without 'name' property called"); + return undefined; + } + var myService = new Service.LockMechanism(config.name,config.name); + // LockCurrentState + if (config.LockCurrentState) { + // for normal contacts: Secured = 1 + this.log("LockMechanism LockCurrentState characteristic enabled"); + this.bindCharacteristic(myService, Characteristic.LockCurrentState, "Bool", config.LockCurrentState); + } else if (config.LockCurrentStateSecured0) { + // for reverse contacts Secured = 0 + this.log("LockMechanism LockCurrentState characteristic enabled"); + this.bindCharacteristic(myService, Characteristic.LockCurrentState, "BoolReverse", config.LockCurrentStateSecured0); + } + // LockTargetState + if (config.LockTargetState) { + this.log("LockMechanism LockTargetState characteristic enabled"); + this.bindCharacteristic(myService, Characteristic.LockTargetState, "Bool", config.LockTargetState); + } else if (config.LockTargetStateSecured0) { + this.log("LockMechanism LockTargetState characteristic enabled"); + this.bindCharacteristic(myService, Characteristic.LockTargetState, "BoolReverse", config.LockTargetStateSecured0); + } + + //iterate(myService); + return myService; + }, + getThermostatService: function(config) { @@ -459,15 +543,18 @@ KNXDevice.prototype = { this.log("[ERROR] Thermostat Service without 'name' property called"); return undefined; } - var myService = new Service.Thermostat() //(config.name,config.name); + var myService = new Service.Thermostat(config.name,config.name); // CurrentTemperature) if (config.CurrentTemperature) { this.log("Thermostat CurrentTemperature characteristic enabled"); this.bindCharacteristic(myService, Characteristic.CurrentTemperature, "Float", config.CurrentTemperature); } - // TargetTemperature if available + // TargetTemperature if available if (config.TargetTemperature) { this.log("Thermostat TargetTemperature characteristic enabled"); + // default boundary too narrow for thermostats + myService.getCharacteristic(Characteristic.TargetTemperature).minimumValue=0; // °C + myService.getCharacteristic(Characteristic.TargetTemperature).maximumValue=40; // °C this.bindCharacteristic(myService, Characteristic.TargetTemperature, "Float", config.TargetTemperature); } // HVAC missing yet @@ -477,9 +564,33 @@ KNXDevice.prototype = { } return myService; }, + + // temperature sensor type (iOS9 assumed) + getTemperatureSensorService: function(config) { + // some sanity checks + + + if (config.type !== "TemperatureSensor") { + this.log("[ERROR] TemperatureSensor Service for non 'TemperatureSensor' service called"); + return undefined; + } + if (!config.name) { + this.log("[ERROR] TemperatureSensor Service without 'name' property called"); + return undefined; + } + var myService = new Service.TemperatureSensor(config.name,config.name); + // CurrentTemperature) + if (config.CurrentTemperature) { + this.log("Thermostat CurrentTemperature characteristic enabled"); + this.bindCharacteristic(myService, Characteristic.CurrentTemperature, "Float", config.CurrentTemperature); + } + return myService; + }, + + /* assemble the device ***************************************************************************************************/ @@ -518,6 +629,12 @@ KNXDevice.prototype = { case "Lightbulb": accessoryServices.push(this.getLightbulbService(configService)); break; + case "LockMechanism": + accessoryServices.push(this.getLockMechanismService(configService)); + break; + case "TemperatureSensor": + accessoryServices.push(this.getTemperatureSensorService(configService)); + break; case "Thermostat": accessoryServices.push(this.getThermostatService(configService)); break; From 86d548b8d9621339d56329acc4906bd07407963a Mon Sep 17 00:00:00 2001 From: Snowdd1 Date: Sun, 6 Sep 2015 09:39:51 +0200 Subject: [PATCH 39/63] Resolve package.json conflict Merged WEMO update --- package.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/package.json b/package.json index c4dcfcb..217e71b 100644 --- a/package.json +++ b/package.json @@ -15,12 +15,8 @@ "carwingsjs": "0.0.x", "color": "0.10.x", "elkington": "kevinohara80/elkington", -<<<<<<< HEAD "eibd": "0.3.1", - "hap-nodejs": "git+https://github.com/KhaosT/HAP-NodeJS#fff863d7a387636fc612cf27cb859e82d9ee3294", -======= "hap-nodejs": "git+https://github.com/KhaosT/HAP-NodeJS#6bf0f9eaaa2d87db8d1768114c61f4acbb095c41", ->>>>>>> nfarina/master "harmonyhubjs-client": "^1.1.4", "harmonyhubjs-discover": "git+https://github.com/swissmanu/harmonyhubjs-discover.git", "mdns": "^2.2.4", From 09516acaf3ed112a6c4d2802533a74bd3db0bc93 Mon Sep 17 00:00:00 2001 From: Snowdd1 Date: Sun, 6 Sep 2015 11:11:58 +0200 Subject: [PATCH 40/63] package.json update eibd dependency to include future versions (^0.3.1) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 217e71b..9fa4cb5 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "carwingsjs": "0.0.x", "color": "0.10.x", "elkington": "kevinohara80/elkington", - "eibd": "0.3.1", + "eibd": "^0.3.1", "hap-nodejs": "git+https://github.com/KhaosT/HAP-NodeJS#6bf0f9eaaa2d87db8d1768114c61f4acbb095c41", "harmonyhubjs-client": "^1.1.4", "harmonyhubjs-discover": "git+https://github.com/swissmanu/harmonyhubjs-discover.git", From 9d8afc4bcb73e6b52a3855249b91b3f8656a71d0 Mon Sep 17 00:00:00 2001 From: Snowdd1 Date: Sun, 6 Sep 2015 11:41:33 +0200 Subject: [PATCH 41/63] cleanup of sample.json and platform shim removed non-existent old device types from platform shim. --- config-sample-knx.json | 205 ++++++++++++++++++++++++----------------- platforms/KNX.js | 18 ---- 2 files changed, 119 insertions(+), 104 deletions(-) diff --git a/config-sample-knx.json b/config-sample-knx.json index 8e7d11e..df0de60 100644 --- a/config-sample-knx.json +++ b/config-sample-knx.json @@ -1,88 +1,121 @@ { - "bridge": { - "name": "Homebridge", - "username": "CC:22:3D:E3:CE:30", - "port": 51826, - "pin": "031-45-154" - }, - "description": "This is an example configuration file for KNX platform shim", - "platforms": - [ - { - "platform": "KNX", - "name": "KNX", - "knxd_ip": "192.168.178.205", - "knxd_port": 6720, - "accessories": - [ - { - "accessory_type": "knxdevice", - "name": "Living Room North Lamp", - "services": - [ - { - "type": "Lightbulb", - "name": "Living Room North Lamp", - "On": { - "Set": "1/1/6", - "Listen": [ - "1/1/63" - ] - }, - "Brightness": { - "Set": "1/1/62", - "Listen": [ - "1/1/64" - ] - } - } - ] - }, - { - "accessory_type": "knxdevice", - "name": "Office", - "services": - [ - { - "type": "Lightbulb", - "name": "Office Lamp", - "On": { - "Set": "1/3/5" - } - }, - { - "type": "Thermostat", - "name": "Raumtemperatur", - "CurrentTemperature": { - "Listen": "3/3/44" - }, - "TargetTemperature": { - "Set": "3/3/94" - }, - "CurrentHeatingCoolingState": { - "Listen": "3/3/64" - } - }, - { - "type": "WindowCovering", - "name": "Rollo", - "Target": { - "Set": "address", - "Listen": "adresses" - }, - "Current": { - "Set": "address", - "Listen": "adresses" - }, - "PositionState": { - "Listen": "adresses" - } - } - ] - } - ] - } - ], - "accessories": [], - "prototyping": {} + "bridge": { + "name": "Homebridge", + "username": "CC:22:3D:E3:CE:30", + "port": 51826, + "pin": "031-45-154" + }, + "description": "This is an example configuration file for KNX platform shim", + "hint": "Always paste into jsonlint.com validation page before starting your homebridge, saves a lot of frustration", + "platforms": [ + { + "platform": "KNX", + "name": "KNX", + "knxd_ip": "192.168.178.205", + "knxd_port": 6720, + "accessories": [ + { + "accessory_type": "knxdevice", + "description": "Only generic type knxdevice is supported, all previous knx type have been merged into that.", + "name": "Living Room North Lamp", + "services": [ + { + "type": "Lightbulb", + "description": "iOS8 Lightbulb type, supports On (Switch) and Brightness", + "name": "Living Room North Lamp", + "On": { + "Set": "1/1/6", + "Listen": [ + "1/1/63" + ] + }, + "Brightness": { + "Set": "1/1/62", + "Listen": [ + "1/1/64" + ] + } + } + ], + "services-description": "Services is an array, you CAN have multiple service types in one accessory, though it is not fully supported in many iOS HK apps, such as EVE and myTouchHome" + }, + { + "accessory_type": "knxdevice", + "name": "Office Temperature", + "description": "iOS8.4.1 TemperatureSensor type, supports CurrentTemperature", + "services": [ + { + "type": "TemperatureSensor", + "name": "Raumtemperatur", + "CurrentTemperature": { + "Listen": "3/3/44" + } + } + ] + }, + { + "accessory_type": "knxdevice", + "name": "Office Window Lock", + "services": [ + { + "type": "LockMechanism", + "description": "iOS8 Lock mechanism, Supports LockCurrentStateSecured0 OR LockCurrentState, LockTargetStateSecured0 OR LockTargetState, use depending if LOCKED is 0 or 1", + "name": "Office Window Lock", + "LockCurrentStateSecured0": { + "Listen": "5/3/15" + }, + "LockTargetStateSecured0": { + "Listen": "5/3/15" + } + } + ] + }, + { + "accessory_type": "knxdevice", + "description":"sample device with multiple services. Multiple services of different types are widely supported" + "name": "Office", + "services": [ + { + "type": "Lightbulb", + "name": "Office Lamp", + "On": { + "Set": "1/3/5" + } + }, + { + "type": "Thermostat", + "description": "iOS8 Thermostat type, supports CurrentTemperature, TargetTemperature, CurrentHeatingCoolingState ", + "name": "Raumtemperatur", + "CurrentTemperature": { + "Listen": "3/3/44" + }, + "TargetTemperature": { + "Set": "3/3/94" + }, + "CurrentHeatingCoolingState": { + "Listen": "3/3/64" + } + }, + { + "type": "WindowCovering", + "description": "iOS9 Window covering (blinds etc) type, still WIP", + "name": "Blinds", + "Target": { + "Set": "address", + "Listen": "adresses" + }, + "Current": { + "Set": "address", + "Listen": "adresses" + }, + "PositionState": { + "Listen": "adresses" + } + } + ] + } + ] + } + ], + "accessories": [] } \ No newline at end of file diff --git a/platforms/KNX.js b/platforms/KNX.js index 7070137..573b3b9 100644 --- a/platforms/KNX.js +++ b/platforms/KNX.js @@ -47,24 +47,6 @@ KNXPlatform.prototype = { this.log("created "+acc.name+" universal accessory"); myAccessories.push(acc); break; - case "knxlamp": - this.log("push new lamp with "+foundAccessories[int].name ); - foundAccessories[int].knxd_ip = this.config.knxd_ip; - foundAccessories[int].knxd_port = this.config.knxd_port; - var accConstructor = require('./../accessories/knxlamp.js'); - var acc = new accConstructor.accessory(this.log,foundAccessories[int]); - this.log("created "+acc.name+" accessory"); - myAccessories.push(acc); - break; - case "knxthermostat": - this.log("push new thermostat with "+foundAccessories[int].name); - foundAccessories[int].knxd_ip = this.config.knxd_ip; - foundAccessories[int].knxd_port = this.config.knxd_port; - var accConstructor = require('./../accessories/knxthermostat.js'); - var acc = new accConstructor.accessory(this.log,foundAccessories[int]); - this.log("created "+acc.name+" accessory"); - myAccessories.push(acc); - break; default: // do something else this.log("unkown accessory type found") From 6614705d6c96fcdb9b7a3bc5d7cd0dc019cc3769 Mon Sep 17 00:00:00 2001 From: Raoul Date: Mon, 7 Sep 2015 15:27:30 +0200 Subject: [PATCH 42/63] missing comma should have used fsonlint.com after adding the comments and secriptions --- config-sample-knx.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config-sample-knx.json b/config-sample-knx.json index df0de60..a8e52b1 100644 --- a/config-sample-knx.json +++ b/config-sample-knx.json @@ -72,7 +72,7 @@ }, { "accessory_type": "knxdevice", - "description":"sample device with multiple services. Multiple services of different types are widely supported" + "description":"sample device with multiple services. Multiple services of different types are widely supported", "name": "Office", "services": [ { @@ -118,4 +118,4 @@ } ], "accessories": [] -} \ No newline at end of file +} From 78987a775fc31436154ca62328ca6f4c0534f818 Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Mon, 7 Sep 2015 19:16:51 +0200 Subject: [PATCH 43/63] Might be stable now for Switches, Dimmers, & Thermostats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Basically, I needed to provide a default value instead of a `getDefaultValue` function. Keeping with this for a while before trying battery stats and door sensors again, but I think I figured out my compliance problems…don’t know how it ever successfully added before, actually. Also removed some more cruft from the earlier old-API version and started laying some groundwork for polling/updating from ZWay. --- platforms/ZWayServer.js | 152 +++++++++++++++++++--------------------- 1 file changed, 74 insertions(+), 78 deletions(-) diff --git a/platforms/ZWayServer.js b/platforms/ZWayServer.js index 9a515ee..da1496e 100644 --- a/platforms/ZWayServer.js +++ b/platforms/ZWayServer.js @@ -57,6 +57,7 @@ function ZWayServerPlatform(log, config){ this.password = config["password"]; this.name_overrides = config["name_overrides"]; this.batteryLow = config["battery_low_level"]; + this.pollInterval = config["poll_interval"] || 2; this.userAgent = "HomeBridge/-1^0.5"; this.sessionId = ""; this.jar = request.jar(new tough.CookieJar()); @@ -66,22 +67,6 @@ ZWayServerPlatform.getVDevTypeKey = function(vdev){ return vdev.deviceType + (vdev.metrics && vdev.metrics.probeTitle ? "." + vdev.metrics.probeTitle : "") } -ZWayServerPlatform.getVDevServiceTypes = function(vdev){ - var typeKey = ZWayServerPlatform.getVDevTypeKey(vdev); - switch (typeKey) { - case "switchBinary": - return [types.SWITCH_STYPE]; - case "switchMultilevel": - return [types.LIGHTBULB_STYPE]; - case "thermostat": - return [types.THERMOSTAT_STYPE]; - case "sensorMultilevel.Temperature": - return [types.TEMPERATURE_SENSOR_STYPE]; - case "sensorBinary.Door/Window": - return [types.GARAGE_DOOR_OPENER_STYPE]; - } -} - ZWayServerPlatform.prototype = { zwayRequest: function(opts){ @@ -211,7 +196,14 @@ function ZWayServerAccessory(name, dclass, devDesc, platform) { ZWayServerAccessory.prototype = { - + + getVDev: function(vdev){ + return this.platform.zwayRequest({ + method: "GET", + url: this.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id + })//.then(function()); + } + , command: function(vdev, command, value) { return this.platform.zwayRequest({ method: "GET", @@ -237,7 +229,7 @@ ZWayServerAccessory.prototype = { services.push(new Service.TemperatureSensor(vdev.metrics.title)); break; case "sensorBinary.Door/Window": - services.push(new Service.GarageDoorOpener(vdev.metrics.title)); + //services.push(new Service.GarageDoorOpener(vdev.metrics.title)); break; case "battery.Battery": services.push(new Service.BatteryService(vdev.metrics.title)); @@ -316,23 +308,23 @@ ZWayServerAccessory.prototype = { } if(cx instanceof Characteristic.On){ - cx.getDefaultValue = gdv; + cx.zway_getValueFromVDev = function(vdev){ + var val = false; + if(vdev.metrics.level === "off"){ + val = false; + } else if(vdev.metrics.level <= 5) { + val = false; + } else if (vdev.metrics.level > 5) { + val = true; + } + return val; + }; + cx.value = cx.zway_getValueFromVDev(vdev); cx.on('get', function(callback, context){ debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); - that.platform.zwayRequest({ - method: "GET", - url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id - }).then(function(result){ - debug("Got value: " + result.data.metrics.level + ", for " + vdev.metrics.title + "."); - var val; - if(result.data.metrics.level === "off"){ - val = false; - } else if(val <= 5) { - val = false; - } else if (val > 5) { - val = true; - } - callback(false, val); + this.getVDev(vdev).then(function(result){ + debug("Got value: " + cx.zway_getValueFromVDev(result.data) + ", for " + vdev.metrics.title + "."); + callback(false, cx.zway_getValueFromVDev(result.data)); }); }.bind(this)); cx.on('set', function(powerOn, callback){ @@ -344,15 +336,15 @@ ZWayServerAccessory.prototype = { } if(cx instanceof Characteristic.Brightness){ - cx.getDefaultValue = gdv; + cx.zway_getValueFromVDev = function(vdev){ + return vdev.metrics.level; + }; + cx.value = cx.zway_getValueFromVDev(vdev); cx.on('get', function(callback, context){ debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); - that.platform.zwayRequest({ - method: "GET", - url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id - }).then(function(result){ - debug("Got value: " + result.data.metrics.level + ", for " + vdev.metrics.title + "."); - callback(false, result.data.metrics.level); + this.getVDev(vdev).then(function(result){ + debug("Got value: " + cx.zway_getValueFromVDev(result.data) + ", for " + vdev.metrics.title + "."); + callback(false, cx.zway_getValueFromVDev(result.data)); }); }.bind(this)); cx.on('set', function(level, callback){ @@ -364,15 +356,15 @@ ZWayServerAccessory.prototype = { } if(cx instanceof Characteristic.CurrentTemperature){ - cx.getDefaultValue = gdv; + cx.zway_getValueFromVDev = function(vdev){ + return vdev.metrics.level; + }; + cx.value = cx.zway_getValueFromVDev(vdev); cx.on('get', function(callback, context){ debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); - that.platform.zwayRequest({ - method: "GET", - url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id - }).then(function(result){ - debug("Got value: " + result.data.metrics.level + ", for " + vdev.metrics.title + "."); - callback(false, result.data.metrics.level); + this.getVDev(vdev).then(function(result){ + debug("Got value: " + cx.zway_getValueFromVDev(result.data) + ", for " + vdev.metrics.title + "."); + callback(false, cx.zway_getValueFromVDev(result.data)); }); }.bind(this)); cx.minimumValue = vdev.metrics && vdev.metrics.min !== undefined ? vdev.metrics.min : -40; @@ -381,15 +373,15 @@ ZWayServerAccessory.prototype = { } if(cx instanceof Characteristic.TargetTemperature){ - cx.getDefaultValue = gdv; + cx.zway_getValueFromVDev = function(vdev){ + return vdev.metrics.level; + }; + cx.value = cx.zway_getValueFromVDev(vdev); cx.on('get', function(callback, context){ debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); - this.platform.zwayRequest({ - method: "GET", - url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id - }).then(function(result){ - debug("Got value: " + result.data.metrics.level + ", for " + vdev.metrics.title + "."); - callback(false, result.data.metrics.level); + this.getVDev(vdev).then(function(result){ + debug("Got value: " + cx.zway_getValueFromVDev(result.data) + ", for " + vdev.metrics.title + "."); + callback(false, cx.zway_getValueFromVDev(result.data)); }); }.bind(this)); cx.on('set', function(level, callback){ @@ -405,10 +397,11 @@ ZWayServerAccessory.prototype = { if(cx instanceof Characteristic.TemperatureDisplayUnits){ //TODO: Always in °C for now. - cx.getDefaultValue = function(){ return Characteristic.TemperatureDisplayUnits.CELCIUS; }; + cx.getDefaultValue = function(){ return Characteristic.TemperatureDisplayUnits.CELSIUS; }; + cx.value = cx.getDefaultValue(); cx.on('get', function(callback, context){ debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); - callback(false, Characteristic.TemperatureDisplayUnits.CELCIUS); + callback(false, Characteristic.TemperatureDisplayUnits.CELSIUS); }); cx.writable = false; return cx; @@ -417,6 +410,7 @@ ZWayServerAccessory.prototype = { if(cx instanceof Characteristic.CurrentHeatingCoolingState){ //TODO: Always HEAT for now, we don't have an example to work with that supports another function. cx.getDefaultValue = function(){ return Characteristic.CurrentHeatingCoolingState.HEAT; }; + cx.value = cx.getDefaultValue(); cx.on('get', function(callback, context){ debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); callback(false, Characteristic.CurrentHeatingCoolingState.HEAT); @@ -427,6 +421,7 @@ ZWayServerAccessory.prototype = { if(cx instanceof Characteristic.TargetHeatingCoolingState){ //TODO: Always HEAT for now, we don't have an example to work with that supports another function. cx.getDefaultValue = function(){ return Characteristic.TargetHeatingCoolingState.HEAT; }; + cx.value = cx.getDefaultValue(); cx.on('get', function(callback, context){ debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); callback(false, Characteristic.TargetHeatingCoolingState.HEAT); @@ -436,17 +431,15 @@ ZWayServerAccessory.prototype = { } if(cx instanceof Characteristic.CurrentDoorState){ - cx.getDefaultValue = function(){ - return vdev.metrics.level == "off" ? Characteristic.CurrentDoorState.CLOSED : Characteristic.CurrentDoorState.OPEN; + cx.zway_getValueFromVDev = function(vdev){ + return vdev.metrics.level === "off" ? Characteristic.CurrentDoorState.CLOSED : Characteristic.CurrentDoorState.OPEN; }; + cx.value = cx.zway_getValueFromVDev(vdev); cx.on('get', function(callback, context){ debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); - this.platform.zwayRequest({ - method: "GET", - url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id - }).then(function(result){ - debug("Got value: " + result.data.metrics.level + ", for " + vdev.metrics.title + "."); - callback(false, result.data.metrics.level == "off" ? Characteristic.CurrentDoorState.CLOSED : Characteristic.CurrentDoorState.OPEN); + this.getVDev(vdev).then(function(result){ + debug("Got value: " + cx.zway_getValueFromVDev(result.data) + ", for " + vdev.metrics.title + "."); + callback(false, cx.zway_getValueFromVDev(result.data)); }); }.bind(this)); } @@ -454,6 +447,7 @@ ZWayServerAccessory.prototype = { if(cx instanceof Characteristic.TargetDoorState){ //TODO: We only support this for Door sensors now, so it's a fixed value. cx.getDefaultValue = function(){ return Characteristic.TargetDoorState.CLOSED; }; + cx.value = cx.getDefaultValue(); cx.on('get', function(callback, context){ debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); callback(false, Characteristic.TargetDoorState.CLOSED); @@ -465,6 +459,7 @@ ZWayServerAccessory.prototype = { if(cx instanceof Characteristic.ObstructionDetected){ //TODO: We only support this for Door sensors now, so it's a fixed value. cx.getDefaultValue = function(){ return false; }; + cx.value = cx.getDefaultValue(); cx.on('get', function(callback, context){ debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); callback(false, false); @@ -474,29 +469,29 @@ ZWayServerAccessory.prototype = { } if(cx instanceof Characteristic.BatteryLevel){ - cx.getDefaultValue = gdv; + cx.zway_getValueFromVDev = function(vdev){ + return vdev.metrics.level; + }; + cx.value = cx.zway_getValueFromVDev(vdev); cx.on('get', function(callback, context){ debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); - that.platform.zwayRequest({ - method: "GET", - url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id - }).then(function(result){ - debug("Got value: " + result.data.metrics.level + ", for " + vdev.metrics.title + "."); - callback(false, result.data.metrics.level); + this.getVDev(vdev).then(function(result){ + debug("Got value: " + cx.zway_getValueFromVDev(result.data) + ", for " + vdev.metrics.title + "."); + callback(false, cx.zway_getValueFromVDev(result.data)); }); }.bind(this)); } if(cx instanceof Characteristic.StatusLowBattery){ - cx.getDefaultValue = function(){ return Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; }; + cx.zway_getValueFromVDev = function(vdev){ + return vdev.metrics.level <= that.platform.batteryLow ? Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; + }; + cx.value = cx.zway_getValueFromVDev(vdev); cx.on('get', function(callback, context){ debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); - that.platform.zwayRequest({ - method: "GET", - url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id - }).then(function(result){ - debug("Got value: " + result.data.metrics.level + ", for " + vdev.metrics.title + "."); - callback(false, result.data.metrics.level <= that.platform.batteryLow ? Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL); + this.getVDev(vdev).then(function(result){ + debug("Got value: " + cx.zway_getValueFromVDev(result.data) + ", for " + vdev.metrics.title + "."); + callback(false, cx.zway_getValueFromVDev(result.data)); }); }.bind(this)); } @@ -504,6 +499,7 @@ ZWayServerAccessory.prototype = { if(cx instanceof Characteristic.ChargingState){ //TODO: No known chargeable devices(?), so always return false. cx.getDefaultValue = function(){ return Characteristic.ChargingState.NOT_CHARGING; }; + cx.value = cx.getDefaultValue(); cx.on('get', function(callback, context){ debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); callback(false, Characteristic.ChargingState.NOT_CHARGING); From a4c3f73eb54620abfe73ad95627110d34f8df0bb Mon Sep 17 00:00:00 2001 From: Sam Edwards Date: Mon, 7 Sep 2015 13:29:51 -0700 Subject: [PATCH 44/63] [MiLight] Fix scope issue that prevented config from working, added documentation, and added correct pause for night mode commands --- accessories/MiLight.js | 68 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 61 insertions(+), 7 deletions(-) diff --git a/accessories/MiLight.js b/accessories/MiLight.js index 85b09d6..cfe339a 100644 --- a/accessories/MiLight.js +++ b/accessories/MiLight.js @@ -1,3 +1,53 @@ +/* + +MiLight accessory shim for Homebridge +Written by Sam Edwards (https://samedwards.ca/) + +Uses the node-milight-promise library (https://github.com/mwittig/node-milight-promise) which features some code from +applamp.nl (http://www.applamp.nl/service/applamp-api/) and uses other details from (http://www.limitlessled.com/dev/) + +Configure in config.json as follows: + +"accessories": [ + { + "accessory":"MiLight", + "name": "Lamp", + "ip_address": "255.255.255.255", + "port": 8899, + "zone": 1, + "type": "rgbw", + "delay": 30, + "repeat": 3 + } +] + +Where the parameters are: + *accessory (required): This must be "MiLight", and refers to the name of the accessory as exported from this file + *name (required): The name for this light/zone, as passed on to Homebridge and HomeKit + *ip_address (optional): The IP address of the WiFi Bridge. Default to the broadcast address of 255.255.255.255 if not specified + *port (optional): Port of the WiFi bridge. Defaults to 8899 if not specified + *zone (required): The zone to target with this accessory. "0" for all zones on the bridge, otherwise 1-4 for a specific zone + *type (required): One of either "rgbw", "rgb", or "white", depending on the type of bulb being controlled + *delay (optional): Delay between commands sent over UDP. Default 30ms + *repeat (optional): Number of times to repeat the UDP command for better reliability. Default 3 + +Tips and Tricks: + *Setting the brightness of an rgbw or a white bulb will set it to "night mode", which is dimmer than the lowest brightness setting + *White and rgb bulbs don't support absolute brightness setting, so we just send a brightness up/brightness down command depending + if we got a percentage above/below 50% respectively + *The only exception to the above is that white bulbs support a "maximum brightness" command, so we send that when we get 100% + *Implemented warmer/cooler for white lamps in a similar way to brightnes, except this time above/below 180 degrees on the colour wheel + *I welcome feedback on a better way to work the brightness/hue for white and rgb bulbs + +Troubleshooting: +The node-milight-promise library provides additional debugging output when the MILIGHT_DEBUG environmental variable is set + +TODO: + *Probably convert this module to a platform that can configure an entire bridge at once, just passing a name for each zone + *Possibly build in some sort of state logging and persistance so that we can answswer HomeKit status queries to the best of our ability + +*/ + var Service = require("HAP-NodeJS").Service; var Characteristic = require("HAP-NodeJS").Characteristic; var Milight = require('node-milight-promise').MilightController; @@ -18,15 +68,15 @@ function MiLight(log, config) { this.type = config["type"]; this.delay = config["delay"]; this.repeat = config["repeat"]; -} -var light = new Milight({ - ip: this.ip_address, - port: this.port, - delayBetweenCommands: this.delay, - commandRepeat: this.repeat + var light = new Milight({ + ip: this.ip_address, + port: this.port, + delayBetweenCommands: this.delay, + commandRepeat: this.repeat }); +} MiLight.prototype = { setPowerState: function(powerOn, callback) { @@ -42,12 +92,15 @@ MiLight.prototype = { setBrightness: function(level, callback) { if (level <= 2 && (this.type == "rgbw" || this.type == "white")) { + // If setting brightness to 2 or lower, instead set night mode for lamps that support it this.log("Setting night mode", level); light.sendCommands(commands[this.type].off(this.zone)); - // Not sure if this timing is going to work or not? It's supposed to be 100ms after the off command + // Ensure we're pausing for 100ms between these commands as per the spec + light.pause(100); light.sendCommands(commands[this.type].nightMode(this.zone)); + } else { this.log("Setting brightness to %s", level); @@ -58,6 +111,7 @@ MiLight.prototype = { if (this.type == "rgbw") { light.sendCommands(commands.rgbw.brightness(level)); } else { + // If this is an rgb or a white lamp, they only support brightness up and down. // Set brightness up when value is >50 and down otherwise. Not sure how well this works real-world. if (level >= 50) { From 2710412ca669055a042338bebd12c3adf1671893 Mon Sep 17 00:00:00 2001 From: Sam Edwards Date: Mon, 7 Sep 2015 16:45:27 -0700 Subject: [PATCH 45/63] [MiLight] Correctly reference light object, and also fix bug with brightness=0 setting the lamp to night mode --- accessories/MiLight.js | 46 +++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/accessories/MiLight.js b/accessories/MiLight.js index cfe339a..c5ce67c 100644 --- a/accessories/MiLight.js +++ b/accessories/MiLight.js @@ -69,60 +69,62 @@ function MiLight(log, config) { this.delay = config["delay"]; this.repeat = config["repeat"]; - var light = new Milight({ + this.light = new Milight({ ip: this.ip_address, port: this.port, delayBetweenCommands: this.delay, commandRepeat: this.repeat -}); + }); } MiLight.prototype = { setPowerState: function(powerOn, callback) { if (powerOn) { - light.sendCommands(commands[this.type].on(this.zone)); this.log("Setting power state to on"); + this.light.sendCommands(commands[this.type].on(this.zone)); } else { - light.sendCommands(commands[this.type].off(this.zone)); this.log("Setting power state to off"); + this.light.sendCommands(commands[this.type].off(this.zone)); } callback(); }, setBrightness: function(level, callback) { - if (level <= 2 && (this.type == "rgbw" || this.type == "white")) { - + if (level == 0) { + // If brightness is set to 0, turn off the lamp + this.log("Setting brightness to 0 (off)"); + this.light.sendCommands(commands[this.type].off(this.zone)); + } else if (level <= 2 && (this.type == "rgbw" || this.type == "white")) { // If setting brightness to 2 or lower, instead set night mode for lamps that support it this.log("Setting night mode", level); - light.sendCommands(commands[this.type].off(this.zone)); + this.light.sendCommands(commands[this.type].off(this.zone)); // Ensure we're pausing for 100ms between these commands as per the spec - light.pause(100); - light.sendCommands(commands[this.type].nightMode(this.zone)); + this.light.pause(100); + this.light.sendCommands(commands[this.type].nightMode(this.zone)); } else { this.log("Setting brightness to %s", level); // Send on command to ensure we're addressing the right bulb - light.sendCommands(commands[this.type].on(this.zone)); + this.light.sendCommands(commands[this.type].on(this.zone)); // If this is an rgbw lamp, set the absolute brightness specified if (this.type == "rgbw") { - light.sendCommands(commands.rgbw.brightness(level)); + this.light.sendCommands(commands.rgbw.brightness(level)); } else { - // If this is an rgb or a white lamp, they only support brightness up and down. // Set brightness up when value is >50 and down otherwise. Not sure how well this works real-world. if (level >= 50) { if (this.type == "white" && level == 100) { // But the white lamps do have a "maximum brightness" command - light.sendCommands(commands.white.maxBright(this.zone)); + this.light.sendCommands(commands.white.maxBright(this.zone)); } else { - light.sendCommands(commands[this.type].brightUp()); + this.light.sendCommands(commands[this.type].brightUp()); } } else { - light.sendCommands(commands[this.type].brightDown()); + this.light.sendCommands(commands[this.type].brightDown()); } } } @@ -132,23 +134,25 @@ MiLight.prototype = { setHue: function(value, callback) { this.log("Setting hue to %s", value); + var hue = Array(value, 0, 0); + // Send on command to ensure we're addressing the right bulb - light.sendCommands(commands[this.type].on(this.zone)); + this.light.sendCommands(commands[this.type].on(this.zone)); if (this.type == "rgbw") { if (value == 0) { - light.sendCommands(commands.rgbw.whiteMode(this.zone)); + this.light.sendCommands(commands.rgbw.whiteMode(this.zone)); } else { - light.sendCommands(commands.rgbw.hue(commands.rgbw.hsvToMilightColor(Array(value, 0, 0)))); + this.light.sendCommands(commands.rgbw.hue(commands.rgbw.hsvToMilightColor(hue))); } } else if (this.type == "rgb") { - light.sendCommands(commands.rgb.hue(commands.rgbw.hsvToMilightColor(Array(value, 0, 0)))); + this.light.sendCommands(commands.rgb.hue(commands.rgbw.hsvToMilightColor(hue))); } else if (this.type == "white") { // Again, white lamps don't support setting an absolue colour temp, so trying to do warmer/cooler step at a time based on colour if (value >= 180) { - light.sendCommands(commands.white.warmer()); + this.light.sendCommands(commands.white.warmer()); } else { - light.sendCommands(commands.white.cooler()); + this.light.sendCommands(commands.white.cooler()); } } From 1df411d916e73b73faea5787c417e1a39f69bd85 Mon Sep 17 00:00:00 2001 From: Raoul Date: Tue, 8 Sep 2015 09:25:44 +0200 Subject: [PATCH 46/63] TESTING NEEDED Proposing idea to de-asynchronize Sonos device discovery with a timeout event for push. I have no IDE at hand right now, so please check syntax before merging! I have no Sonos devices, so please check somebody with the hardware at hand! Thanks Raoul --- platforms/Sonos.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/platforms/Sonos.js b/platforms/Sonos.js index 67d086e..1d19c2f 100644 --- a/platforms/Sonos.js +++ b/platforms/Sonos.js @@ -6,6 +6,8 @@ function SonosPlatform(log, config){ this.config = config; this.name = config["name"]; this.playVolume = config["play_volume"]; + // timeout for device discovery + this.discoveryTimeout = (config.deviceDiscoveryTimeout || 10)*1000; // assume 10sec as a default } SonosPlatform.prototype = { @@ -16,6 +18,18 @@ SonosPlatform.prototype = { // track found devices so we don't add duplicates var roomNamesFound = {}; + // collector array for the devices from callbacks + var devicesFound = []; + // tell the sonos callbacks if timeout already occured + var timeout = false; + + // the timeout event will push the accessories back + setTimeout(function(){ + timeout=true; + callback(devicesFound); + }, this.discoveryTimeout); + + sonos.search(function (device) { that.log("Found device at " + device.host); @@ -26,9 +40,13 @@ SonosPlatform.prototype = { if (!roomNamesFound[roomName]) { roomNamesFound[roomName] = true; that.log("Found playable device - " + roomName); + if (timeout) { + that.log("Ignored: Discovered after timeout (Set deviceDiscoveryTimeout parameter in Sonos section of config.json)"); + } // device is an instance of sonos.Sonos var accessory = new SonosAccessory(that.log, that.config, device, description); - callback([accessory]); + // add it to the collector array + devicesFound.push(accessory); } else { that.log("Ignoring playable device with duplicate room name - " + roomName); From abe88b75020c73551747228355ccdcf03701f6d0 Mon Sep 17 00:00:00 2001 From: Sam Edwards Date: Tue, 8 Sep 2015 10:41:03 -0700 Subject: [PATCH 47/63] [MiLight] Converted accessory to platform. Not fully tested yet --- {accessories => platforms}/MiLight.js | 81 ++++++++++++++++++++++----- 1 file changed, 67 insertions(+), 14 deletions(-) rename {accessories => platforms}/MiLight.js (73%) diff --git a/accessories/MiLight.js b/platforms/MiLight.js similarity index 73% rename from accessories/MiLight.js rename to platforms/MiLight.js index c5ce67c..d38cb3f 100644 --- a/accessories/MiLight.js +++ b/platforms/MiLight.js @@ -1,6 +1,6 @@ /* -MiLight accessory shim for Homebridge +MiLight platform shim for Homebridge Written by Sam Edwards (https://samedwards.ca/) Uses the node-milight-promise library (https://github.com/mwittig/node-milight-promise) which features some code from @@ -8,28 +8,28 @@ applamp.nl (http://www.applamp.nl/service/applamp-api/) and uses other details f Configure in config.json as follows: -"accessories": [ +"platforms": [ { - "accessory":"MiLight", - "name": "Lamp", + "platform":"MiLight", + "name":"MiLight", "ip_address": "255.255.255.255", "port": 8899, - "zone": 1, "type": "rgbw", "delay": 30, - "repeat": 3 + "repeat": 3, + "zones":["Kitchen Lamp","Bedroom Lamp","Living Room Lamp","Hallway Lamp"] } ] Where the parameters are: - *accessory (required): This must be "MiLight", and refers to the name of the accessory as exported from this file - *name (required): The name for this light/zone, as passed on to Homebridge and HomeKit + *platform (required): This must be "MiLight", and refers to the name of the accessory as exported from this file + *name (optional): The display name used for logging output by Homebridge. Best to set to "MiLight" *ip_address (optional): The IP address of the WiFi Bridge. Default to the broadcast address of 255.255.255.255 if not specified *port (optional): Port of the WiFi bridge. Defaults to 8899 if not specified - *zone (required): The zone to target with this accessory. "0" for all zones on the bridge, otherwise 1-4 for a specific zone - *type (required): One of either "rgbw", "rgb", or "white", depending on the type of bulb being controlled + *type (optional): One of either "rgbw", "rgb", or "white", depending on the type of bulb being controlled. This applies to all zones. Defaults to rgbw. *delay (optional): Delay between commands sent over UDP. Default 30ms *repeat (optional): Number of times to repeat the UDP command for better reliability. Default 3 + *zones (required): An array of the names of the zones, in order, 1-4. Use null if a zone is skipped. RGB lamps can only have a single zone. Tips and Tricks: *Setting the brightness of an rgbw or a white bulb will set it to "night mode", which is dimmer than the lowest brightness setting @@ -43,7 +43,6 @@ Troubleshooting: The node-milight-promise library provides additional debugging output when the MILIGHT_DEBUG environmental variable is set TODO: - *Probably convert this module to a platform that can configure an entire bridge at once, just passing a name for each zone *Possibly build in some sort of state logging and persistance so that we can answswer HomeKit status queries to the best of our ability */ @@ -54,10 +53,64 @@ var Milight = require('node-milight-promise').MilightController; var commands = require('node-milight-promise').commands; module.exports = { - accessory: MiLight + accessory: MiLightAccessory, + platform: MiLightPlatform } -function MiLight(log, config) { +function MiLightPlatform(log, config) { + this.log = log; + + this.config = config; +} + +MiLightPlatform.prototype = { + accessories: function(callback) { + var that = this; + var zones = []; + + // Various error checking + if (this.config.zones) { + var zoneLength = this.config.zones.length; + } else { + this.log("ERROR: Could not read zones from configuration."); + return; + } + + if (!this.config["type"]) { + this.log("INFO: Type not specified, defaulting to rgbw"); + this.config["type"] = "rgbw"; + } + + if (zoneLength == 0) { + this.log("ERROR: No zones found in configuration."); + return; + } else if (this.config["type"] == "rgb" && zoneLength > 1) { + this.log("WARNING: RGB lamps only have a single zone. Only the first defined zone will be used."); + zoneLength = 1; + } else if (zoneLength > 4) { + this.log("WARNING: Only a maximum of 4 zones are supported per bridge. Only recognizing the first 4 zones."); + zoneLength = 4; + } + + // Create lamp accessories for all of the defined zones + for (var i=0; i < zoneLength; i++) { + if (!!this.config.zones[i]) { + this.config["name"] = this.config.zones[i]; + this.config["zone"] = i+1; + lamp = new MiLightAccessory(this.log, this.config); + zones.push(lamp); + } + } + if (zones.length > 0) { + callback(zones); + } else { + this.log("ERROR: Unable to find any valid zones"); + return; + } + } +} + +function MiLightAccessory(log, config) { this.log = log; // config info @@ -77,7 +130,7 @@ function MiLight(log, config) { }); } -MiLight.prototype = { +MiLightAccessory.prototype = { setPowerState: function(powerOn, callback) { if (powerOn) { From 5cccd3f916d93b0dd576a3b31d6f72d4bfb63b89 Mon Sep 17 00:00:00 2001 From: Sam Edwards Date: Tue, 8 Sep 2015 11:01:30 -0700 Subject: [PATCH 48/63] [MiLight] Modify logging to show the zone name when used as a platform accessory --- platforms/MiLight.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/platforms/MiLight.js b/platforms/MiLight.js index d38cb3f..7cbf1c9 100644 --- a/platforms/MiLight.js +++ b/platforms/MiLight.js @@ -134,10 +134,10 @@ MiLightAccessory.prototype = { setPowerState: function(powerOn, callback) { if (powerOn) { - this.log("Setting power state to on"); + this.log("["+this.name+"] Setting power state to on"); this.light.sendCommands(commands[this.type].on(this.zone)); } else { - this.log("Setting power state to off"); + this.log("["+this.name+"] Setting power state to off"); this.light.sendCommands(commands[this.type].off(this.zone)); } callback(); @@ -146,11 +146,11 @@ MiLightAccessory.prototype = { setBrightness: function(level, callback) { if (level == 0) { // If brightness is set to 0, turn off the lamp - this.log("Setting brightness to 0 (off)"); + this.log("["+this.name+"] Setting brightness to 0 (off)"); this.light.sendCommands(commands[this.type].off(this.zone)); } else if (level <= 2 && (this.type == "rgbw" || this.type == "white")) { // If setting brightness to 2 or lower, instead set night mode for lamps that support it - this.log("Setting night mode", level); + this.log("["+this.name+"] Setting night mode", level); this.light.sendCommands(commands[this.type].off(this.zone)); // Ensure we're pausing for 100ms between these commands as per the spec @@ -158,7 +158,7 @@ MiLightAccessory.prototype = { this.light.sendCommands(commands[this.type].nightMode(this.zone)); } else { - this.log("Setting brightness to %s", level); + this.log("["+this.name+"] Setting brightness to %s", level); // Send on command to ensure we're addressing the right bulb this.light.sendCommands(commands[this.type].on(this.zone)); @@ -185,7 +185,7 @@ MiLightAccessory.prototype = { }, setHue: function(value, callback) { - this.log("Setting hue to %s", value); + this.log("["+this.name+"] Setting hue to %s", value); var hue = Array(value, 0, 0); @@ -212,7 +212,7 @@ MiLightAccessory.prototype = { }, identify: function(callback) { - this.log("Identify requested!"); + this.log("["+this.name+"] Identify requested!"); callback(); // success }, From 18333242ff3866fb66750123caf001bfe9f74760 Mon Sep 17 00:00:00 2001 From: Sam Edwards Date: Tue, 8 Sep 2015 11:20:36 -0700 Subject: [PATCH 49/63] [MiLight] Add missing callback from hue function --- platforms/MiLight.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/platforms/MiLight.js b/platforms/MiLight.js index 7cbf1c9..4382dea 100644 --- a/platforms/MiLight.js +++ b/platforms/MiLight.js @@ -65,7 +65,6 @@ function MiLightPlatform(log, config) { MiLightPlatform.prototype = { accessories: function(callback) { - var that = this; var zones = []; // Various error checking @@ -208,7 +207,7 @@ MiLightAccessory.prototype = { this.light.sendCommands(commands.white.cooler()); } } - + callback(); }, identify: function(callback) { From 7dc168e9dc51178df4e84793488c3ad06192d76f Mon Sep 17 00:00:00 2001 From: Sam Edwards Date: Tue, 8 Sep 2015 11:33:17 -0700 Subject: [PATCH 50/63] [MiLight] Update config-sample.json to replace MiLight accessory with MiLight platform --- config-sample.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/config-sample.json b/config-sample.json index 4245bd5..d684c9d 100644 --- a/config-sample.json +++ b/config-sample.json @@ -71,7 +71,17 @@ "platform": "YamahaAVR", "play_volume": -35, "setMainInputTo": "AirPlay" - } + }, + { + "platform": "MiLight", + "name": "MiLight", + "ip_address": "255.255.255.255", + "port": 8899, + "type": "rgbw", + "delay": 30, + "repeat": 3, + "zones":["Kitchen Lamp","Bedroom Lamp","Living Room Lamp","Hallway Lamp"] + } ], "accessories": [ @@ -152,16 +162,6 @@ "port" : 4999, // Port the SER2SOCK process is running on "pin": "1234" // PIN used for arming / disarming }, - { - "accessory":"MiLight", - "name": "Lamp", - "ip_address": "255.255.255.255", - "port": 8899, - "zone": 1, - "type": "rgbw", - "delay": 35, - "repeat": 3 - }, { "accessory": "Tesla", "name": "Tesla", From 4170b8a533bf5aaa3657afa45b6033817f4b7f8e Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Wed, 9 Sep 2015 06:28:17 +0200 Subject: [PATCH 51/63] Polling! Updates from ZWay are now reflected in HomeKit! --- platforms/ZWayServer.js | 113 +++++++++++++++++++++++++++++++--------- 1 file changed, 87 insertions(+), 26 deletions(-) diff --git a/platforms/ZWayServer.js b/platforms/ZWayServer.js index da1496e..80da4f8 100644 --- a/platforms/ZWayServer.js +++ b/platforms/ZWayServer.js @@ -58,7 +58,9 @@ function ZWayServerPlatform(log, config){ this.name_overrides = config["name_overrides"]; this.batteryLow = config["battery_low_level"]; this.pollInterval = config["poll_interval"] || 2; - this.userAgent = "HomeBridge/-1^0.5"; + this.lastUpdate = 0; + this.cxVDevMap = {}; + this.vDevStore = {}; this.sessionId = ""; this.jar = request.jar(new tough.CookieJar()); } @@ -96,8 +98,7 @@ proxy: 'http://localhost:8888', }, headers: { "Accept": "application/json", - "Content-Type": "application/json", - "User-Agent": that.userAgent + "Content-Type": "application/json" }, json: true, jar: true//that.jar @@ -137,6 +138,8 @@ proxy: 'http://localhost:8888', method: "GET", url: this.url + 'ZAutomation/api/v1/devices' }).then(function(result){ + this.lastUpdate = result.data.updateTime; + var devices = result.data.devices; var groupedDevices = {}; for(var i = 0; i < devices.length; i++){ @@ -179,9 +182,52 @@ proxy: 'http://localhost:8888', } //foundAccessories = foundAccessories.slice(0, 10); // Limit to a few devices for testing... callback(foundAccessories); - }); + + // Start the polling process... + this.pollingTimer = setTimeout(this.pollUpdate.bind(this), this.pollInterval*1000); + + }.bind(this)); } + , + + pollUpdate: function(){ + //debug("Polling for updates since " + this.lastUpdate + "..."); + return this.zwayRequest({ + method: "GET", + url: this.url + 'ZAutomation/api/v1/devices', + qs: {since: this.lastUpdate} + }).then(function(result){ + this.lastUpdate = result.data.updateTime; + if(result.data && result.data.devices && result.data.devices.length){ + var updates = result.data.devices; + debug("Got " + updates.length + " updates."); + for(var i = 0; i < updates.length; i++){ + var upd = updates[i]; + if(this.cxVDevMap[upd.id]){ + var vdev = this.vDevStore[upd.id]; + vdev.metrics.level = upd.metrics.level; + vdev.updateTime = upd.updateTime; + var cxs = this.cxVDevMap[upd.id]; + for(var j = 0; j < cxs.length; j++){ + var cx = cxs[j]; + if(typeof cx.zway_getValueFromVDev !== "function") continue; + var oldValue = cx.value; + var newValue = cx.zway_getValueFromVDev(vdev); + if(oldValue !== newValue){ + cx.value = newValue; + cx.emit('change', { oldValue:oldValue, newValue:cx.value, context:null }); + debug("Updated characteristic " + cx.displayName + " on " + vdev.metrics.title); + } + } + } + } + } + + // setup next poll... + this.pollingTimer = setTimeout(this.pollUpdate.bind(this), this.pollInterval*1000); + }.bind(this)); + } } @@ -291,14 +337,17 @@ ZWayServerAccessory.prototype = { , configureCharacteristic: function(cx, vdev){ var that = this; - - var gdv = function(){ - debug("Default value for " + vdev.metrics.title + " is " + vdev.metrics.level); - return vdev.metrics.level; - }; + + // Add this combination to the maps... + if(!this.platform.cxVDevMap[vdev.id]) this.platform.cxVDevMap[vdev.id] = []; + this.platform.cxVDevMap[vdev.id].push(cx); + if(!this.platform.vDevStore[vdev.id]) this.platform.vDevStore[vdev.id] = vdev; if(cx instanceof Characteristic.Name){ - cx.getDefaultValue = function(){ return this.name; }; + cx.zway_getValueFromVDev = function(vdev){ + return vdev.metrics.title; + }; + cx.value = cx.zway_getValueFromVDev(vdev); cx.on('get', function(callback, context){ debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); callback(false, that.name); @@ -310,8 +359,8 @@ ZWayServerAccessory.prototype = { if(cx instanceof Characteristic.On){ cx.zway_getValueFromVDev = function(vdev){ var val = false; - if(vdev.metrics.level === "off"){ - val = false; + if(vdev.metrics.level === "on"){ + val = true; } else if(vdev.metrics.level <= 5) { val = false; } else if (vdev.metrics.level > 5) { @@ -386,7 +435,7 @@ ZWayServerAccessory.prototype = { }.bind(this)); cx.on('set', function(level, callback){ this.command(vdev, "exact", {level: parseInt(level, 10)}).then(function(result){ - debug("Got value: " + result.data.metrics.level + ", for " + vdev.metrics.title + "."); + //debug("Got value: " + result.data.metrics.level + ", for " + vdev.metrics.title + "."); callback(); }); }.bind(this)); @@ -397,8 +446,10 @@ ZWayServerAccessory.prototype = { if(cx instanceof Characteristic.TemperatureDisplayUnits){ //TODO: Always in °C for now. - cx.getDefaultValue = function(){ return Characteristic.TemperatureDisplayUnits.CELSIUS; }; - cx.value = cx.getDefaultValue(); + cx.zway_getValueFromVDev = function(vdev){ + return Characteristic.TemperatureDisplayUnits.CELSIUS; + }; + cx.value = cx.zway_getValueFromVDev(vdev); cx.on('get', function(callback, context){ debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); callback(false, Characteristic.TemperatureDisplayUnits.CELSIUS); @@ -409,8 +460,10 @@ ZWayServerAccessory.prototype = { if(cx instanceof Characteristic.CurrentHeatingCoolingState){ //TODO: Always HEAT for now, we don't have an example to work with that supports another function. - cx.getDefaultValue = function(){ return Characteristic.CurrentHeatingCoolingState.HEAT; }; - cx.value = cx.getDefaultValue(); + cx.zway_getValueFromVDev = function(vdev){ + return Characteristic.CurrentHeatingCoolingState.HEAT; + }; + cx.value = cx.zway_getValueFromVDev(vdev); cx.on('get', function(callback, context){ debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); callback(false, Characteristic.CurrentHeatingCoolingState.HEAT); @@ -420,8 +473,10 @@ ZWayServerAccessory.prototype = { if(cx instanceof Characteristic.TargetHeatingCoolingState){ //TODO: Always HEAT for now, we don't have an example to work with that supports another function. - cx.getDefaultValue = function(){ return Characteristic.TargetHeatingCoolingState.HEAT; }; - cx.value = cx.getDefaultValue(); + cx.zway_getValueFromVDev = function(vdev){ + return Characteristic.TargetHeatingCoolingState.HEAT; + }; + cx.value = cx.zway_getValueFromVDev(vdev); cx.on('get', function(callback, context){ debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); callback(false, Characteristic.TargetHeatingCoolingState.HEAT); @@ -446,8 +501,10 @@ ZWayServerAccessory.prototype = { if(cx instanceof Characteristic.TargetDoorState){ //TODO: We only support this for Door sensors now, so it's a fixed value. - cx.getDefaultValue = function(){ return Characteristic.TargetDoorState.CLOSED; }; - cx.value = cx.getDefaultValue(); + cx.zway_getValueFromVDev = function(vdev){ + return Characteristic.TargetDoorState.CLOSED; + }; + cx.value = cx.zway_getValueFromVDev(vdev); cx.on('get', function(callback, context){ debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); callback(false, Characteristic.TargetDoorState.CLOSED); @@ -458,8 +515,10 @@ ZWayServerAccessory.prototype = { if(cx instanceof Characteristic.ObstructionDetected){ //TODO: We only support this for Door sensors now, so it's a fixed value. - cx.getDefaultValue = function(){ return false; }; - cx.value = cx.getDefaultValue(); + cx.zway_getValueFromVDev = function(vdev){ + return false; + }; + cx.value = cx.zway_getValueFromVDev(vdev); cx.on('get', function(callback, context){ debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); callback(false, false); @@ -498,8 +557,10 @@ ZWayServerAccessory.prototype = { if(cx instanceof Characteristic.ChargingState){ //TODO: No known chargeable devices(?), so always return false. - cx.getDefaultValue = function(){ return Characteristic.ChargingState.NOT_CHARGING; }; - cx.value = cx.getDefaultValue(); + cx.zway_getValueFromVDev = function(vdev){ + return Characteristic.ChargingState.NOT_CHARGING; + }; + cx.value = cx.zway_getValueFromVDev(vdev); cx.on('get', function(callback, context){ debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); callback(false, Characteristic.ChargingState.NOT_CHARGING); @@ -547,7 +608,7 @@ ZWayServerAccessory.prototype = { services = services.concat(this.getVDevServices(this.devDesc.devices[this.devDesc.primary])); if(this.devDesc.types["battery.Battery"]){ - //services = services.concat(this.getVDevServices(this.devDesc.devices[this.devDesc.types["battery.Battery"]])); + services = services.concat(this.getVDevServices(this.devDesc.devices[this.devDesc.types["battery.Battery"]])); } debug("Loaded services for " + this.name); From 62cabc23f3cde3f55639e5be917bb22470009ff6 Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Wed, 9 Sep 2015 06:48:33 +0200 Subject: [PATCH 52/63] Added Door/Window sensors and ancillary temperature sensors. Door/Window sensors are still implemented as garage door openers, because that seems to make the most sense at the moment. --- platforms/ZWayServer.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/platforms/ZWayServer.js b/platforms/ZWayServer.js index 80da4f8..416d431 100644 --- a/platforms/ZWayServer.js +++ b/platforms/ZWayServer.js @@ -80,7 +80,7 @@ ZWayServerPlatform.prototype = { opts.headers = { "Cookie": "ZWAYSession=" + this.sessionId }; -opts.proxy = 'http://localhost:8888'; +//opts.proxy = 'http://localhost:8888'; request(opts, function(error, response, body){ if(response.statusCode == 401){ @@ -88,7 +88,7 @@ opts.proxy = 'http://localhost:8888'; request({ method: "POST", url: that.url + 'ZAutomation/api/v1/login', -proxy: 'http://localhost:8888', +//proxy: 'http://localhost:8888', body: { //JSON.stringify({ "form": true, "login": that.login, @@ -275,7 +275,7 @@ ZWayServerAccessory.prototype = { services.push(new Service.TemperatureSensor(vdev.metrics.title)); break; case "sensorBinary.Door/Window": - //services.push(new Service.GarageDoorOpener(vdev.metrics.title)); + services.push(new Service.GarageDoorOpener(vdev.metrics.title)); break; case "battery.Battery": services.push(new Service.BatteryService(vdev.metrics.title)); @@ -611,6 +611,12 @@ ZWayServerAccessory.prototype = { services = services.concat(this.getVDevServices(this.devDesc.devices[this.devDesc.types["battery.Battery"]])); } + // Odds and ends...if there are sensors that haven't been used, add services for them... + var tempSensor = this.devDesc.types["sensorMultilevel.Temperature"] !== undefined ? this.devDesc.devices[this.devDesc.types["sensorMultilevel.Temperature"]] : false; + if(tempSensor && !this.platform.cxVDevMap[tempSensor.id]){ + services = services.concat(this.getVDevServices(tempSensor)); + } + debug("Loaded services for " + this.name); return services; } From d6e31b4aa71d388e6f2905893acfa4273c423b4a Mon Sep 17 00:00:00 2001 From: Sam Edwards Date: Wed, 9 Sep 2015 08:13:22 -0700 Subject: [PATCH 53/63] [MiLight] Swap cooler/warmer direction for white bulbs, and add note about delay --- platforms/MiLight.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/platforms/MiLight.js b/platforms/MiLight.js index 4382dea..3869e74 100644 --- a/platforms/MiLight.js +++ b/platforms/MiLight.js @@ -27,7 +27,7 @@ Where the parameters are: *ip_address (optional): The IP address of the WiFi Bridge. Default to the broadcast address of 255.255.255.255 if not specified *port (optional): Port of the WiFi bridge. Defaults to 8899 if not specified *type (optional): One of either "rgbw", "rgb", or "white", depending on the type of bulb being controlled. This applies to all zones. Defaults to rgbw. - *delay (optional): Delay between commands sent over UDP. Default 30ms + *delay (optional): Delay between commands sent over UDP. Default 30ms. May cause delays when sending a lot of commands. Try decreasing to improve. *repeat (optional): Number of times to repeat the UDP command for better reliability. Default 3 *zones (required): An array of the names of the zones, in order, 1-4. Use null if a zone is skipped. RGB lamps can only have a single zone. @@ -202,9 +202,9 @@ MiLightAccessory.prototype = { } else if (this.type == "white") { // Again, white lamps don't support setting an absolue colour temp, so trying to do warmer/cooler step at a time based on colour if (value >= 180) { - this.light.sendCommands(commands.white.warmer()); - } else { this.light.sendCommands(commands.white.cooler()); + } else { + this.light.sendCommands(commands.white.warmer()); } } callback(); From c73e22984d06d2668e96667eb096de91ec73c34d Mon Sep 17 00:00:00 2001 From: Nelson Melo Date: Wed, 9 Sep 2015 15:26:21 -0400 Subject: [PATCH 54/63] Implemented LIFx bulb platform --- package.json | 3 +- platforms/LIFx.js | 174 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 platforms/LIFx.js diff --git a/package.json b/package.json index 9fa4cb5..8427666 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,12 @@ "ad2usb": "git+https://github.com/alistairg/node-ad2usb.git#local", "carwingsjs": "0.0.x", "color": "0.10.x", - "elkington": "kevinohara80/elkington", "eibd": "^0.3.1", + "elkington": "kevinohara80/elkington", "hap-nodejs": "git+https://github.com/KhaosT/HAP-NodeJS#6bf0f9eaaa2d87db8d1768114c61f4acbb095c41", "harmonyhubjs-client": "^1.1.4", "harmonyhubjs-discover": "git+https://github.com/swissmanu/harmonyhubjs-discover.git", + "lifx-api": "^1.0.1", "mdns": "^2.2.4", "node-hue-api": "^1.0.5", "node-icontrol": "^0.1.4", diff --git a/platforms/LIFx.js b/platforms/LIFx.js new file mode 100644 index 0000000..55d66f2 --- /dev/null +++ b/platforms/LIFx.js @@ -0,0 +1,174 @@ +var types = require("HAP-NodeJS/accessories/types.js"); +var lifxObj = require('lifx-api'); +var lifx; + +function LIFxPlatform(log, config){ + + // auth info + this.access_token = config["access_token"]; + + lifx = new lifxObj(this.access_token); + + this.log = log; +} + +LIFxPlatform.prototype = { + accessories: function(callback) { + this.log("Fetching LIFx devices."); + + var that = this; + var foundAccessories = []; + + lifx.listLights("all", function(body) { + var bulbs = JSON.parse(body); + + for(var i = 0; i < bulbs.length; i ++) { + var bulb = bulbs[i]; + var accessory = new LIFxBulbAccessory( + that.log, + bulb.label, + bulb.uuid, + bulb.model, + bulb.id + ); + foundAccessories.push(accessory); + } + callback(foundAccessories) + }); + } +} + +function LIFxBulbAccessory(log, label, serial, model, deviceId) { + // device info + this.name = label; + this.model = model; + this.deviceId = deviceId; + this.serial = serial; + this.log = log; +} + +LIFxBulbAccessory.prototype = { + getPower: function(callback){ + var that = this; + + lifx.listLights("all", function(body) { + var bulbs = JSON.parse(body); + + for(var i = 0; i < bulbs.length; i ++) { + var bulb = bulbs[i]; + + if(bulb.deviceId == that.deviceId) { + return bulb.state; + } + } + return "off"; + }); + + nest.fetchStatus(function (data) { + var device = data.shared[that.deviceId]; + that.log("Target temperature for " + this.name + " is: " + device.target_temperature); + callback(device.target_temperature); + }); + }, + setPower: function(state){ + var that = this; + this.log("Setting power state for heating cooling for " + this.name + " to: " + targetTemperatureType); + lifx.setPower("all", state, 1, function (body) { + this.log("body"); + }); + }, + + getServices: function() { + var that = this; + var chars= [{ + 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: "LIFx", + supportEvents: false, + supportBonjour: false, + manfDescription: "Manufacturer", + designedMaxLength: 255 + },{ + cType: types.MODEL_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: this.model, + supportEvents: false, + supportBonjour: false, + manfDescription: "Model", + designedMaxLength: 255 + },{ + cType: types.SERIAL_NUMBER_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: this.serial, + supportEvents: false, + supportBonjour: false, + manfDescription: "SN", + designedMaxLength: 255 + },{ + cType: types.IDENTIFY_CTYPE, + onUpdate: null, + perms: ["pw"], + format: "bool", + initialValue: true, + 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: false, + supportBonjour: false, + manfDescription: "Name of LIFx bulb", + designedMaxLength: 255 + }, { + cType: types.POWER_STATE_CTYPE, + onUpdate: function (value) { + that.setPower(value); + }, + onRead: function (callback) { + that.getPower(function (state) { + callback(state); + }); + }, + perms: ["pw", "pr", "ev"], + format: "int", + initialValue: 0, + supportEvents: false, + supportBonjour: false, + manfDescription: "Power state", + designedMinValue: 0, + designedMaxValue: 1, + designedMinStep: 1 + }] + }]; + return chars; + } +} + +module.exports.accessory = LIFxBulbAccessory; +module.exports.platform = LIFxPlatform; From 4b1637152219a27aadbad5a3cbed01918ab7206d Mon Sep 17 00:00:00 2001 From: Nelson Melo Date: Wed, 9 Sep 2015 15:31:11 -0400 Subject: [PATCH 55/63] Added LIFx on Readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4fae78b..f11dd75 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Since Siri supports devices added through HomeKit, this means that with Homebrid * _Siri, turn off the Speakers._ ([Sonos](http://www.sonos.com)) * _Siri, turn on the Dehumidifier._ ([WeMo](http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/)) * _Siri, turn on Away Mode._ ([Xfinity Home](http://www.comcast.com/home-security.html)) - * _Siri, turn on the living room lights._ ([Wink](http://www.wink.com), [SmartThings](http://www.smartthings.com), [X10](http://github.com/edc1591/rest-mochad), [Philips Hue](http://meethue.com), [LimitlessLED/MiLight/Easybulb](http://www.limitlessled.com/)) + * _Siri, turn on the living room lights._ ([Wink](http://www.wink.com), [SmartThings](http://www.smartthings.com), [X10](http://github.com/edc1591/rest-mochad), [Philips Hue](http://meethue.com), [LimitlessLED/MiLight/Easybulb](http://www.limitlessled.com/), [LIFx](http://www.lifx.com/)) * _Siri, set the movie scene._ ([Logitech Harmony](http://myharmony.com/)) If you would like to support any other devices, please write a shim and create a pull request and I'd be happy to add it to this official list. From 09f5e2bed0274e885b9c0198186f4492d23345fa Mon Sep 17 00:00:00 2001 From: David Parry Date: Thu, 10 Sep 2015 12:50:55 +1000 Subject: [PATCH 56/63] add support for WeMo motion sensor --- accessories/WeMo.js | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/accessories/WeMo.js b/accessories/WeMo.js index 26ef822..df16f56 100644 --- a/accessories/WeMo.js +++ b/accessories/WeMo.js @@ -30,6 +30,30 @@ WeMoAccessory.prototype.search = function() { }.bind(this)); } +WeMoAccessory.prototype.getMotion = function(callback) { + + if (!this.device) { + this.log("No '%s' device found (yet?)", this.wemoName); + callback(new Error("Device not found"), false); + return; + } + + this.log("Getting motion state on the '%s'...", this.wemoName); + + this.device.getBinaryState(function(err, result) { + if (!err) { + var binaryState = parseInt(result); + var powerOn = binaryState > 0; + this.log("Motion state for the '%s' is %s", this.wemoName, binaryState); + callback(null, powerOn); + } + else { + this.log("Error getting motion state on the '%s': %s", this.wemoName, err.message); + callback(err); + } + }.bind(this)); +} + WeMoAccessory.prototype.getPowerOn = function(callback) { if (!this.device) { @@ -122,6 +146,15 @@ WeMoAccessory.prototype.getServices = function() { return [garageDoorService]; } + else if (this.service == "MotionSensor") { + var motionSensorService = new Service.MotionSensor(this.name); + + motionSensorService + .getCharacteristic(Characteristic.MotionDetected) + .on('get', this.getMotion.bind(this)); + + return [motionSensorService]; + } else { throw new Error("Unknown service type '%s'", this.service); } From 3c35311c4aff446ec4b2b49e82df80714c26ddd8 Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Thu, 10 Sep 2015 06:55:07 +0200 Subject: [PATCH 57/63] Added Luminiscence sensors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Though there doesn’t seem to be much app support for it yet…and my math (% -> lux) is complete guesswork. --- platforms/ZWayServer.js | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/platforms/ZWayServer.js b/platforms/ZWayServer.js index 416d431..6c68de9 100644 --- a/platforms/ZWayServer.js +++ b/platforms/ZWayServer.js @@ -280,6 +280,9 @@ ZWayServerAccessory.prototype = { case "battery.Battery": services.push(new Service.BatteryService(vdev.metrics.title)); break; + case "sensorMultilevel.Luminiscence": + services.push(new Service.LightSensor(vdev.metrics.title)); + break; } var validServices =[]; @@ -310,6 +313,7 @@ ZWayServerAccessory.prototype = { map[(new Characteristic.BatteryLevel).UUID] = ["battery.Battery"]; map[(new Characteristic.StatusLowBattery).UUID] = ["battery.Battery"]; map[(new Characteristic.ChargingState).UUID] = ["battery.Battery"]; //TODO: Always a fixed result + map[(new Characteristic.CurrentAmbientLightLevel).UUID] = ["sensorMultilevel.Luminiscence"]; } if(cx instanceof Characteristic.Name) return vdevPreferred; @@ -381,6 +385,9 @@ ZWayServerAccessory.prototype = { callback(); }); }.bind(this)); + cx.on('change', function(ev){ + debug("Device " + vdev.metrics.title + ", characteristic " + cx.displayName + " changed from " + ev.oldValue + " to " + ev.newValue); + }); return cx; } @@ -497,6 +504,9 @@ ZWayServerAccessory.prototype = { callback(false, cx.zway_getValueFromVDev(result.data)); }); }.bind(this)); + cx.on('change', function(ev){ + debug("Device " + vdev.metrics.title + ", characteristic " + cx.displayName + " changed from " + ev.oldValue + " to " + ev.newValue); + }); } if(cx instanceof Characteristic.TargetDoorState){ @@ -568,7 +578,30 @@ ZWayServerAccessory.prototype = { //cx.readable = false; cx.writable = false; } - + + if(cx instanceof Characteristic.CurrentAmbientLightLevel){ + cx.zway_getValueFromVDev = function(vdev){ + if(vdev.metrics.scaleTitle === "%"){ + // Completely unscientific guess, based on test-fit data and Wikipedia real-world lux values. + // This will probably change! + return 0.0005 * (vdev.metrics.level^3.6); + } else { + return vdev.metrics.level; + } + }; + cx.value = cx.zway_getValueFromVDev(vdev); + cx.on('get', function(callback, context){ + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + this.getVDev(vdev).then(function(result){ + debug("Got value: " + cx.zway_getValueFromVDev(result.data) + ", for " + vdev.metrics.title + "."); + callback(false, cx.zway_getValueFromVDev(result.data)); + }); + }.bind(this)); + cx.on('change', function(ev){ + debug("Device " + vdev.metrics.title + ", characteristic " + cx.displayName + " changed from " + ev.oldValue + " to " + ev.newValue); + }); + return cx; + } } , configureService: function(service, vdev){ @@ -612,11 +645,17 @@ ZWayServerAccessory.prototype = { } // Odds and ends...if there are sensors that haven't been used, add services for them... + var tempSensor = this.devDesc.types["sensorMultilevel.Temperature"] !== undefined ? this.devDesc.devices[this.devDesc.types["sensorMultilevel.Temperature"]] : false; if(tempSensor && !this.platform.cxVDevMap[tempSensor.id]){ services = services.concat(this.getVDevServices(tempSensor)); } + var lightSensor = this.devDesc.types["sensorMultilevel.Luminiscence"] !== undefined ? this.devDesc.devices[this.devDesc.types["sensorMultilevel.Luminiscence"]] : false; + if(lightSensor && !this.platform.cxVDevMap[lightSensor.id]){ + services = services.concat(this.getVDevServices(lightSensor)); + } + debug("Loaded services for " + this.name); return services; } From 7e6df6191e923d44b8b829a89524ba20520d36e5 Mon Sep 17 00:00:00 2001 From: David Parry Date: Thu, 10 Sep 2015 22:19:41 +1000 Subject: [PATCH 58/63] [LiFX] fix/enhance the LiFX platform --- platforms/LIFx.js | 218 +++++++++++++++++++++------------------------- 1 file changed, 98 insertions(+), 120 deletions(-) diff --git a/platforms/LIFx.js b/platforms/LIFx.js index 55d66f2..8f4ef0d 100644 --- a/platforms/LIFx.js +++ b/platforms/LIFx.js @@ -1,4 +1,5 @@ -var types = require("HAP-NodeJS/accessories/types.js"); +var Service = require("HAP-NodeJS").Service; +var Characteristic = require("HAP-NodeJS").Characteristic; var lifxObj = require('lifx-api'); var lifx; @@ -23,14 +24,7 @@ LIFxPlatform.prototype = { var bulbs = JSON.parse(body); for(var i = 0; i < bulbs.length; i ++) { - var bulb = bulbs[i]; - var accessory = new LIFxBulbAccessory( - that.log, - bulb.label, - bulb.uuid, - bulb.model, - bulb.id - ); + var accessory = new LIFxBulbAccessory(that.log, bulbs[i]); foundAccessories.push(accessory); } callback(foundAccessories) @@ -38,135 +32,119 @@ LIFxPlatform.prototype = { } } -function LIFxBulbAccessory(log, label, serial, model, deviceId) { +function LIFxBulbAccessory(log, bulb) { // device info - this.name = label; - this.model = model; - this.deviceId = deviceId; - this.serial = serial; + this.name = bulb.label; + this.model = bulb.product_name; + this.deviceId = bulb.id; + this.serial = bulb.uuid; + this.capabilities = bulb.capabilities; this.log = log; } LIFxBulbAccessory.prototype = { - getPower: function(callback){ + get: function(type, callback){ var that = this; - lifx.listLights("all", function(body) { - var bulbs = JSON.parse(body); + lifx.listLights("id:"+ that.deviceId, function(body) { + var bulb = JSON.parse(body); - for(var i = 0; i < bulbs.length; i ++) { - var bulb = bulbs[i]; - - if(bulb.deviceId == that.deviceId) { - return bulb.state; - } + if (bulb.connected != true) { + callback(new Error("Device not found"), false); + return; } - return "off"; - }); - nest.fetchStatus(function (data) { - var device = data.shared[that.deviceId]; - that.log("Target temperature for " + this.name + " is: " + device.target_temperature); - callback(device.target_temperature); + switch(type) { + case "power": + callback(null, bulb.power == "on" ? 1 : 0); + break; + case "brightness": + callback(null, Math.round(bulb.brightness * 100)); + break; + case "hue": + callback(null, bulb.color.hue); + break; + case "saturation": + callback(null, Math.round(bulb.color.saturation * 100)); + break; + } }); }, - setPower: function(state){ + identify: function(callback) { var that = this; - this.log("Setting power state for heating cooling for " + this.name + " to: " + targetTemperatureType); - lifx.setPower("all", state, 1, function (body) { - this.log("body"); + + lifx.breatheEffect("id:"+ that.deviceId, 'green', null, 1, 3, false, true, 0.5, function (body) { + callback(); + }); + }, + setColor: function(type, state, callback){ + var that = this; + var color; + + switch(type) { + case "brightness": + color = "brightness:" + (state / 100); + break; + case "hue": + color = "hue:" + state; + break; + case "saturation": + color = "saturation:" + (state / 100); + break; + } + + lifx.setColor("id:"+ that.deviceId, color, 0, null, function (body) { + callback(); + }); + }, + setPower: function(state, callback){ + var that = this; + + lifx.setPower("id:"+ that.deviceId, (state == 1 ? "on" : "off"), 0, function (body) { + callback(); }); }, getServices: function() { var that = this; - var chars= [{ - 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: "LIFx", - supportEvents: false, - supportBonjour: false, - manfDescription: "Manufacturer", - designedMaxLength: 255 - },{ - cType: types.MODEL_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: this.model, - supportEvents: false, - supportBonjour: false, - manfDescription: "Model", - designedMaxLength: 255 - },{ - cType: types.SERIAL_NUMBER_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: this.serial, - supportEvents: false, - supportBonjour: false, - manfDescription: "SN", - designedMaxLength: 255 - },{ - cType: types.IDENTIFY_CTYPE, - onUpdate: null, - perms: ["pw"], - format: "bool", - initialValue: true, - 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: false, - supportBonjour: false, - manfDescription: "Name of LIFx bulb", - designedMaxLength: 255 - }, { - cType: types.POWER_STATE_CTYPE, - onUpdate: function (value) { - that.setPower(value); - }, - onRead: function (callback) { - that.getPower(function (state) { - callback(state); - }); - }, - perms: ["pw", "pr", "ev"], - format: "int", - initialValue: 0, - supportEvents: false, - supportBonjour: false, - manfDescription: "Power state", - designedMinValue: 0, - designedMaxValue: 1, - designedMinStep: 1 - }] - }]; - return chars; + var services = [] + var service = new Service.Lightbulb(this.name); + + service + .getCharacteristic(Characteristic.On) + .on('identify', function(callback) {}) + .on('get', function(callback) { that.get("power", callback);}) + .on('set', function(value, callback) {that.setPower(value, callback);}); + + service + .addCharacteristic(Characteristic.Brightness) + .on('get', function(callback) { that.get("brightness", callback);}) + .on('set', function(value, callback) { that.setColor("brightness", value, callback);}); + + if (this.capabilities.has_color == true) { + service + .addCharacteristic(Characteristic.Hue) + .on('get', function(callback) { that.get("hue", callback);}) + .on('set', function(value, callback) { that.setColor("hue", value, callback);}); + + service + .addCharacteristic(Characteristic.Saturation) + .on('get', function(callback) { that.get("saturation", callback);}) + .on('set', function(value, callback) { that.setColor("saturation", value, callback);}); + } + + services.push(service); + + service = new Service.AccessoryInformation(); + + service + .setCharacteristic(Characteristic.Manufacturer, "LiFX") + .setCharacteristic(Characteristic.Model, this.model) + .setCharacteristic(Characteristic.SerialNumber, this.serial); + + services.push(service); + + return services; } } From 17fc8f1829ee884c097cda3127122858220c509a Mon Sep 17 00:00:00 2001 From: David Parry Date: Fri, 11 Sep 2015 13:04:09 +1000 Subject: [PATCH 59/63] implement the LiFX LAN API as a configurable option for higher lantency connections --- package.json | 1 + platforms/LIFx.js | 245 +++++++++++++++++++++++++++++++++++++--------- 2 files changed, 200 insertions(+), 46 deletions(-) diff --git a/package.json b/package.json index 8427666..3732b9d 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "harmonyhubjs-client": "^1.1.4", "harmonyhubjs-discover": "git+https://github.com/swissmanu/harmonyhubjs-discover.git", "lifx-api": "^1.0.1", + "lifx": "https://github.com/magicmonkey/lifxjs.git", "mdns": "^2.2.4", "node-hue-api": "^1.0.5", "node-icontrol": "^0.1.4", diff --git a/platforms/LIFx.js b/platforms/LIFx.js index 8f4ef0d..62167f6 100644 --- a/platforms/LIFx.js +++ b/platforms/LIFx.js @@ -1,16 +1,45 @@ +'use strict'; + +// LiFX Platform Shim for HomeBridge +// +// Remember to add platform to config.json. Example: +// "platforms": [ +// { +// "platform": "LIFx", // required +// "name": "LIFx", // required +// "access_token": "access token", // required +// "use_lan": "true" // optional set to "true" (gets and sets over the lan) or "get" (gets only over the lan) +// } +// ], +// +// When you attempt to add a device, it will ask for a "PIN code". +// The default code for all HomeBridge accessories is 031-45-154. +// + var Service = require("HAP-NodeJS").Service; var Characteristic = require("HAP-NodeJS").Characteristic; -var lifxObj = require('lifx-api'); -var lifx; +var lifxRemoteObj = require('lifx-api'); +var lifx_remote; + +var lifxLanObj; +var lifx_lan; +var use_lan; function LIFxPlatform(log, config){ + // auth info + this.access_token = config["access_token"]; - // auth info - this.access_token = config["access_token"]; + lifx_remote = new lifxRemoteObj(this.access_token); - lifx = new lifxObj(this.access_token); + // use remote or lan api ? + use_lan = config["use_lan"] || false; - this.log = log; + if (use_lan != false) { + lifxLanObj = require('lifx'); + lifx_lan = lifxLanObj.init(); + } + + this.log = log; } LIFxPlatform.prototype = { @@ -20,7 +49,7 @@ LIFxPlatform.prototype = { var that = this; var foundAccessories = []; - lifx.listLights("all", function(body) { + lifx_remote.listLights("all", function(body) { var bulbs = JSON.parse(body); for(var i = 0; i < bulbs.length; i ++) { @@ -33,20 +62,54 @@ LIFxPlatform.prototype = { } function LIFxBulbAccessory(log, bulb) { - // device info - this.name = bulb.label; - this.model = bulb.product_name; - this.deviceId = bulb.id; - this.serial = bulb.uuid; - this.capabilities = bulb.capabilities; - this.log = log; + // device info + this.name = bulb.label; + this.model = bulb.product_name; + this.deviceId = bulb.id; + this.serial = bulb.uuid; + this.capabilities = bulb.capabilities; + this.log = log; } LIFxBulbAccessory.prototype = { - get: function(type, callback){ + getLan: function(type, callback){ var that = this; - lifx.listLights("id:"+ that.deviceId, function(body) { + if (!lifx_lan.bulbs[this.deviceId]) { + callback(new Error("Device not found"), false); + return; + } + + lifx_lan.requestStatus(); + lifx_lan.on('bulbstate', function(bulb) { + if (callback == null) { + return; + } + + if (bulb.addr.toString('hex') == that.deviceId) { + switch(type) { + case "power": + callback(null, bulb.state.power > 0); + break; + case "brightness": + callback(null, Math.round(bulb.state.brightness * 100 / 65535)); + break; + case "hue": + callback(null, Math.round(bulb.state.hue * 360 / 65535)); + break; + case "saturation": + callback(null, Math.round(bulb.state.saturation * 100 / 65535)); + break; + } + + callback = null + } + }); + }, + getRemote: function(type, callback){ + var that = this; + + lifx_remote.listLights("id:"+ that.deviceId, function(body) { var bulb = JSON.parse(body); if (bulb.connected != true) { @@ -71,66 +134,156 @@ LIFxBulbAccessory.prototype = { }); }, identify: function(callback) { - var that = this; - - lifx.breatheEffect("id:"+ that.deviceId, 'green', null, 1, 3, false, true, 0.5, function (body) { + lifx_remote.breatheEffect("id:"+ this.deviceId, 'green', null, 1, 3, false, true, 0.5, function (body) { callback(); }); }, - setColor: function(type, state, callback){ - var that = this; + setLanColor: function(type, value, callback){ + var bulb = lifx_lan.bulbs[this.deviceId]; + + if (!bulb) { + callback(new Error("Device not found"), false); + return; + } + + var state = { + hue: bulb.state.hue, + saturation: bulb.state.saturation, + brightness: bulb.state.brightness, + kelvin: bulb.state.kelvin + }; + + var scale = type == "hue" ? 360 : 100; + + state[type] = Math.round(value * 65535 / scale) & 0xffff; + lifx_lan.lightsColour(state.hue, state.saturation, state.brightness, state.kelvin, 0, bulb); + + callback(null); + }, + setLanPower: function(state, callback){ + var bulb = lifx_lan.bulbs[this.deviceId]; + + if (!bulb) { + callback(new Error("Device not found"), false); + return; + } + + if (state) { + lifx_lan.lightsOn(bulb); + } + else { + lifx_lan.lightsOff(bulb); + } + + callback(null); + }, + setRemoteColor: function(type, value, callback){ var color; switch(type) { case "brightness": - color = "brightness:" + (state / 100); + color = "brightness:" + (value / 100); break; case "hue": - color = "hue:" + state; + color = "hue:" + value; break; case "saturation": - color = "saturation:" + (state / 100); + color = "saturation:" + (value / 100); break; } - lifx.setColor("id:"+ that.deviceId, color, 0, null, function (body) { + lifx_remote.setColor("id:"+ this.deviceId, color, 0, null, function (body) { callback(); }); }, - setPower: function(state, callback){ + setRemotePower: function(state, callback){ var that = this; - lifx.setPower("id:"+ that.deviceId, (state == 1 ? "on" : "off"), 0, function (body) { + lifx_remote.setPower("id:"+ that.deviceId, (state == 1 ? "on" : "off"), 0, function (body) { callback(); }); }, - getServices: function() { var that = this; var services = [] var service = new Service.Lightbulb(this.name); - service - .getCharacteristic(Characteristic.On) - .on('identify', function(callback) {}) - .on('get', function(callback) { that.get("power", callback);}) - .on('set', function(value, callback) {that.setPower(value, callback);}); + switch(use_lan) { + case true: + case "true": + // gets and sets over the lan api + service + .getCharacteristic(Characteristic.On) + .on('identify', function(callback) {}) + .on('get', function(callback) { that.getLan("power", callback);}) + .on('set', function(value, callback) {that.setLanPower(value, callback);}); - service - .addCharacteristic(Characteristic.Brightness) - .on('get', function(callback) { that.get("brightness", callback);}) - .on('set', function(value, callback) { that.setColor("brightness", value, callback);}); + service + .addCharacteristic(Characteristic.Brightness) + .on('get', function(callback) { that.getLan("brightness", callback);}) + .on('set', function(value, callback) { that.setLanColor("brightness", value, callback);}); - if (this.capabilities.has_color == true) { - service - .addCharacteristic(Characteristic.Hue) - .on('get', function(callback) { that.get("hue", callback);}) - .on('set', function(value, callback) { that.setColor("hue", value, callback);}); + if (this.capabilities.has_color == true) { + service + .addCharacteristic(Characteristic.Hue) + .on('get', function(callback) { that.getLan("hue", callback);}) + .on('set', function(value, callback) { that.setLanColor("hue", value, callback);}); - service - .addCharacteristic(Characteristic.Saturation) - .on('get', function(callback) { that.get("saturation", callback);}) - .on('set', function(value, callback) { that.setColor("saturation", value, callback);}); + service + .addCharacteristic(Characteristic.Saturation) + .on('get', function(callback) { that.getLan("saturation", callback);}) + .on('set', function(value, callback) { that.setLanColor("saturation", value, callback);}); + } + break; + case "get": + // gets over the lan api, sets over the remote api + service + .getCharacteristic(Characteristic.On) + .on('identify', function(callback) {}) + .on('get', function(callback) { that.getLan("power", callback);}) + .on('set', function(value, callback) {that.setRemotePower(value, callback);}); + + service + .addCharacteristic(Characteristic.Brightness) + .on('get', function(callback) { that.getLan("brightness", callback);}) + .on('set', function(value, callback) { that.setRemoteColor("brightness", value, callback);}); + + if (this.capabilities.has_color == true) { + service + .addCharacteristic(Characteristic.Hue) + .on('get', function(callback) { that.getLan("hue", callback);}) + .on('set', function(value, callback) { that.setRemoteColor("hue", value, callback);}); + + service + .addCharacteristic(Characteristic.Saturation) + .on('get', function(callback) { that.getLan("saturation", callback);}) + .on('set', function(value, callback) { that.setRemoteColor("saturation", value, callback);}); + } + break; + default: + // gets and sets over the remote api + service + .getCharacteristic(Characteristic.On) + .on('identify', function(callback) {}) + .on('get', function(callback) { that.getRemote("power", callback);}) + .on('set', function(value, callback) {that.setRemotePower(value, callback);}); + + service + .addCharacteristic(Characteristic.Brightness) + .on('get', function(callback) { that.getRemote("brightness", callback);}) + .on('set', function(value, callback) { that.setRemoteColor("brightness", value, callback);}); + + if (this.capabilities.has_color == true) { + service + .addCharacteristic(Characteristic.Hue) + .on('get', function(callback) { that.getRemote("hue", callback);}) + .on('set', function(value, callback) { that.setRemoteColor("hue", value, callback);}); + + service + .addCharacteristic(Characteristic.Saturation) + .on('get', function(callback) { that.getRemote("saturation", callback);}) + .on('set', function(value, callback) { that.setRemoteColor("saturation", value, callback);}); + } } services.push(service); From 3da6fcb5108cd1405633a3833545670682a1cbf7 Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Fri, 11 Sep 2015 05:43:33 +0200 Subject: [PATCH 60/63] FIX: Prevent light sensor values from going out of bounds --- platforms/ZWayServer.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/platforms/ZWayServer.js b/platforms/ZWayServer.js index 6c68de9..77842a9 100644 --- a/platforms/ZWayServer.js +++ b/platforms/ZWayServer.js @@ -584,7 +584,10 @@ ZWayServerAccessory.prototype = { if(vdev.metrics.scaleTitle === "%"){ // Completely unscientific guess, based on test-fit data and Wikipedia real-world lux values. // This will probably change! - return 0.0005 * (vdev.metrics.level^3.6); + var lux = 0.0005 * (vdev.metrics.level^3.6); + if(lux < cx.minimumValue) return cx.minimumValue; + if(lux > cx.maximumValue) return cx.maximumValue; + return lux; } else { return vdev.metrics.level; } From 7d5a992c98173d8486df8ae4ee68a15db346119f Mon Sep 17 00:00:00 2001 From: David Parry Date: Fri, 11 Sep 2015 17:30:25 +1000 Subject: [PATCH 61/63] minor cleanup --- platforms/LIFx.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/platforms/LIFx.js b/platforms/LIFx.js index 62167f6..79988eb 100644 --- a/platforms/LIFx.js +++ b/platforms/LIFx.js @@ -214,7 +214,6 @@ LIFxBulbAccessory.prototype = { // gets and sets over the lan api service .getCharacteristic(Characteristic.On) - .on('identify', function(callback) {}) .on('get', function(callback) { that.getLan("power", callback);}) .on('set', function(value, callback) {that.setLanPower(value, callback);}); @@ -239,7 +238,6 @@ LIFxBulbAccessory.prototype = { // gets over the lan api, sets over the remote api service .getCharacteristic(Characteristic.On) - .on('identify', function(callback) {}) .on('get', function(callback) { that.getLan("power", callback);}) .on('set', function(value, callback) {that.setRemotePower(value, callback);}); @@ -264,7 +262,6 @@ LIFxBulbAccessory.prototype = { // gets and sets over the remote api service .getCharacteristic(Characteristic.On) - .on('identify', function(callback) {}) .on('get', function(callback) { that.getRemote("power", callback);}) .on('set', function(value, callback) {that.setRemotePower(value, callback);}); @@ -291,7 +288,7 @@ LIFxBulbAccessory.prototype = { service = new Service.AccessoryInformation(); service - .setCharacteristic(Characteristic.Manufacturer, "LiFX") + .setCharacteristic(Characteristic.Manufacturer, "LIFX") .setCharacteristic(Characteristic.Model, this.model) .setCharacteristic(Characteristic.SerialNumber, this.serial); From 17573524ce55c91db3618c319dabe132d356f0de Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Sat, 12 Sep 2015 14:23:17 +0200 Subject: [PATCH 62/63] Prep for initial release. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cleaned up some more old cruft, added config-sample.json entry, and now there’s a different default grouping of characteristics, which makes for more optional characteristics on fewer services. The older behavior (more services per accessory) can be switched on in config.json. The new default works better in Eve, other clients not so much. --- config-sample.json | 8 ++++++++ platforms/ZWayServer.js | 1 - 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/config-sample.json b/config-sample.json index 4245bd5..3f8b136 100644 --- a/config-sample.json +++ b/config-sample.json @@ -71,6 +71,14 @@ "platform": "YamahaAVR", "play_volume": -35, "setMainInputTo": "AirPlay" + }, + { + "platform": "ZWayServer", + "url": "http://192.168.1.10:8083/", + "login": "zwayusername", + "password": "zwayuserpassword", + "poll_interval": 2, + "split_services": false } ], diff --git a/platforms/ZWayServer.js b/platforms/ZWayServer.js index 77842a9..799c08d 100644 --- a/platforms/ZWayServer.js +++ b/platforms/ZWayServer.js @@ -56,7 +56,6 @@ function ZWayServerPlatform(log, config){ this.login = config["login"]; this.password = config["password"]; this.name_overrides = config["name_overrides"]; - this.batteryLow = config["battery_low_level"]; this.pollInterval = config["poll_interval"] || 2; this.lastUpdate = 0; this.cxVDevMap = {}; From 2b1aa5e296e2f038636dcb96b7806456f1aa01a7 Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Sat, 12 Sep 2015 14:24:31 +0200 Subject: [PATCH 63/63] Erm...missing modifications that should have been in the last commit. --- platforms/ZWayServer.js | 138 ++++++++++++++++++++-------------------- 1 file changed, 69 insertions(+), 69 deletions(-) diff --git a/platforms/ZWayServer.js b/platforms/ZWayServer.js index 799c08d..322b9fb 100644 --- a/platforms/ZWayServer.js +++ b/platforms/ZWayServer.js @@ -6,57 +6,15 @@ var request = require("request"); var tough = require('tough-cookie'); var Q = require("q"); -var zwshkDeviceClasses = [ - { - primaryType: "switchBinary", - subTypes: { - "battery": true, - "sensorMultilevel.Electric": true - }, - tcType: types.SWITCH_TCTYPE - } - , - { - primaryType: "thermostat", - subTypes: { - "sensorMultiLevel.Temperature": true, - "battery": true - }, - tcType: types.THERMOSTAT_TCTYPE - } - , - { - primaryType: "sensorBinary.Door/Window", - subTypes: { - "battery": true - }, - tcType: types.SENSOR_TCTYPE - } - , - { - primaryType: "sensorMultilevel.Temperature", - subTypes: { - "battery": true - }, - tcType: types.SENSOR_TCTYPE - } - , - { - primaryType: "switchMultilevel", - subTypes: { - "battery": true - }, - tcType: types.LIGHTBULB_TCTYPE - } -]; - function ZWayServerPlatform(log, config){ this.log = log; this.url = config["url"]; this.login = config["login"]; this.password = config["password"]; this.name_overrides = config["name_overrides"]; + this.batteryLow = config["battery_low_level"] || 15; this.pollInterval = config["poll_interval"] || 2; + this.splitServices= config["split_services"] || false; this.lastUpdate = 0; this.cxVDevMap = {}; this.vDevStore = {}; @@ -79,7 +37,6 @@ ZWayServerPlatform.prototype = { opts.headers = { "Cookie": "ZWAYSession=" + this.sessionId }; -//opts.proxy = 'http://localhost:8888'; request(opts, function(error, response, body){ if(response.statusCode == 401){ @@ -87,7 +44,6 @@ ZWayServerPlatform.prototype = { request({ method: "POST", url: that.url + 'ZAutomation/api/v1/login', -//proxy: 'http://localhost:8888', body: { //JSON.stringify({ "form": true, "login": that.login, @@ -130,6 +86,16 @@ ZWayServerPlatform.prototype = { accessories: function(callback) { debug("Fetching Z-Way devices..."); + //TODO: Unify this with getVDevServices, so there's only one place with mapping between service and vDev type. + //Note: Order matters! + var primaryDeviceClasses = [ + "switchBinary", + "thermostat", + "sensorBinary.Door/Window", + "sensorMultilevel.Temperature", + "switchMultilevel" + ]; + var that = this; var foundAccessories = []; @@ -143,7 +109,7 @@ ZWayServerPlatform.prototype = { var groupedDevices = {}; for(var i = 0; i < devices.length; i++){ var vdev = devices[i]; - if(vdev.tags.indexOf("HomeBridge:Skip") >= 0) { debug("Tag says skip!"); continue; } + if(vdev.tags.indexOf("Homebridge:Skip") >= 0) { debug("Tag says skip!"); continue; } var gdid = vdev.id.replace(/^(.*?)_zway_(\d+-\d+)-\d.*/, '$1_$2'); var gd = groupedDevices[gdid] || (groupedDevices[gdid] = {devices: [], types: {}, primary: undefined}); gd.devices.push(vdev); @@ -162,13 +128,13 @@ ZWayServerPlatform.prototype = { } var accessory = null; - for(var ti = 0; ti < zwshkDeviceClasses.length; ti++){ - if(gd.types[zwshkDeviceClasses[ti].primaryType] !== undefined){ - gd.primary = gd.types[zwshkDeviceClasses[ti].primaryType]; + for(var ti = 0; ti < primaryDeviceClasses.length; ti++){ + if(gd.types[primaryDeviceClasses[ti]] !== undefined){ + gd.primary = gd.types[primaryDeviceClasses[ti]]; var pd = gd.devices[gd.primary]; var name = pd.metrics && pd.metrics.title ? pd.metrics.title : pd.id; - debug("Using class with primaryType " + zwshkDeviceClasses[ti].primaryType + ", " + name + " (" + pd.id + ") as primary."); - accessory = new ZWayServerAccessory(name, zwshkDeviceClasses[ti], gd, that); + debug("Using primary device with type " + primaryDeviceClasses[ti] + ", " + name + " (" + pd.id + ") as primary."); + accessory = new ZWayServerAccessory(name, gd, that); break; } } @@ -230,10 +196,9 @@ ZWayServerPlatform.prototype = { } -function ZWayServerAccessory(name, dclass, devDesc, platform) { +function ZWayServerAccessory(name, devDesc, platform) { // device info this.name = name; - this.dclass = dclass; this.devDesc = devDesc; this.platform = platform; this.log = platform.log; @@ -295,6 +260,12 @@ ZWayServerAccessory.prototype = { , uuidToTypeKeyMap: null , + extraCharacteristicsMap: { + "battery.Battery": [Characteristic.BatteryLevel, Characteristic.StatusLowBattery], + "sensorMultilevel.Temperature": [Characteristic.CurrentTemperature, Characteristic.TemperatureDisplayUnits], + "sensorMultilevel.Luminiscence": [Characteristic.CurrentAmbientLightLevel] + } + , getVDevForCharacteristic: function(cx, vdevPreferred){ var map = this.uuidToTypeKeyMap; if(!map){ @@ -641,21 +612,50 @@ ZWayServerAccessory.prototype = { var services = [informationService]; services = services.concat(this.getVDevServices(this.devDesc.devices[this.devDesc.primary])); - - if(this.devDesc.types["battery.Battery"]){ - services = services.concat(this.getVDevServices(this.devDesc.devices[this.devDesc.types["battery.Battery"]])); - } - - // Odds and ends...if there are sensors that haven't been used, add services for them... - - var tempSensor = this.devDesc.types["sensorMultilevel.Temperature"] !== undefined ? this.devDesc.devices[this.devDesc.types["sensorMultilevel.Temperature"]] : false; - if(tempSensor && !this.platform.cxVDevMap[tempSensor.id]){ - services = services.concat(this.getVDevServices(tempSensor)); - } - - var lightSensor = this.devDesc.types["sensorMultilevel.Luminiscence"] !== undefined ? this.devDesc.devices[this.devDesc.types["sensorMultilevel.Luminiscence"]] : false; - if(lightSensor && !this.platform.cxVDevMap[lightSensor.id]){ - services = services.concat(this.getVDevServices(lightSensor)); + + if(this.platform.splitServices){ + if(this.devDesc.types["battery.Battery"]){ + services = services.concat(this.getVDevServices(this.devDesc.devices[this.devDesc.types["battery.Battery"]])); + } + + // Odds and ends...if there are sensors that haven't been used, add services for them... + + var tempSensor = this.devDesc.types["sensorMultilevel.Temperature"] !== undefined ? this.devDesc.devices[this.devDesc.types["sensorMultilevel.Temperature"]] : false; + if(tempSensor && !this.platform.cxVDevMap[tempSensor.id]){ + services = services.concat(this.getVDevServices(tempSensor)); + } + + var lightSensor = this.devDesc.types["sensorMultilevel.Luminiscence"] !== undefined ? this.devDesc.devices[this.devDesc.types["sensorMultilevel.Luminiscence"]] : false; + if(lightSensor && !this.platform.cxVDevMap[lightSensor.id]){ + services = services.concat(this.getVDevServices(lightSensor)); + } + } else { + // Everything outside the primary service gets added as optional characteristics... + var service = services[1]; + var existingCxUUIDs = {}; + for(var i = 0; i < service.characteristics.length; i++) existingCxUUIDs[service.characteristics[i].UUID] = true; + + for(var i = 0; i < this.devDesc.devices.length; i++){ + var vdev = this.devDesc.devices[i]; + if(this.platform.cxVDevMap[vdev.id]) continue; // Don't double-use anything + var extraCxClasses = this.extraCharacteristicsMap[ZWayServerPlatform.getVDevTypeKey(vdev)]; + var extraCxs = []; + if(!extraCxClasses || extraCxClasses.length === 0) continue; + for(var j = 0; j < extraCxClasses.length; j++){ + var cx = new extraCxClasses[j](); + if(existingCxUUIDs[cx.UUID]) continue; // Don't have two of the same Characteristic type in one service! + var vdev2 = this.getVDevForCharacteristic(cx, vdev); // Just in case...will probably return vdev. + if(!vdev2){ + // Uh oh... one of the extraCxClasses can't be configured! Abort all extras for this vdev! + extraCxs = []; // to wipe out any already setup cxs. + break; + } + this.configureCharacteristic(cx, vdev2); + extraCxs.push(cx); + } + for(var j = 0; j < extraCxs.length; j++) + service.addCharacteristic(extraCxs[j]); + } } debug("Loaded services for " + this.name);