UNPKG

homematic-rega

Version:

Homematic CCU ReGaHSS Remote Script Interface

394 lines (359 loc) 12.4 kB
const fs = require('fs'); const path = require('path'); const tempDir = require('temp-dir'); const request = require('request'); const iconv = require('iconv-lite'); const parseXml = require('xml2js').parseString; class Rega { /** * @param {object} options * @param {string} options.host - hostname or IP address of the Homematic CCU * @param {string} [options.language=de] - language used for translation of placeholders in variables/rooms/functions * @param {boolean} [options.disableTranslation=false] - disable translation of placeholders * @param {boolean} [options.tls=false] - Connect using TLS * @param {boolean} [options.inSecure=false] - Ignore invalid TLS Certificates * @param {boolean} [options.auth=false] - Use Basic Authentication * @param {string} [options.user] - Auth Username * @param {string} [options.pass] - Auth Password * @param {number} [options.port=8181] - rega remote script port. Defaults to 48181 if options.tls is true */ constructor(options) { this.language = options.language || 'de'; this.disableTranslation = options.disableTranslation; this.host = options.host; this.tls = options.tls; this.port = options.port || (this.tls ? 48181 : 8181); this.inSecure = options.inSecure; this.auth = options.auth; this.user = options.user; this.pass = options.pass; this.url = (this.tls ? 'https' : 'http') + '://' + this.host + ':' + this.port + '/rega.exe'; this.encoding = 'iso-8859-1'; this.requestOptions = { method: 'POST', url: this.url, encoding: null }; if (this.auth) { this.requestOptions.auth = { user: this.user, pass: this.pass, sendImmediately: true }; } if (this.tls) { this.requestOptions.strictSSL = !this.inSecure; } } /** * @callback Rega~scriptCallback * @param {?Error} err * @param {string} output - the scripts output * @param {Object.<string, string>} variables - contains all variables that are set in the script (as strings) */ _parseResponse(res, callback) { const ERROR_XML_MISSING = new Error('xml in rega response missing'); if (res) { const outputEnd = res.lastIndexOf('<xml>'); if (outputEnd === -1) { callback(ERROR_XML_MISSING); } else { const output = res.slice(0, outputEnd); const xml = res.slice(outputEnd); if (xml) { parseXml(xml, {explicitArray: false}, (err, res) => { if (err) { callback(err, output); } else if (res) { callback(null, output, res.xml); } else { callback(ERROR_XML_MISSING); } }); } else { callback(ERROR_XML_MISSING); } } } else { callback(new Error('empty rega response')); } } /** * Execute a rega script * @method Rega#exec * @param {string} script - string containing a rega script * @param {Rega~scriptCallback} [callback] */ exec(script, callback) { if (typeof callback !== 'function') { callback = () => {}; } script = iconv.encode(script, this.encoding); request(Object.assign(this.requestOptions, { body: script, headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': script.length } }), (err, res, body) => { if (!err && body) { if (res.statusCode === 401) { callback(new Error('401 Unauthorized')); } else { body = iconv.decode(body, this.encoding); this._parseResponse(body, callback); } } else { callback(err); } }); } /** * Execute a rega script from a file * @method Rega#script * @param {string} file - path to script file * @param {Rega~scriptCallback} [callback] */ script(file, callback) { // TODO cache files fs.readFile(file, (err, res) => { if (err) { if (typeof callback === 'function') { callback(err); } } else { this.exec(res.toString(), callback); } }); } _jsonScript(file, callback) { this.script(file, (err, res) => { if (err) { callback(err); } else { // Todo: remove ugly workaround for https://github.com/rdmtc/RedMatic/issues/381 res = res.replace(/, "val": nan,/g, ', "val": null,'); try { callback(null, JSON.parse(res)); } catch { const debugFile = path.join(tempDir, path.basename(file) + '.failed.json'); fs.writeFile(debugFile, res, () => {}); callback(new Error('JSON.parse failed. Saved debug data to ' + debugFile)); } } }); } /** * Get all devices and channels * @method Rega#getChannels * @param {Rega~channelCallback} callback */ getChannels(callback) { this._jsonScript(path.join(__dirname, 'scripts', 'channels.rega'), (err, res) => { if (err) { callback(err, res); } else { res.forEach((channel, index) => { channel.name = unescape(channel.name); res[index] = channel; }); callback(null, res); } }); } /** * Get all devices and channels values * @method Rega#getValues * @param {Rega~valuesCallback} callback */ getValues(callback) { this._jsonScript(path.join(__dirname, 'scripts', 'values.rega'), (err, res) => { if (err) { callback(err, res); } else { res.forEach((ch, index) => { ch.name = unescape(ch.name); if (typeof ch.value === 'string') { ch.value = unescape(ch.value); } res[index] = ch; }); callback(null, res); } }); } /** * Get all programs * @method Rega#getPrograms * @param {Rega~programsCallback} callback */ getPrograms(callback) { this._jsonScript(path.join(__dirname, 'scripts', 'programs.rega'), (err, res) => { if (err) { callback(err, res); } else { res.forEach((prg, index) => { prg.name = unescape(prg.name); prg.info = unescape(prg.info); res[index] = prg; }); callback(null, res); } }); } _getTranslations(callback) { const url = 'http://' + this.host + '/webui/js/lang/' + this.language + '/translate.lang.extension.js'; this.translations = {}; request({ method: 'GET', url, encoding: null }, (err, res, body) => { if (!err && body) { this._parseTranslations(iconv.decode(body, this.encoding)); } callback(); }); } _parseTranslations(body) { const lines = body.split('\n'); lines.forEach(line => { const match = line.match(/\s*"((func|room|sysVar)[^"]+)"\s*:\s*"([^"]+)"/); if (match) { this.translations[match[1]] = unescape(match[3]); // TODO replace deprecated unescape } }); } _translate(item) { if (!this.disableTranslation) { let key = item; if (key.startsWith('${') && key.endsWith('}')) { key = key.slice(2, item.length - 1); } if (this.translations[key]) { item = this.translations[key]; } } return item; } _translateNames(res) { if (!this.disableTranslation) { Object.keys(res).forEach(id => { const object = res[id]; object.name = this._translate(unescape(object.name)); if (object.info) { object.info = this._translate(unescape(object.info)); } }); } return res; } _translateEnum(values) { if (!this.disableTranslation) { values.forEach((value, i) => { values[i] = this._translate(value); }); } return values; } _translateJsonScript(file, callback) { if (this.translations || this.disableTranslation) { this._jsonScript(file, (err, res) => { if (err) { callback(err); } else { callback(null, this.disableTranslation ? res : this._translateNames(res)); } }); } else { this._getTranslations(() => { this._translateJsonScript(file, callback); }); } } /** * Get all variables * @method Rega#getVariables * @param {Rega~variablesCallback} callback */ getVariables(callback) { this._translateJsonScript(path.join(__dirname, 'scripts', 'variables.rega'), (err, res) => { if (err) { callback(err); } else { res.forEach((sysvar, index) => { if (sysvar.type === 'string') { sysvar.val = unescape(sysvar.val); } if (sysvar.enum === '') { sysvar.enum = []; } else { sysvar.enum = this._translateEnum(unescape(sysvar.enum).split(';')); } res[index] = sysvar; }); callback(null, res); } }); } /** * Get all rooms * @method Rega#getRooms * @param {Rega~roomsCallback} callback */ getRooms(callback) { this._translateJsonScript(path.join(__dirname, 'scripts', 'rooms.rega'), callback); } /** * Get all functions * @method Rega#getFunctions * @param {Rega~functionsCallback} callback */ getFunctions(callback) { this._translateJsonScript(path.join(__dirname, 'scripts', 'functions.rega'), callback); } /** * Set a variables value * @method Rega#setVariable * @param {number} id * @param {number|boolean|string} val * @param {function} [callback] */ setVariable(id, value, callback) { const script = 'dom.GetObject(' + id + ').State(' + JSON.stringify(value) + ');'; this.exec(script, callback); } /** * Execute a program * @method Rega#startProgram * @param {number} id * @param {function} [callback] */ startProgram(id, callback) { const script = 'dom.GetObject(' + id + ').ProgramExecute();'; this.exec(script, callback); } /** * Activate/Deactivate a program * @method Rega#setProgram * @param {number} id * @param {boolean} active * @param {function} [callback] */ setProgram(id, active, callback) { const script = 'dom.GetObject(' + id + ').Active(' + Boolean(active) + ');'; this.exec(script, callback); } /** * Rename an object * @method Rega#setName * @param {number} id * @param {string} name * @param {function} [callback] */ setName(id, name, callback) { const script = 'dom.GetObject(' + id + ').Name("' + name + '");'; this.exec(script, callback); } } module.exports = Rega;