From aa6a39cf8360d6f7f83eabb39a46a356a54d58fc Mon Sep 17 00:00:00 2001 From: gizmocuz Date: Sun, 14 Jun 2015 14:27:33 +0200 Subject: [PATCH 01/10] - Implemented, optional configuration parameter for 'roomid' - Fixed: trying to add duplicate sensors --- platforms/Domoticz.js | 67 +++++++++++++++++++++++++++++++------------ 1 file changed, 49 insertions(+), 18 deletions(-) diff --git a/platforms/Domoticz.js b/platforms/Domoticz.js index fb09e9d..ee4e57c 100644 --- a/platforms/Domoticz.js +++ b/platforms/Domoticz.js @@ -17,7 +17,8 @@ // "platform": "Domoticz", // "name": "Domoticz", // "server": "127.0.0.1", -// "port": "8080" +// "port": "8080", +// "roomid": 123 (0=no roomplan) // } // ], // @@ -31,6 +32,10 @@ function DomoticzPlatform(log, config){ this.log = log; this.server = config["server"]; this.port = config["port"]; + this.roomid = 0; + if (typeof config["roomid"] != 'undefined') { + this.roomid = config["roomid"]; + } } function sortByKey(array, key) { @@ -46,25 +51,51 @@ DomoticzPlatform.prototype = { var that = this; var foundAccessories = []; - //Get Lights - request.get({ - url: "http://" + this.server + ":" + this.port + "/json.htm?type=devices&filter=light&used=true&order=Name", - json: true - }, function(err, response, json) { - if (!err && response.statusCode == 200) { - if (json['result'] != undefined) { - var sArray=sortByKey(json['result'],"Name"); - sArray.map(function(s) { - accessory = new DomoticzAccessory(that.log, that.server, that.port, false, s.idx, s.Name, s.HaveDimmer, s.MaxDimLevel, (s.SubType=="RGB")||(s.SubType=="RGBW")); - foundAccessories.push(accessory); - }) + if (this.roomid == 0) { + //Get Lights + request.get({ + url: "http://" + this.server + ":" + this.port + "/json.htm?type=devices&filter=light&used=true&order=Name", + json: true + }, function(err, response, json) { + if (!err && response.statusCode == 200) { + if (json['result'] != undefined) { + var sArray=sortByKey(json['result'],"Name"); + sArray.map(function(s) { + accessory = new DomoticzAccessory(that.log, that.server, that.port, false, s.idx, s.Name, s.HaveDimmer, s.MaxDimLevel, (s.SubType=="RGB")||(s.SubType=="RGBW")); + foundAccessories.push(accessory); + }) + } + callback(foundAccessories); + } else { + that.log("There was a problem connecting to Domoticz."); } - callback(foundAccessories); - } else { - that.log("There was a problem connecting to Domoticz."); - } - }); + }); + } + else { + //Get all devices specified in the room + request.get({ + url: "http://" + this.server + ":" + this.port + "/json.htm?type=devices&plan=" + this.roomid, + json: true + }, function(err, response, json) { + if (!err && response.statusCode == 200) { + if (json['result'] != undefined) { + var sArray=sortByKey(json['result'],"Name"); + sArray.map(function(s) { + //only accept switches for now + if (typeof s.SwitchType != 'undefined') { + accessory = new DomoticzAccessory(that.log, that.server, that.port, false, s.idx, s.Name, s.HaveDimmer, s.MaxDimLevel, (s.SubType=="RGB")||(s.SubType=="RGBW")); + foundAccessories.push(accessory); + } + }) + } + callback(foundAccessories); + } else { + that.log("There was a problem connecting to Domoticz."); + } + }); + } //Get Scenes + foundAccessories = []; request.get({ url: "http://" + this.server + ":" + this.port + "/json.htm?type=scenes", json: true From 27d18abefe7507a69a33dff03b2a8707046f821b Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Wed, 24 Jun 2015 21:22:03 -0400 Subject: [PATCH 02/10] bump the version of HAP-NodeJS to get onRead support --- lib/HAP-NodeJS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/HAP-NodeJS b/lib/HAP-NodeJS index 19a4bee..40de0e9 160000 --- a/lib/HAP-NodeJS +++ b/lib/HAP-NodeJS @@ -1 +1 @@ -Subproject commit 19a4bee7d82674ac0051706687def1e3570c2b68 +Subproject commit 40de0e9a32eeff2986495faf78a8018d339cd52b From 847f0731c81498511fd6e08584f6b7e5ac04f68f Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Wed, 24 Jun 2015 22:20:12 -0400 Subject: [PATCH 03/10] read in the onRead --- app.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app.js b/app.js index 15828ab..4f94b23 100644 --- a/app.js +++ b/app.js @@ -123,6 +123,7 @@ function createHAPServer(name, services) { //loop through characteristics for (var k = 0; k < services[j].characteristics.length; k++) { var options = { + onRead: services[j].characteristics[k].onRead, type: services[j].characteristics[k].cType, perms: services[j].characteristics[k].perms, format: services[j].characteristics[k].format, From 08ce5a9ecc84dd811570ee43c44dc0515b09c921 Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Thu, 25 Jun 2015 01:42:21 -0400 Subject: [PATCH 04/10] bump HAP-NodeJS again to pull in async onreads --- lib/HAP-NodeJS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/HAP-NodeJS b/lib/HAP-NodeJS index 40de0e9..18fc477 160000 --- a/lib/HAP-NodeJS +++ b/lib/HAP-NodeJS @@ -1 +1 @@ -Subproject commit 40de0e9a32eeff2986495faf78a8018d339cd52b +Subproject commit 18fc4770c6c3bb5de4a8a2b23b7bafda36219290 From 396b56e3a535ad1f946f4898811eb8431cd539bf Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Thu, 25 Jun 2015 02:00:37 -0400 Subject: [PATCH 05/10] add methods to Wink accessory to fetch brightness and power state --- platforms/Wink.js | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/platforms/Wink.js b/platforms/Wink.js index f7a36f1..58b09a9 100644 --- a/platforms/Wink.js +++ b/platforms/Wink.js @@ -56,6 +56,40 @@ function WinkAccessory(log, device) { } WinkAccessory.prototype = { + getPowerState: function(callback){ + if (!this.device) { + this.log("No '"+this.name+"' device found (yet?)"); + return; + } + + var that = this; + + this.log("checking power state for: " + this.name); + wink.user().device(this.name, function(light_obj){ + powerState = light_obj.desired_state.powered + that.log("power state for " + that.name + " is: " + powerState) + callback(powerState); + }); + + + }, + + getBrightness: function(callback){ + if (!this.device) { + this.log("No '"+this.name+"' device found (yet?)"); + return; + } + + var that = this; + + this.log("checking brightness level for: " + this.name); + wink.user().device(this.name, function(light_obj){ + level = light_obj.desired_state.brightness * 100 + that.log("brightness level for " + that.name + " is: " + level) + callback(level); + }); + + }, setPowerState: function(powerOn) { if (!this.device) { From b147a26738794c7c322ff8fcd9ecd0aad18f91d7 Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Thu, 25 Jun 2015 02:00:52 -0400 Subject: [PATCH 06/10] add onRead functions to fetch state --- platforms/Wink.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/platforms/Wink.js b/platforms/Wink.js index 58b09a9..fd0fc53 100644 --- a/platforms/Wink.js +++ b/platforms/Wink.js @@ -209,6 +209,11 @@ WinkAccessory.prototype = { },{ cType: types.POWER_STATE_CTYPE, onUpdate: function(value) { that.setPowerState(value); }, + onRead: function(callback) { + that.getPowerState(function(powerState){ + callback(powerState); + }); + }, perms: ["pw","pr","ev"], format: "bool", initialValue: 0, @@ -219,6 +224,11 @@ WinkAccessory.prototype = { },{ cType: types.BRIGHTNESS_CTYPE, onUpdate: function(value) { that.setBrightness(value); }, + onRead: function(callback) { + that.getBrightness(function(level){ + callback(level); + }); + }, perms: ["pw","pr","ev"], format: "int", initialValue: 0, From 5e89bbe74d3192aa2e64f4495306dd05e71d0d52 Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Thu, 25 Jun 2015 02:01:01 -0400 Subject: [PATCH 07/10] clean up --- platforms/Wink.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/platforms/Wink.js b/platforms/Wink.js index fd0fc53..7b6083f 100644 --- a/platforms/Wink.js +++ b/platforms/Wink.js @@ -208,7 +208,9 @@ WinkAccessory.prototype = { designedMaxLength: 255 },{ cType: types.POWER_STATE_CTYPE, - onUpdate: function(value) { that.setPowerState(value); }, + onUpdate: function(value) { + that.setPowerState(value); + }, onRead: function(callback) { that.getPowerState(function(powerState){ callback(powerState); @@ -223,7 +225,9 @@ WinkAccessory.prototype = { designedMaxLength: 1 },{ cType: types.BRIGHTNESS_CTYPE, - onUpdate: function(value) { that.setBrightness(value); }, + onUpdate: function(value) { + that.setBrightness(value); + }, onRead: function(callback) { that.getBrightness(function(level){ callback(level); From e3786b183611c9799ffd08f99519d35c4911d4a0 Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Fri, 26 Jun 2015 16:19:38 -0400 Subject: [PATCH 08/10] bump HAP-NodeJS to get the final async onRead stuff --- lib/HAP-NodeJS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/HAP-NodeJS b/lib/HAP-NodeJS index 18fc477..b130842 160000 --- a/lib/HAP-NodeJS +++ b/lib/HAP-NodeJS @@ -1 +1 @@ -Subproject commit 18fc4770c6c3bb5de4a8a2b23b7bafda36219290 +Subproject commit b130842359214062eb61a220577ebd7de98d0dd9 From 38d621a13fe770dff59a73e54c22eda9cc9d5a4d Mon Sep 17 00:00:00 2001 From: Nick Farina Date: Fri, 26 Jun 2015 13:26:04 -0700 Subject: [PATCH 09/10] Lockitron support for onRead "Is my front door locked?" --- accessories/Lockitron.js | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/accessories/Lockitron.js b/accessories/Lockitron.js index 8aedbf0..05c4984 100644 --- a/accessories/Lockitron.js +++ b/accessories/Lockitron.js @@ -9,11 +9,38 @@ function LockitronAccessory(log, config) { } LockitronAccessory.prototype = { + getState: function(callback) { + this.log("Getting current state..."); + + var that = this; + + var query = { + access_token: this.accessToken + }; + + request.get({ + url: "https://api.lockitron.com/v2/locks/"+this.lockID, + qs: query + }, function(err, response, body) { + + if (!err && response.statusCode == 200) { + var json = JSON.parse(body); + var state = json.state; // "lock" or "unlock" + var locked = state == "lock" + callback(locked); + } + else { + that.log("Error getting state (status code "+response.statusCode+"): " + err) + callback(undefined); + } + }); + }, + setState: function(state) { this.log("Set state to " + state); var lockitronState = (state == 1) ? "lock" : "unlock"; - var that = this; + var that = this; var query = { access_token: this.accessToken, @@ -103,6 +130,7 @@ LockitronAccessory.prototype = { designedMaxLength: 255 },{ cType: types.CURRENT_LOCK_MECHANISM_STATE_CTYPE, + onRead: function(callback) { that.getState(callback); }, onUpdate: function(value) { that.log("Update current state to " + value); }, perms: ["pr","ev"], format: "int", From 0dedbcd36283b81794a1f949c3fde13cc60affcf Mon Sep 17 00:00:00 2001 From: Ray Bennett Date: Fri, 26 Jun 2015 18:03:31 -0700 Subject: [PATCH 10/10] Adding support for ISY. --- config-sample.json | 8 + package.json | 1 + platforms/ISY.js | 385 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 394 insertions(+) create mode 100644 platforms/ISY.js diff --git a/config-sample.json b/config-sample.json index 7ff4ddc..de32480 100644 --- a/config-sample.json +++ b/config-sample.json @@ -21,6 +21,14 @@ "name": "Domoticz", "server": "127.0.0.1", "port": "8005" + }, + { + "platform": "ISY", + "name": "ISY", + "host": "192.168.1.20", + "port": "8000", + "username": "username", + "password": "password" } ], diff --git a/package.json b/package.json index 5d27054..92d1d2b 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "request": "2.49.x", "node-persist": "0.0.x", "xmldoc": "0.1.x", + "xml2js": "0.4.x", "carwingsjs": "0.0.x", "sonos": "0.8.x", diff --git a/platforms/ISY.js b/platforms/ISY.js new file mode 100644 index 0000000..6304b44 --- /dev/null +++ b/platforms/ISY.js @@ -0,0 +1,385 @@ +var types = require("../lib/HAP-NodeJS/accessories/types.js"); +var xml2js = require('xml2js'); +var request = require('request'); +var util = require('util'); + +var parser = new xml2js.Parser(); + + +var power_state_ctype = { + cType: types.POWER_STATE_CTYPE, + onUpdate: function(value) { return; }, + perms: ["pw","pr","ev"], + format: "bool", + initialValue: 0, + supportEvents: true, + supportBonjour: false, + manfDescription: "Change the power state", + designedMaxLength: 1 +}; + +function ISYURL(user, pass, host, port, path) { + return util.format("http://%s:%s@%s:%d%s", user, pass, host, port, encodeURI(path)); +} + +function ISYPlatform(log, config) { + this.host = config["host"]; + this.port = config["port"]; + this.user = config["username"]; + this.pass = config["password"]; + + this.log = log; +} + +ISYPlatform.prototype = { + accessories: function(callback) { + this.log("Fetching ISY Devices."); + + var that = this; + var url = ISYURL(this.user, this.pass, this.host, this.port, "/rest/nodes"); + + var options = { + url: url, + method: 'GET' + }; + + var foundAccessories = []; + + request(options, function(error, response, body) { + if (error) + { + console.trace("Requesting ISY devices."); + that.log(error); + return error; + } + + parser.parseString(body, function(err, result) { + result.nodes.node.forEach(function(obj) { + var enabled = obj.enabled[0] == 'true'; + + if (enabled) + { + var device = new ISYAccessory( + that.log, + that.host, + that.port, + that.user, + that.pass, + obj.name[0], + obj.address[0], + obj.property[0].$.uom + ); + + foundAccessories.push(device); + } + }); + }); + + callback(foundAccessories.sort(function (a,b) { + return (a.name > b.name) - (a.name < b.name); + })); + }); + } +} + +function ISYAccessory(log, host, port, user, pass, name, address, uom) { + this.log = log; + this.host = host; + this.port = port; + this.user = user; + this.pass = pass; + this.name = name; + this.address = address; + this.uom = uom; +} + +ISYAccessory.prototype = { + query: function() { + var path = util.format("/rest/status/%s", encodeURI(this.address)); + var url = ISYURL(this.user, this.pass, this.host, this.port, path); + + var options = { url: url, method: 'GET' }; + request(options, function(error, response, body) { + if (error) + { + console.trace("Requesting Device Status."); + that.log(error); + return error; + } + + parser.parseString(body, function(err, result) { + var value = result.properties.property[0].$.value; + return value; + }); + + }); + }, + + command: function(c, value) { + this.log(this.name + " sending command " + c + " with value " + value); + + switch (c) + { + case 'On': + path = "/rest/nodes/" + this.address + "/cmd/DFON"; + break; + case 'Off': + path = "/rest/nodes/" + this.address + "/cmd/DFOF"; + break; + case 'Low': + path = "/rest/nodes/" + this.address + "/cmd/DON/85"; + break; + case 'Medium': + path = "/rest/nodes/" + this.address + "/cmd/DON/128"; + break; + case 'High': + path = "/rest/nodes/" + this.address + "/cmd/DON/255"; + break; + case 'setLevel': + if (value > 0) + { + path = "/rest/nodes/" + this.address + "/cmd/DON/" + Math.floor(255 * (value / 100)); + } + break; + default: + this.log("Unimplemented command sent to " + this.name + " Command " + c); + break; + } + + if (path) + { + var url = ISYURL(this.user, this.pass, this.host, this.port, path); + var options = { + url: url, + method: 'GET' + }; + + var that = this; + request(options, function(error, response, body) { + if (error) + { + console.trace("Sending Command."); + that.log(error); + return error; + } + that.log("Sent command " + path + " to " + that.name); + }); + } + }, + + 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: "SmartHome", + 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: this.address, + 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.uom == "%/on/off") { + cTypes.push({ + cType: types.POWER_STATE_CTYPE, + perms: ["pw","pr","ev"], + format: "bool", + initialValue: 0, + supportEvents: true, + supportBonjour: false, + manfDescription: "Change the power state", + designedMaxLength: 1, + onUpdate: function(value) { + if (value == 0) { + that.command("Off") + } else { + that.command("On") + } + }, + onRead: function() { + return this.query(); + } + }); + cTypes.push({ + cType: types.BRIGHTNESS_CTYPE, + perms: ["pw","pr","ev"], + format: "int", + initialValue: 0, + supportEvents: true, + supportBonjour: false, + manfDescription: "Adjust Brightness of Light", + designedMinValue: 0, + designedMaxValue: 100, + designedMinStep: 1, + unit: "%", + onUpdate: function(value) { + that.command("setLevel", value); + }, + onRead: function() { + var val = this.query(); + that.log("Query: " + val); + return val; + } + }); + } + else if (this.uom == "off/low/med/high") + { + cTypes.push({ + cType: types.POWER_STATE_CTYPE, + perms: ["pw","pr","ev"], + format: "bool", + initialValue: 0, + supportEvents: true, + supportBonjour: false, + manfDescription: "Change the power state", + designedMaxLength: 1, + onUpdate: function(value) { + if (value == 0) { + that.command("Off") + } else { + that.command("On") + } + }, + onRead: function() { + return this.query(); + } + }); + cTypes.push({ + cType: types.ROTATION_SPEED_CTYPE, + perms: ["pw","pr","ev"], + format: "bool", + initialValue: 0, + supportEvents: true, + supportBonjour: false, + manfDescription: "Change the speed of the fan", + designedMaxLength: 1, + onUpdate: function(value) { + if (value == 0) { + that.command("Off"); + } else if (value > 0 && value < 40) { + that.command("Low"); + } else if (value > 40 && value < 75) { + that.command("Medium"); + } else { + that.command("High"); + } + }, + onRead: function() { + return this.query(); + } + }); + } + else if (this.uom == "on/off") + { + cTypes.push({ + cType: types.POWER_STATE_CTYPE, + perms: ["pw","pr","ev"], + format: "bool", + initialValue: 0, + supportEvents: true, + supportBonjour: false, + manfDescription: "Change the power state", + designedMaxLength: 1, + onUpdate: function(value) { + if (value == 0) { + that.command("Off") + } else { + that.command("On") + } + }, + onRead: function() { + return this.query(); + } + }); + } + + return cTypes; + }, + + sType: function() { + if (this.uom == "%/on/off") { + return types.LIGHTBULB_STYPE; + } else if (this.uom == "on/off") { + return types.SWITCH_STYPE; + } else if (this.uom == "off/low/med/high") { + return types.FAN_STYPE; + } + + 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) + }]; + + //that.log("Loaded services for " + that.name); + return services; + } +}; + +module.exports.accessory = ISYAccessory; +module.exports.platform = ISYPlatform;