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", 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, diff --git a/config-sample.json b/config-sample.json index 891b7cf..e97c5de 100644 --- a/config-sample.json +++ b/config-sample.json @@ -27,6 +27,14 @@ "name": "Phillips Hue", "ip_address": "127.0.0.1", "username": "252deadbeef0bf3f34c7ecb810e832f" + }, + { + "platform": "ISY", + "name": "ISY", + "host": "192.168.1.20", + "port": "8000", + "username": "username", + "password": "password" } ], diff --git a/lib/HAP-NodeJS b/lib/HAP-NodeJS index 19a4bee..b130842 160000 --- a/lib/HAP-NodeJS +++ b/lib/HAP-NodeJS @@ -1 +1 @@ -Subproject commit 19a4bee7d82674ac0051706687def1e3570c2b68 +Subproject commit b130842359214062eb61a220577ebd7de98d0dd9 diff --git a/package.json b/package.json index 5a09872..c12ab9f 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "node-persist": "0.0.x", "xmldoc": "0.1.x", "node-hue-api": "^1.0.5", + "xml2js": "0.4.x", "carwingsjs": "0.0.x", "sonos": "0.8.x", "wemo": "0.2.x", 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 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; diff --git a/platforms/Wink.js b/platforms/Wink.js index f7a36f1..7b6083f 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) { @@ -174,7 +208,14 @@ 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); + }); + }, perms: ["pw","pr","ev"], format: "bool", initialValue: 0, @@ -184,7 +225,14 @@ 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); + }); + }, perms: ["pw","pr","ev"], format: "int", initialValue: 0,