From 5782ff997f38870ffaa68c7da94df1fb73f13c4e Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Fri, 7 Aug 2015 20:42:28 +0200 Subject: [PATCH 01/14] 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 02/14] 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 03/14] 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 04/14] 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 b56d9346c8c3ddd725ce9b5fcda8340b3784077f Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Sun, 30 Aug 2015 14:24:43 +0200 Subject: [PATCH 05/14] 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 30a705e79f0e9de2245f310080809c5bc164ca99 Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Wed, 2 Sep 2015 06:27:13 +0200 Subject: [PATCH 06/14] 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 a677edb2cf4c44d39fde54623f410711d1260897 Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Fri, 4 Sep 2015 06:18:23 +0200 Subject: [PATCH 07/14] 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 78987a775fc31436154ca62328ca6f4c0534f818 Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Mon, 7 Sep 2015 19:16:51 +0200 Subject: [PATCH 08/14] 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 4170b8a533bf5aaa3657afa45b6033817f4b7f8e Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Wed, 9 Sep 2015 06:28:17 +0200 Subject: [PATCH 09/14] 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 10/14] 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 3c35311c4aff446ec4b2b49e82df80714c26ddd8 Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Thu, 10 Sep 2015 06:55:07 +0200 Subject: [PATCH 11/14] 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 3da6fcb5108cd1405633a3833545670682a1cbf7 Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Fri, 11 Sep 2015 05:43:33 +0200 Subject: [PATCH 12/14] 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 17573524ce55c91db3618c319dabe132d356f0de Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Sat, 12 Sep 2015 14:23:17 +0200 Subject: [PATCH 13/14] 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 14/14] 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);