From c6877193cc1c8346b5460f9c287d91845dc46363 Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Sat, 15 Aug 2015 06:17:58 +0200 Subject: [PATCH] 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;