From 01d2c21aac1045d789a6fc3e7d23e02dfc2eed28 Mon Sep 17 00:00:00 2001 From: Nick Farina Date: Sat, 29 Aug 2015 22:28:52 -0700 Subject: [PATCH] New iControl accessory, supports Xfinity Home. - No more mysterious "dsig" param --- accessories/XfinityHome.js | 284 ------------------------------------- accessories/iControl.js | 130 +++++++++++++++++ config-sample.json | 10 +- package.json | 5 +- 4 files changed, 138 insertions(+), 291 deletions(-) delete mode 100644 accessories/XfinityHome.js create mode 100644 accessories/iControl.js diff --git a/accessories/XfinityHome.js b/accessories/XfinityHome.js deleted file mode 100644 index cafdbd1..0000000 --- a/accessories/XfinityHome.js +++ /dev/null @@ -1,284 +0,0 @@ -var types = require("HAP-NodeJS/accessories/types.js"); -var request = require("request"); -var xmldoc = require("xmldoc"); - -function XfinityHomeAccessory(log, config) { - this.log = log; - this.name = config["name"]; - this.email = config["email"]; - this.password = config["password"]; - this.dsig = config["dsig"]; - this.pinCode = config["pin"]; -} - -XfinityHomeAccessory.prototype = { - - armWithType: function(armed, type) { - this.log("Arming with type " + type + " = " + armed + "..."); - this.targetArmed = armed; - this.targetArmType = type; - this.getLoginToken(); - }, - - getLoginToken: function() { - this.log("Retrieving login token..."); - - var that = this; - - request.post({ - url: "https://login.comcast.net/api/login", - form: { - appkey:"iControl", - dsig: this.dsig, - u: this.email, - p: this.password - } - }, function(err, response, body) { - - if (!err && response.statusCode == 200) { - - var doc = new xmldoc.XmlDocument(body); - that.loginToken = doc.valueWithPath("LoginToken"); - that.refreshLoginCookie(); - } - else { - that.log("Error '"+err+"' getting login token: " + body); - } - }); - }, - - refreshLoginCookie: function() { - this.log("Refreshing login cookie..."); - - var that = this; - - request.post({ - url: "https://www.xfinityhomesecurity.com/rest/icontrol/login", - form: { - token: this.loginToken - } - }, function(err, response, body) { - - if (!err && response.statusCode == 200) { - - // extract our "site" from the login response - var json = JSON.parse(body); - that.siteHref = json["login"]["site"]["href"]; - - // manual cookie handling - that.loginCookie = response.headers["set-cookie"]; - - that.getInstances(); - } - else { - that.log("Error '"+err+"' refreshing login cookie: " + body); - } - }); - }, - - getInstances: function() { - this.log("Getting instances for site " + this.siteHref + "..."); - - this.panelHref = null; - var that = this; - - request.get({ - url: "https://www.xfinityhomesecurity.com/"+that.siteHref+"/network/instances", - headers: { Cookie: this.loginCookie }, - json: true - }, function(err, response, json) { - - if (!err && response.statusCode == 200) { - - // extract our "instance" from the response. look for the first "panel" - var instances = json["instances"]["instance"]; - for (var i=0; i= 200 && response.statusCode < 300) { - that.log("Arm response: " + response); - } - else { - that.log("Error '"+err+"' performing arm request: " + body); - } - }); - }, - - getServices: function() { - var that = this; - return [{ - sType: types.ACCESSORY_INFORMATION_STYPE, - characteristics: [{ - cType: types.NAME_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: this.name, - supportEvents: false, - supportBonjour: false, - manfDescription: "Name of the accessory", - designedMaxLength: 255 - },{ - cType: types.MANUFACTURER_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: "Comcast", - supportEvents: false, - supportBonjour: false, - manfDescription: "Manufacturer", - designedMaxLength: 255 - },{ - cType: types.MODEL_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: "Rev-1", - supportEvents: false, - supportBonjour: false, - manfDescription: "Model", - designedMaxLength: 255 - },{ - cType: types.SERIAL_NUMBER_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: "A1S2NASF88EW", - supportEvents: false, - supportBonjour: false, - manfDescription: "SN", - designedMaxLength: 255 - },{ - cType: types.IDENTIFY_CTYPE, - onUpdate: null, - perms: ["pw"], - format: "bool", - initialValue: false, - supportEvents: false, - supportBonjour: false, - manfDescription: "Identify Accessory", - designedMaxLength: 1 - }] - },{ - sType: types.SWITCH_STYPE, - characteristics: [{ - cType: types.NAME_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: "Away Mode", - supportEvents: false, - supportBonjour: false, - manfDescription: "Away Mode service", - designedMaxLength: 255 - },{ - cType: types.POWER_STATE_CTYPE, - onUpdate: function(value) { that.armWithType(value, "away"); }, - perms: ["pw","pr","ev"], - format: "bool", - initialValue: false, - supportEvents: false, - supportBonjour: false, - manfDescription: "Turn on the Away alarm", - designedMaxLength: 1 - }] - },{ - sType: types.SWITCH_STYPE, - characteristics: [{ - cType: types.NAME_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: "Night Mode", - supportEvents: false, - supportBonjour: false, - manfDescription: "Night Mode service", - designedMaxLength: 255 - },{ - cType: types.POWER_STATE_CTYPE, - onUpdate: function(value) { that.armWithType(value, "night"); }, - perms: ["pw","pr","ev"], - format: "bool", - initialValue: false, - supportEvents: false, - supportBonjour: false, - manfDescription: "Turn on the Night alarm", - designedMaxLength: 1 - }] - },{ - sType: types.SWITCH_STYPE, - characteristics: [{ - cType: types.NAME_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: "Stay Mode", - supportEvents: false, - supportBonjour: false, - manfDescription: "Stay Mode service", - designedMaxLength: 255 - },{ - cType: types.POWER_STATE_CTYPE, - onUpdate: function(value) { that.armWithType(value, "stay"); }, - perms: ["pw","pr","ev"], - format: "bool", - initialValue: false, - supportEvents: false, - supportBonjour: false, - manfDescription: "Turn on the Stay alarm", - designedMaxLength: 1 - }] - }]; - } -}; - -// Enable cookie handling and append our expected headers -request = request.defaults({ - headers: { - "X-appkey": "comcastTokenKey", - "X-ClientInfo": "5.2.51", - "X-format": "json" - } -}); - -module.exports.accessory = XfinityHomeAccessory; diff --git a/accessories/iControl.js b/accessories/iControl.js new file mode 100644 index 0000000..d948867 --- /dev/null +++ b/accessories/iControl.js @@ -0,0 +1,130 @@ +var iControl = require('node-icontrol').iControl; +var Service = require('HAP-NodeJS').Service; +var Characteristic = require('HAP-NodeJS').Characteristic; + +module.exports = { + accessory: iControlAccessory +} + +/** + * Provides a Security System accessory for an iControl-based security system like Xfinity Home. + */ + +function iControlAccessory(log, config) { + this.log = log; + + this.iControl = new iControl({ + system: iControl.Systems[config.system], + email: config.email, + password: config.password, + pinCode: config.pin + }); + + this.iControl.on('change', this._handleChange.bind(this)); + this.iControl.on('error', this._handleError.bind(this)); + + this.log("Logging into iControl..."); + this.iControl.login(); + + this._securitySystem = new Service.SecuritySystem("Security System"); + + this._securitySystem + .getCharacteristic(Characteristic.SecuritySystemTargetState) + .on('get', this._getTargetState.bind(this)) + .on('set', this._setTargetState.bind(this)); + + this._securitySystem + .getCharacteristic(Characteristic.SecuritySystemCurrentState) + .on('get', this._getCurrentState.bind(this)); +} + +iControlAccessory.prototype._getTargetState = function(callback) { + this.iControl.getArmState(function(err, armState) { + if (err) return callback(err); + + var currentState = this._getHomeKitStateFromArmState(armState); + callback(null, currentState); + + }.bind(this)); +} + +iControlAccessory.prototype._getCurrentState = function(callback) { + this.iControl.getArmState(function(err, armState) { + if (err) return callback(err); + + var currentState = this._getHomeKitStateFromArmState(armState); + callback(null, currentState); + + }.bind(this)); +} + +iControlAccessory.prototype._setTargetState = function(targetState, callback, context) { + if (context == "internal") return callback(null); // we set this state ourself, no need to react to it + + var armState = this._getArmStateFromHomeKitState(targetState); + this.log("Setting target state to %s", armState); + + this.iControl.setArmState(armState, function(err) { + if (err) return callback(err); + + this.log("Successfully set target state to %s", armState); + + // also update current state + this._securitySystem + .getCharacteristic(Characteristic.SecuritySystemCurrentState) + .setValue(targetState); + + callback(null); // success! + + }.bind(this)); +} + +iControlAccessory.prototype._handleChange = function(armState) { + this.log("Arm state changed to %s", armState); + + var homeKitState = this._getHomeKitStateFromArmState(armState); + + this._securitySystem + .getCharacteristic(Characteristic.SecuritySystemCurrentState) + .setValue(homeKitState); + + this._securitySystem + .getCharacteristic(Characteristic.SecuritySystemTargetState) + .setValue(homeKitState, null, "internal"); // these characteristics happen to share underlying values +} + +iControlAccessory.prototype._handleError = function(err) { + this.log(err.message); +} + +iControlAccessory.prototype.getServices = function() { + return [this._securitySystem]; +} + +iControlAccessory.prototype._getHomeKitStateFromArmState = function(armState) { + switch (armState) { + case "disarmed": return Characteristic.SecuritySystemCurrentState.DISARMED; + case "away": return Characteristic.SecuritySystemCurrentState.AWAY_ARM; + case "night": return Characteristic.SecuritySystemCurrentState.NIGHT_ARM; + case "stay": return Characteristic.SecuritySystemCurrentState.STAY_ARM; + } +} + +iControlAccessory.prototype._getArmStateFromHomeKitState = function(homeKitState) { + switch (homeKitState) { + case Characteristic.SecuritySystemCurrentState.DISARMED: return "disarmed"; + case Characteristic.SecuritySystemCurrentState.AWAY_ARM: return "away"; + case Characteristic.SecuritySystemCurrentState.NIGHT_ARM: return "night"; + case Characteristic.SecuritySystemCurrentState.STAY_ARM: return "stay"; + } +} + + +/** + * TESTING + */ + +if (require.main === module) { + var config = JSON.parse(require('fs').readFileSync("config.json")).accessories[0]; + var accessory = new iControlAccessory(console.log, config); +} diff --git a/config-sample.json b/config-sample.json index c9ba829..4245bd5 100644 --- a/config-sample.json +++ b/config-sample.json @@ -104,15 +104,15 @@ "password" : "your-carwings-password" }, { - "accessory": "XfinityHome", + "accessory": "iControl", "name": "Xfinity Home", - "description": "This shim supports the 'Xfinity Home' security system. Unfortunately I don't know how to generate the 'dsig' property, so you'll need to figure yours out by running the Xfinity Home app on your iOS device while connected to a proxy server like Charles. If you didn't understand any of that, sorry! I welcome any suggestions for how to figure out dsig automatically.", - "email": "your-comcast-email@example.com", + "description": "This shim supports iControl-based security systems like Xfinity Home.", + "system": "XFINITY_HOME", + "email": "your-comcast-email", "password": "your-comcast-password", - "dsig": "your-digital-signature", "pin": "your-security-system-pin-code" }, - { + { "accessory": "HomeMatic", "name": "Light", "description": "Control HomeMatic devices (The XMP-API addon for the CCU is required)", diff --git a/package.json b/package.json index ab5b117..7eb761f 100644 --- a/package.json +++ b/package.json @@ -20,17 +20,18 @@ "harmonyhubjs-discover": "git+https://github.com/swissmanu/harmonyhubjs-discover.git", "mdns": "^2.2.4", "node-hue-api": "^1.0.5", + "node-icontrol": "^0.1.0", "node-milight-promise": "0.0.x", "node-persist": "0.0.x", "request": "2.49.x", "sonos": "0.8.x", "telldus-live": "0.2.x", + "teslams": "1.0.1", "unofficial-nest-api": "git+https://github.com/hachidorii/unofficial_nodejs_nest.git#d8d48edc952b049ff6320ef99afa7b2f04cdee98", "wemo": "0.2.x", "wink-js": "0.0.5", "xml2js": "0.4.x", "xmldoc": "0.1.x", - "yamaha-nodejs": "0.4.x", - "teslams": "1.0.1" + "yamaha-nodejs": "0.4.x" } }