UNPKG

homebridge-tcc

Version:

Honeywell Total Connect Comfort support for Homebridge: https://github.com/nfarina/homebridge

348 lines (325 loc) 13.8 kB
const soapRequest = require('easy-soap-request'); const { XMLParser, XMLBuilder } = require('fast-xml-parser'); var tccMessage = require('./tccMessage.js'); var debug = require('debug')('tcc-lib'); // Configure XML parser and builder const xmlParser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: "@", parseAttributeValue: true, parseTagValue: true, trimValues: true, parseTrueNumberOnly: true, arrayMode: false }); const xmlBuilder = new XMLBuilder({ ignoreAttributes: false, attributeNamePrefix: "@", suppressEmptyNode: true, format: false }); const { default: PQueue } = require('p-queue'); const queue = new PQueue({ concurrency: 1 }); let count = 0; queue.on('active', () => { debug(`Queue: Working on item #${++count}. Size: ${queue.size} Pending: ${queue.pending}`); }); // var thermostats = {}; const URL = 'https://TCCNA.resideo.com/ws/MobileV2.asmx'; var HEADER = { 'user-agent': 'TCCStageC/1092 CFNetwork/1125.2 Darwin/19.4.0', 'Content-Type': 'text/xml;charset=UTF-8', 'ADRUM': 'isAjax:true', 'Accept': '*/*', 'Accept-Language': 'en-ca', 'Accept-Encoding': 'gzip, deflate, br', 'ADRUM_1': 'isMobile:true' }; module.exports = { tcc: tcc }; function tcc(options, callback) { if (options.debug) { debug.enabled = true; } debug("Setting up TCC component"); this._username = options.username; this._password = options.password; this._refresh = options.refresh; this.sessionID = null; this.timeout = 10000; // SOAP request timeout this.usePermanentHolds = options.usePermanentHolds; this.desiredState = {}; this.thermostats = {}; } // Public interface to login and read all thermostats tcc.prototype.pollThermostat = function() { return queue.add(async () => { try { if (!this.sessionID) { this.sessionID = await this._login(); debug("TCC - Login Succeeded"); } var current = await this._GetLocationListData(true); if (this.thermostats.LocationInfo && current.LocationInfo) { debug("pollThermostat - delta", JSON.stringify(tccMessage.diff(this.thermostats, current), null, 2)); } this.thermostats = current; return (current); } catch (err) { // console.error("pollThermostat Error:", err.message); // debug("pollThermostat", err); throw new Error(err); } }); }; // Public interface to login and update specific thermostat settings tcc.prototype.ChangeThermostat = function(desiredState) { // debug("ChangeThermostat()", desiredState); return queue.add(async () => { try { if (!this.sessionID) { this.sessionID = await this._login(); debug("TCC - Login Succeeded"); this.thermostats = await this._GetLocationListData(true); } var CommTaskID = await this._UpdateThermostat(desiredState, true); await this._GetCommTaskState(CommTaskID); var thermostat = await this._GetThermostat(desiredState.ThermostatID); return (thermostat); } catch (err) { console.error("ChangeThermostat Error:", err); this.sessionID = null; throw new Error(err); } }); }; // private interface to update thermostat settings tcc.prototype._UpdateThermostat = function(desiredState, withRetry) { debug("_UpdateThermostat()", desiredState); return new Promise((resolve, reject) => { (async () => { try { if (!this.sessionID) { this.sessionID = await this._login(); } HEADER.soapAction = 'http://services.alarmnet.com/Services/MobileV2/ChangeThermostatUI'; var message = '<?xml version="1.0" encoding="utf-8"?>' + xmlBuilder.build(tccMessage.soapMessage(tccMessage.ChangeThermostatMessage(this.sessionID, desiredState, this.thermostats.hb[desiredState.ThermostatID], this.usePermanentHolds))); debug("_UpdateThermostat: SOAP Message", message, this.sessionID, desiredState, this.thermostats.hb[desiredState.ThermostatID], this.usePermanentHolds); var { response } = await soapRequest({ url: URL, headers: HEADER, xml: message, timeout: this.timeout, withCredentials: true }); if (response.statusCode === 200) { var ChangeThermostat = xmlParser.parse(response.body)["soap:Envelope"]["soap:Body"].ChangeThermostatUIResponse.ChangeThermostatUIResult; // debug("_UpdateThermostat", ChangeThermostat); if (ChangeThermostat.Result === "Success") { debug("Success: _UpdateThermostat %s", ChangeThermostat, message); resolve(ChangeThermostat.CommTaskID); } else { this.sessionID = null; debug("ERROR: _UpdateThermostat %s", ChangeThermostat.Result, message); if (withRetry) { try { const CommTaskID = await this._UpdateThermostat(desiredState, false); resolve(CommTaskID); } catch (err) { debug("ERROR: _UpdateThermostat retry"); reject(err); } } else { reject(new Error("ERROR: _UpdateThermostat (200)", ChangeThermostat.Result)); } } } else { debug("ERROR: _UpdateThermostat %s", response, message); reject(new Error("ERROR: _UpdateThermostat (!200)", ChangeThermostat.Result)); } } catch (err) { // console.error("_UpdateThermostat Error:", err); debug("_UpdateThermostat message", xmlBuilder.build(tccMessage.soapMessage(tccMessage.ChangeThermostatMessage(this.sessionID, desiredState, this.thermostats.hb[desiredState.ThermostatID], this.usePermanentHolds)))); reject(err); this.sessionID = null; } })(); }); }; // private interface to login to TCC tcc.prototype._login = function() { return new Promise((resolve, reject) => { (async () => { try { HEADER.soapAction = 'http://services.alarmnet.com/Services/MobileV2/AuthenticateUserLogin'; var message = '<?xml version="1.0" encoding="utf-8"?>' + xmlBuilder.build(tccMessage.soapMessage(tccMessage.AuthenticateUserLoginMessage(this._username, this._password))); var { response } = await soapRequest({ url: URL, headers: HEADER, xml: message, timeout: this.timeout, withCredentials: true }); if (response.statusCode === 200) { var AuthenticateUserLoginResponse = xmlParser.parse(response.body)["soap:Envelope"]["soap:Body"].AuthenticateUserLoginResponse; if (AuthenticateUserLoginResponse.AuthenticateUserLoginResult.Result === "Success") { resolve(AuthenticateUserLoginResponse.AuthenticateUserLoginResult.SessionID); } else { // debug("ERROR: Login Failed %s", AuthenticateUserLoginResponse.AuthenticateUserLoginResult.Result, message); reject(new Error(AuthenticateUserLoginResponse.AuthenticateUserLoginResult.Result)); } } else { // debug("ERROR: Login Response Status Code", response.statusCode, message); reject(new Error("Login Response Status Code", response.statusCode)); } } catch (err) { // console.error("login Error:", err.message); reject(err); } })(); }); }; // private interface to retrieve status of a thermostat update tcc.prototype._GetCommTaskState = function(CommTaskID) { // SOAPAction http://services.alarmnet.com/Services/MobileV2/GetCommTaskState return new Promise((resolve, reject) => { (async () => { try { HEADER.soapAction = 'http://services.alarmnet.com/Services/MobileV2/GetCommTaskState'; var message = '<?xml version="1.0" encoding="utf-8"?>' + xmlBuilder.build(tccMessage.soapMessage(tccMessage.GetCommTaskStateMessage(this.sessionID, CommTaskID))); // debug("_GetCommTaskState", message); var { response } = await soapRequest({ url: URL, headers: HEADER, xml: message, timeout: this.timeout, withCredentials: true }); // debug("_GetCommTaskState", response.statusCode, response.body); if (response.statusCode === 200) { var GetCommTaskStateResponse = xmlParser.parse(response.body)["soap:Envelope"]["soap:Body"].GetCommTaskStateResponse; if (GetCommTaskStateResponse.GetCommTaskStateResult.Result === "Success") { debug("GetCommTaskState Success %s", GetCommTaskStateResponse.GetCommTaskStateResult); resolve(); } else { debug("ERROR: GetCommTaskState Failed %s", GetCommTaskStateResponse.GetCommTaskStateResult, message); reject(new Error("ERROR: GetCommTaskState Failed" + GetCommTaskStateResponse.GetCommTaskStateResult.Result)); } } else { debug("ERROR: GetCommTaskState Response Status Code", response.statusCode, message); reject(new Error("ERROR: GetCommTaskState Response Status Code", response.statusCode)); } } catch (err) { // console.error("login Error:", err.message); reject(err); } })(); }); }; // private interface to retrieve thermostat settings tcc.prototype._GetThermostat = function(ThermostatID) { // SOAPAction http://services.alarmnet.com/Services/MobileV2/GetThermostat return new Promise((resolve, reject) => { (async () => { try { HEADER.soapAction = 'http://services.alarmnet.com/Services/MobileV2/GetThermostat'; var message = '<?xml version="1.0" encoding="utf-8"?>' + xmlBuilder.build(tccMessage.soapMessage(tccMessage.GetThermostatMessage(this.sessionID, ThermostatID))); // debug("_GetThermostat", message); var { response } = await soapRequest({ url: URL, headers: HEADER, xml: message, timeout: this.timeout, withCredentials: true }); // debug("_GetThermostat", response.statusCode, response.body); if (response.statusCode === 200) { var GetThermostatResult = xmlParser.parse(response.body)["soap:Envelope"]["soap:Body"].GetThermostatResponse.GetThermostatResult; // debug("GetThermostatResult", GetThermostatResult); if (GetThermostatResult.Result === "Success") { // debug("_GetThermostat - delta", JSON.stringify(diff(GetThermostatResult.Thermostat, this.thermostats.hb[ThermostatID.toString()]), null, 2)); this.thermostats.hb[ThermostatID.toString()] = tccMessage.toHb(GetThermostatResult.Thermostat); // debug("_GetThermostat Temp %s Switch %s", toHb(GetThermostatResult.Thermostat).TargetTemperature, toHb(GetThermostatResult.Thermostat).TargetHeatingCoolingState); resolve(tccMessage.toHb(GetThermostatResult.Thermostat)); } else { debug("ERROR: GetThermostat Failed %s", GetThermostatResult.Result, message); reject(new Error("ERROR: GetThermostat Failed" + GetThermostatResult.Result)); } } else { debug("ERROR: GetThermostat Response Status Code", response.statusCode, message); reject(new Error("ERROR: GetThermostat Response Status Code", response.statusCode)); } } catch (err) { // console.error("login Error:", err.message); reject(err); } })(); }); }; // private interface to retrieve all thermostat settings tcc.prototype._GetLocationListData = function(withRetry) { return new Promise((resolve, reject) => { (async () => { try { if (!this.sessionID) { this.sessionID = await this._login(); } HEADER.soapAction = 'http://services.alarmnet.com/Services/MobileV2/GetLocations'; var message = '<?xml version="1.0" encoding="utf-8"?>' + xmlBuilder.build(tccMessage.soapMessage(tccMessage.GetLocationsMessage(this.sessionID))); // debug("SOAP Message", parser.toXml(soapMessage(GetLocations))); var { response } = await soapRequest({ url: URL, headers: HEADER, xml: message, timeout: this.timeout, withCredentials: true }); if (response.statusCode === 200) { var GetLocationsResult = xmlParser.parse(response.body)["soap:Envelope"]["soap:Body"].GetLocationsResponse.GetLocationsResult; if (GetLocationsResult.Result === "Success" && GetLocationsResult.Locations.LocationInfo) { // this.sessionID = AuthenticateUserLoginResponse.AuthenticateUserLoginResult.SessionID; // debug("_GetLocationListData", JSON.stringify(GetLocationsResult, null, 2)); // debug("_GetLocationListData-2", GetLocationsResult.Locations.LocationInfo); resolve(tccMessage.normalizeToHb(GetLocationsResult.Locations)); } else { this.sessionID = null; if (withRetry) { try { const locListData = await this._GetLocationListData(false); resolve(locListData); } catch (err) { debug("error get locations retry", err); reject(err); } } else { debug("GetLocations error, Info: %s", GetLocationsResult.Result); reject(new Error("GetLocations " + GetLocationsResult.Result)); } } } else { debug("GetLocations error, statusCode: %s", response.statusCode); reject(new Error("ERROR: GetLocations Response Status Code", response.statusCode)); } } catch (err) { console.error("GetLocations Error:", err); this.sessionID = null; reject(err); } })(); }); };