UNPKG

fake-isy-994i

Version:

Node.js server that acts like a ISY-994i. Unsupported and no longer under active development.

1,020 lines (866 loc) 37.7 kB
var express = require("express"); var path = require("path"); var xmldom = require('xmldom'); var fs = require('fs'); var parser = require('xmldom').DOMParser; var xmlImpl = require('xmldom').DOMImplementation; var FolderNode = require('./foldernode.js').FolderNode; var DeviceNode = require('./devicenode.js').DeviceNode; var SceneNode = require('./scenenode.js').SceneNode; var WebSocket = require('faye-websocket'); var log = require('./utils').log; var setLogEnabled = require('./utils').setLogEnabled; var Ssdp = require('upnp-ssdp'); var xmlparser = require('express-xml-bodyparser'); var restler = require('restler') var ElkStatus = require('./elkstatus.js').ElkStatus; var Variable = require('./variable.js').Variable; var dateFormat = require('dateformat'); var basicAuth = require('basic-auth'); var ISYServer = function(port, config) { this.app = express(); this.app.use(xmlparser()); this.port = port; this.config = {}; this.variables = {}; this.xmlImplementation = new xmlImpl(); this.loadConfig(config); this.resetState(); } ISYServer.prototype.CONFIG_ELK_ENABLED = 'elkEnabled'; ISYServer.prototype.CONFIG_EXTENDED_ERRORS = 'extendedErrors'; ISYServer.prototype.CONFIG_USERNAME = 'userName'; ISYServer.prototype.CONFIG_PASSWORD = 'password'; ISYServer.prototype.CONFIG_REQUIRE_AUTH = 'requireAuth'; ISYServer.prototype.CONFIG_LOGGER_ENABLED = 'loggerEnabled'; ISYServer.prototype.CONFIG_NODE_FILE = 'nodeFile'; ISYServer.prototype.CONFIG_ELK_STATUS_FILE = 'elkStatusFile'; ISYServer.prototype.CONFIG_ELK_TOPOLOGY_FILE = 'elkTopologyFile'; ISYServer.prototype.CONFIG_LOG_RESPONSE_BODY = 'logResponseBody'; ISYServer.prototype.CONFIG_LOG_WEBSOCKET_NOTIFICATION = 'logWebSockets'; ISYServer.prototype.CONFIG_LOG_WEB_NOTIFICATION = 'logWebNotification'; ISYServer.prototype.CONFIG_FAIL_VARIABLE_CALLS = 'failVariableCalls'; ISYServer.prototype.CONFIG_SCENE_FILE = 'sceneFile'; ISYServer.prototype.CONFIG_VARIABLE_FILE_1 = "variable1File"; ISYServer.prototype.CONFIG_VARIABLE_FILE_2 = "variable2File"; ISYServer.prototype.loadConfig = function(config) { this.configSettings = [ // Logger should always be the first setting, used to set default of logger below this block { name: this.CONFIG_LOGGER_ENABLED, default: true }, { name: this.CONFIG_ELK_ENABLED, default: true }, { name: this.CONFIG_EXTENDED_ERRORS, default: true }, { name: this.CONFIG_USERNAME, default: 'admin' }, { name: this.CONFIG_PASSWORD, default: 'password' }, { name: this.CONFIG_REQUIRE_AUTH, default: true }, { name: this.CONFIG_NODE_FILE, default: './example-nodes.xml' }, { name: this.CONFIG_ELK_STATUS_FILE, default: './example-elk-status.xml' }, { name: this.CONFIG_ELK_TOPOLOGY_FILE, default: './example-elk-topology.xml' }, { name: this.CONFIG_SCENE_FILE, default: './example-scenes.xml' }, { name: this.CONFIG_VARIABLE_FILE_1, default: './example-variables-1.xml' }, { name: this.CONFIG_VARIABLE_FILE_2, default: './example-variables-2.xml' }, { name: this.CONFIG_LOG_RESPONSE_BODY, default: false}, { name: this.CONFIG_LOG_WEB_NOTIFICATION, default: false}, { name: this.CONFIG_LOG_WEBSOCKET_NOTIFICATION, default: true}, { name: this.CONFIG_FAIL_VARIABLE_CALLS, default: false } ]; // Special case logging as we need to setup logging BEFORE loading config so we can log setting results if(config != undefined && config.loggerEnabled != undefined) { setLogEnabled(config.loggerEnabled); } else { setLogEnabled(this.configSettings[0].default); } log('Configuration:'); for(var configIndex = 0; configIndex < this.configSettings.length; configIndex++) { if(config == undefined || config[this.configSettings[configIndex].name] == undefined) { this.setConfigSetting(this.configSettings[configIndex].name, this.configSettings[configIndex].default); } else { this.setConfigSetting(this.configSettings[configIndex].name, config[this.configSettings[configIndex].name]); } log(this.configSettings[configIndex].name+': '+this.getConfigSetting(this.configSettings[configIndex].name)); } } ISYServer.prototype.getConfigSetting = function(settingName) { return this.config[settingName]; } ISYServer.prototype.setConfigSetting = function(settingName, value) { this.config[settingName] = value; } ISYServer.prototype.buildCommandResponse = function(res, resultSuccess, resultCode, extended) { this.setupResponseHeaders(res, resultCode); var resultString = '<?xml version="1.0" encoding="UTF-8"?>\r\n'+ '<RestResponse succeeded="'+resultSuccess+ '">\r\n'+ ' <status>'+resultCode+'</status>\r\n'; if(this.getConfigSetting(this.CONFIG_EXTENDED_ERRORS) && extended != undefined && extended != null) { resultString += ' <extended>'+extended+'</extended>\r\n'; } resultString += '</RestResponse>\r\n'; if(this.getConfigSetting(this.CONFIG_LOG_RESPONSE_BODY)) { log('Response Body: '+resultString); } res.send(resultString); } ISYServer.prototype.buildCommandResponseEmpty = function(res, resultCode) { res.status(resultCode).end(); } ISYServer.prototype.buildSubscribeResponse = function(res, subscriptionId) { this.setupSubscribeResponseHeaders(res, 200); var response = '<?xml version="1.0" encoding="UTF-8"?><s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"><s:Body><SubscriptionResponse><SID>uuid:' response += subscriptionId response += '</SID><duration>0</duration></SubscriptionResponse></s:Body></s:Envelope>' if(this.getConfigSetting(this.CONFIG_LOG_RESPONSE_BODY)) { log('Response Body: '+response); } res.send(response); } ISYServer.prototype.setupSubscribeResponseHeaders = function(res, resultCode) { res.set('EXT','UCoS, UPnP/1.0, UDI/1.0'); res.set('cache-control', "max-age=3600, must-revalidate"); res.set('WWW-Authenticate','Basic realm="/"'); res.set('Last-Modified', new Date()); res.set('Connection','Keep-Alive'); res.set('Content-Type', 'application/soap+xml; charset=UTF-8'); res.status(resultCode); } ISYServer.prototype.setupResponseHeaders = function(res, resultCode) { res.set('EXT','UCoS, UPnP/1.0, UDI/1.0'); res.set('Cache-Control', 'no-cache'); res.set('WWW-Authenticate','Basic realm="/"'); res.set('Last-Modified', new Date()); res.set('Connection','Keep-Alive'); res.set('Content-Type', 'text/xml; charset=UTF-8'); res.status(resultCode); } ISYServer.prototype.handleElkStatusRequest = function(req,res) { this.logRequestStartDetails(req); this.setupResponseHeaders(res,200); res.send(this.elkStatus.getStatus()); this.logRequestEndDetails(res); } ISYServer.prototype.handleElkTopologyRequest = function(req,res) { this.logRequestStartDetails(req); this.setupResponseHeaders(res,200); if(this.getConfigSetting(this.CONFIG_LOG_RESPONSE_BODY)) { log('Response Body: '+this.elkTopology); } res.send(this.elkStatus.getTopology()); this.logRequestEndDetails(res); } ISYServer.prototype.handleNodesRequest = function(req,res) { this.logRequestStartDetails(req); this.setupResponseHeaders(res,200); if(this.getConfigSetting(this.CONFIG_LOG_RESPONSE_BODY)) { log(this.rootDoc.toString()); } res.send(this.rootDoc.toString()); this.logRequestEndDetails(res); } ISYServer.prototype.handleNodeRequest = function(req, res, nodeAddress) { this.logRequestStartDetails(req); var node = this.nodeIndex[req.params.address]; if(node == undefined) { this.setupResponseHeaders(res, 404, "Unable to find specified node"); } else { var result = '<?xml version="1.0" encoding="UTF-8"?><nodeInfo>'; result += node.node.toString(); result += '</nodeInfo>'; this.setupResponseHeaders(res, 200); if(this.getConfigSetting(this.CONFIG_LOG_RESPONSE_BODY)) { log(result); } res.send(result); } this.logRequestEndDetails(res); } ISYServer.prototype.createResponseDocument = function() { var responseDoc = this.xmlImplementation.createDocument("","",""); responseDoc.appendChild(responseDoc.createProcessingInstruction('xml', 'version="1.0" encoding="UTF-8"')); return responseDoc; } ISYServer.prototype.handleStatusRequest = function(req, res) { this.logRequestStartDetails(req); var result = ""; this.setupResponseHeaders(res, 200); var responseDoc = this.createResponseDocument(); var nodesElement = responseDoc.createElement('nodes'); for(var nodeIndex = 0; nodeIndex < this.nodeList.length; nodeIndex++) { if(this.nodeList[nodeIndex] instanceof DeviceNode) { nodesElement.appendChild(this.nodeList[nodeIndex].getStatusNode(responseDoc)); } } responseDoc.appendChild(nodesElement); var result = responseDoc.toString(); if(this.getConfigSetting(this.CONFIG_LOG_RESPONSE_BODY)) { log(result); } res.send(result); this.logRequestEndDetails(res); } ISYServer.prototype.handleSceneRequest = function(req, res) { this.logRequestStartDetails(req); this.setupResponseHeaders(res, 200); if(this.getConfigSetting(this.CONFIG_LOG_RESPONSE_BODY)) { log(this.sceneList); } res.send(this.sceneList); this.logRequestEndDetails(res); } ISYServer.prototype.logRequestStartDetails = function(req) { log("REQUEST. Source="+req.ip+" Url: "+req.originalUrl); } ISYServer.prototype.logRequestEndDetails = function(res) { log("RESULT: Code="+res.statusCode); } ISYServer.prototype.handleZoneUpdate = function(zoneId, command) { var change = false; var zoneData = this.elkStatus.getZoneData(zoneId); if(zoneData == null) { log('Error, zone update request came for zoneId: '+zoneId); return; } if(command == "OPEN") { var firstChange = this.elkStatus.setZoneData(zoneId, "51", "2"); var secondChange = this.elkStatus.setZoneData(zoneId, "52", "1"); change = firstChange || secondChange; } else if(command == "CLOSE") { var firstChange = this.elkStatus.setZoneData(zoneId, "51", "0"); var secondChange = this.elkStatus.setZoneData(zoneId, "52", "2"); change = firstChange || secondChange; } return change; } ISYServer.prototype.sendElkZoneUpdateToAll = function(zoneId) { var zoneData = this.elkStatus.getZoneData(zoneId); for(var socketIndex = 0; socketIndex < this.webSocketClientList.length; socketIndex++) { this.sendElkZoneUpdate(this.webSocketClientList[socketIndex],zoneId,"51", zoneData["51"]); this.sendElkZoneUpdate(this.webSocketClientList[socketIndex],zoneId,"52", zoneData["52"]); } var tempWebList = this.webSubscriptions.slice() for(var webIndex = 0; webIndex < tempWebList.length; webIndex++) { this.sendElkZoneUpdateWeb(tempWebList[webIndex],zoneId,"51", zoneData["51"]); this.sendElkZoneUpdateWeb(tempWebList[webIndex],zoneId,"52", zoneData["52"]); } } ISYServer.prototype.handleCommandRequest = function(req, res) { this.logRequestStartDetails(req); var nodeToUpdate = this.nodeIndex[req.params.address]; if(nodeToUpdate == undefined || nodeToUpdate == null) { if(this.elkStatus.getZoneData(req.params.address) != null) { var zoneId = req.params.address; if(this.handleZoneUpdate(zoneId, req.params.command)) { this.sendElkZoneUpdateToAll(zoneId); } this.buildCommandResponse(res, true, 200); } else { this.buildCommandResponse(res, false, 404); } } else if(nodeToUpdate instanceof FolderNode) { this.buildCommandResponse(res, false, 500, 'Specified address is a folder, cannot issue command'); } else if(nodeToUpdate instanceof SceneNode) { try { var nodesChanged = []; for (var i = 0; i < nodeToUpdate.children.length; i++) { // ISY doesn't support dimming a scene, i.e. sending DON with a level command and doesn't return an // error when you try it. It just turns on. So cutting of the parameter just like ISY would. if (nodeToUpdate.children[i].simulateExecuteCommand(req.params.command, null)) { nodesChanged.push(nodeToUpdate.children[i]); } } for (var nodeIndex = 0; nodeIndex < nodesChanged.length; nodeIndex++) { this.sendDeviceUpdateToAll(nodesChanged[nodeIndex]); } this.buildCommandResponse(res, true, 200); } catch(err) { this.buildCommandResponse(res, false, 500, err); } } else { try { if(nodeToUpdate.simulateExecuteCommand(req.params.command, req.params.parameter)) { this.sendDeviceUpdateToAll(nodeToUpdate); } this.buildCommandResponse(res, true, 200); } catch(err) { this.buildCommandResponse(res, false, 500, err); } } this.logRequestEndDetails(res); } ISYServer.prototype.handleConfigureRequest = function(req, res) { this.logRequestStartDetails(req); var configName = req.params.configName; var configValue = req.params.configValue; if(configName == undefined || configValue == undefined || configName == null || configValue == null) { this.buildCommandResponse(res, false, 500, 'No config value or config name specified'); this.logRequestEndDetails(res); return; } if(this.getConfigSetting(configName)==undefined) { this.buildCommandResponse(res, false, 404, "Unknown config value"); this.logRequestEndDetails(res); return; } var valueToSet = configValue; if(valueToSet == 'true') { valueToSet = true; } else if(valueToSet == 'false') { valueToSet = false; } else if(!isNaN(valueToSet)) { valueToSet = Number(valueToSet); } this.setConfigSetting(configName, valueToSet); this.buildCommandResponse(res, true, 200, "Configuration updated"); this.logRequestEndDetails(res); } ISYServer.prototype.resetState = function() { this.webSocketClientList = []; this.sequenceNumber = 0; this.webSubscriptions = []; this.loadNodeState(); } ISYServer.prototype.handleAddWebSubscription = function(req,res) { this.logRequestStartDetails(req) var webSubscriptionNumber = this.webSubscriptions.length var envelopeElement = req.body['s:envelope'] if(envelopeElement == null) { this.buildCommandResponse(res, false, 500, 'Malformed envelope element in request, rejected') } else { var bodyElement = envelopeElement['s:body'] if(bodyElement == null) { this.buildCommandResponse(res, false, 500, 'Malformed envelope body element, rejected') } else { var subscribeElement = bodyElement[0]['u:subscribe'] if(subscribeElement == null) { this.buildCommandResponse(res, false, 500, 'Missing subscribe element, rejected') } else { var subscribeUrl = subscribeElement[0].reporturl[0] if(subscribeUrl.indexOf('http')==-1) { this.buildCommandResponse(res, false, 500, 'Invalid target url') } else { this.webSubscriptions[webSubscriptionNumber] = subscribeUrl this.sendInitialWebState(subscribeUrl) this.buildSubscribeResponse(res, webSubscriptionNumber) } } } } this.logRequestEndDetails(res) } ISYServer.prototype.handleResetNodesRequest = function(req,res) { this.logRequestStartDetails(req); log('RESET: Resetting node state back to initial state'); this.resetState(); this.buildCommandResponse(res, true, 200, 'Node state reset to initial'); this.logRequestEndDetails(res); } ISYServer.prototype.loadVariables = function(data,type) { var variableDoc = new parser().parseFromString(data.substring(2, data.length)); var variableElements = variableDoc.getElementsByTagName('e'); var variableList = []; for(var i = 0; i < variableElements.length; i++) { var newVariable = new Variable(variableElements[i].getAttribute('id'), variableElements[i].getAttribute('name'),type); variableList.push(newVariable); } this.variables[type] = variableList; } ISYServer.prototype.loadNodeState = function() { // Ensure we are clean this.nodeIndex = {}; this.nodeList = []; var fileData = fs.readFileSync(this.getConfigSetting(this.CONFIG_NODE_FILE), 'ascii'); this.rootDoc = new parser().parseFromString(fileData.substring(2, fileData.length)); this.sceneList = fs.readFileSync(this.getConfigSetting(this.CONFIG_SCENE_FILE), 'ascii'); this.varData = {}; this.varData['1'] = fs.readFileSync(this.getConfigSetting(this.CONFIG_VARIABLE_FILE_1), 'ascii'); this.loadVariables(this.varData['1'],'1'); this.varData['2'] = fs.readFileSync(this.getConfigSetting(this.CONFIG_VARIABLE_FILE_2), 'ascii'); this.loadVariables(this.varData['2'],'2'); // Load folders var folders = this.rootDoc.getElementsByTagName('folder'); for(var i = 0; i < folders.length; i++) { var newNode = new FolderNode(folders[i]); this.nodeIndex[newNode.getAddress()] = newNode; this.nodeList.push(newNode); } // Load devices var devices = this.rootDoc.getElementsByTagName('node'); for(var j = 0; j < devices.length; j++) { var newNode = new DeviceNode(devices[j]); this.nodeIndex[newNode.getAddress()] = newNode; this.nodeList.push(newNode); } // Load scenes var scenes = this.rootDoc.getElementsByTagName('group'); for(var i = 0; i < scenes.length; i++) { var newScene = new SceneNode(scenes[i], this.nodeIndex); this.nodeIndex[newScene.getAddress()] = newScene; this.nodeList.push(newScene); } this.elkStatus = new ElkStatus(this.getConfigSetting(this.CONFIG_ELK_STATUS_FILE),this.getConfigSetting(this.CONFIG_ELK_TOPOLOGY_FILE)) } ISYServer.prototype.buildUnauthorizedResponse = function(res) { res.set('WWW-Authenticate', 'Basic realm=Authorization Required'); res.sendStatus(401); } ISYServer.prototype.authHandler = function (req, res, next) { var user = basicAuth(req); if(!this.getConfigSetting(this.CONFIG_REQUIRE_AUTH)) { return next(); } if (!user || !user.name || !user.pass) { this.buildUnauthorizedResponse(res); log('ERROR: Denied request, credentials not specified. user='+user); return res; } if (user.name === this.getConfigSetting(this.CONFIG_USERNAME) && user.pass === this.getConfigSetting(this.CONFIG_PASSWORD)) { return next(); } else { this.buildUnauthorizedResponse(res); log('ERROR: Denied request, bad credentials user='+user.name+' password='+user.pass); return res; } } ISYServer.prototype.getNextSequenceNumber = function() { this.sequenceNumber++; return this.sequenceNumber; } ISYServer.prototype.buildDeviceUpdate = function(device) { var updateData = '<?xml version="1.0"?><Event seqnum="'; updateData += this.getNextSequenceNumber(); updateData += '" side="uuid:47"><control>ST</control><action>'; updateData += device.getValue(); updateData += '</action><node>'; updateData += device.getAddress(); updateData += '</node><eventInfo></eventInfo></Event>'; return updateData; } ISYServer.prototype.sendDeviceUpdate = function(ws,device) { var updateData = this.buildDeviceUpdate(device) if(this.getConfigSetting(this.CONFIG_LOG_WEBSOCKET_NOTIFICATION)) { log('WEBSOCKET: NOTIFICATION: '+updateData); } ws.send(updateData); } ISYServer.prototype.sendDeviceUpdateWeb = function(subscribeUrl,device) { var that = this; var updateData = this.buildDeviceUpdate(device) if(this.getConfigSetting(this.CONFIG_LOG_WEB_NOTIFICATION)) { log('WEB: NOTIFICATION: Target URL: '+subscribeUrl) log('WEB: NOTIFICATION: '+updateData); } var options = { data: updateData, headers: { 'CONTENT-TYPE': 'text/xml' } } restler.post(subscribeUrl, options).on('error', function() { log('WEB: Notification failed to: '+subscribeUrl); }); } ISYServer.prototype.sendTroubledUpdateWeb = function(subscribeUrl,device) { var textToSend = ""+ "POST / HTTP/1.1\r\n"+ "HOST:10.0.1.4:39500\r\n"+ "CONTENT-TYPE:text/xml\r\n"+ "CONTENT-LENGTH: 147\r\n"+ "Connection: keep-alive\r\n"+ "\r\n"+ '<?xml version="1.0"?><Event seqnum="1" side="uuid:47"><control>ST</control><action>0</action><node>14 47 41 1</node><eventInfo></eventInfo></Event>' var net = require('net'); //var HOST = '10.0.1.44'; //var PORT = 3001; var HOST = '10.0.1.4' var PORT = 39500 var client = new net.Socket(); client.connect(PORT, HOST, function() { console.log('CONNECTED TO: ' + HOST + ':' + PORT); // Write a message to the socket as soon as the client is connected, the server will receive it as message from the client client.write(textToSend); client.destroy(); }); // Add a 'data' event handler for the client socket // data is what the server sent to this socket client.on('data', function(data) { console.log('DATA: ' + data); // Close the client socket completely client.destroy(); }); // Add a 'close' event handler for the client socket client.on('close', function() { console.log('Connection closed'); }); } ISYServer.prototype.buildElkZoneUpdate = function(zone, type, value) { var updateData = '<?xml version="1.0"?><Event seqnum="'; updateData += this.getNextSequenceNumber(); updateData += '" sid="uuid:48"><control>_19</control>'; updateData += '<action>3</action>'; updateData += '<node></node>'; updateData += '<eventInfo>'; updateData += '<ze type="'+type+'" zone="'+zone+'" val="'+value+'" />'; updateData += '</eventInfo>'; updateData += '</Event>'; return updateData; } ISYServer.prototype.sendElkZoneUpdate = function(ws, zone, type, value) { var updateData = this.buildElkZoneUpdate(zone,type,value) if(this.getConfigSetting(this.CONFIG_LOG_WEBSOCKET_NOTIFICATION)) { log('WEBSOCKET: NOTIFICATION: '+updateData); } ws.send(updateData); } ISYServer.prototype.sendElkZoneUpdateWeb = function(subscribeUrl, zone, type, value) { var updateData = this.buildElkZoneUpdate(zone,type,value) if(this.getConfigSetting(this.CONFIG_LOG_WEB_NOTIFICATION)) { log('WEB: NOTIFICATION: '+updateData); } var options = { data: updateData, headers: { 'CONTENT-TYPE': 'text/xml' } } restler.post(subscribeUrl, options).on('error', function() { log('WEB: Notification failed to: '+subscribeUrl); }); } ISYServer.prototype.buildElkAreaUpdate = function(type, value) { var updateData = '<?xml version="1.0"?><Event seqnum="'; updateData += this.getNextSequenceNumber(); updateData += '" sid="uuid:48"><control>_19</control>'; updateData += '<action>2</action>'; updateData += '<node></node>'; updateData += '<eventInfo>'; updateData += '<ae type="' + type + '" area="1" val="' + value + '" />'; updateData += '</eventInfo>'; updateData += '</Event>'; return updateData; } ISYServer.prototype.sendElkAreaUpdate = function(ws, type, value) { var updateData = this.buildElkAreaUpdate(type, value); if(this.getConfigSetting(this.CONFIG_LOG_WEBSOCKET_NOTIFICATION)) { log('WEBSOCKET: NOTIFICATION: '+updateData); } ws.send(updateData); } ISYServer.prototype.sendElkAreaUpdateWeb = function(subscribeUrl,type,value) { var updateData = this.buildElkAreaUpdate(type,value); if(this.getConfigSetting(this.CONFIG_LOG_WEB_NOTIFICATION)) { log('WEB: NOTIFICATION: Target URL: '+subscribeUrl) log('WEB: NOTIFICATION: '+updateData); } var options = { data: updateData, headers: { 'CONTENT-TYPE': 'text/xml' } } restler.post(subscribeUrl, options).on('error', function() { log('WEB: Notification failed to: '+subscribeUrl); }); } ISYServer.prototype.sendAlarmStatusUpdate = function(ws, alarmStatus) { if(value == 0) { this.sendElkAreaUpdate(ws, 3, alarmStatus); this.sendElkAreaUpdate(ws, 2, 1); } else { this.sendElkAreaUpdate(ws, 3, alarmStatus); this.sendElkAreaUpdate(ws, 2, 4); this.sendElkAreaUpdate(ws, 2, 3); setTimeout(function () { this.sendElkAreaUpdate(ws, 2, 4); }, 500); } } ISYServer.prototype.sendDeviceUpdateToAll = function(device) { for(var socketIndex = 0; socketIndex < this.webSocketClientList.length; socketIndex++) { this.sendDeviceUpdate(this.webSocketClientList[socketIndex],device); } var tempWebList = this.webSubscriptions.slice() for(var webIndex = 0; webIndex < tempWebList.length; webIndex++) { this.sendDeviceUpdateWeb(tempWebList[webIndex],device); } } ISYServer.prototype.sendInitialState = function(ws) { /*this.sendElkAreaUpdate(ws, "1", this.elkStatus.getAreaAttribute("1")); this.sendElkAreaUpdate(ws, "3", this.elkStatus.getAreaAttribute("3")); this.sendElkAreaUpdate(ws, "2", this.elkStatus.getAreaAttribute("2"));*/ for(var i = 0; i < this.nodeList.length; i++) { var device = this.nodeList[i]; if(device instanceof DeviceNode) { if(device.hasValue()) { this.sendDeviceUpdate(ws, device); } } } } ISYServer.prototype.sendInitialWebState = function(endpoint) { /*this.sendElkAreaUpdateWeb(endpoint, "1", this.elkStatus.getAreaAttribute("1")); this.sendElkAreaUpdateWeb(endpoint, "3", this.elkStatus.getAreaAttribute("3")); this.sendElkAreaUpdateWeb(endpoint, "2", this.elkStatus.getAreaAttribute("2"));*/ for(var i = 0; i < this.nodeList.length; i++) { var device = this.nodeList[i]; if(device instanceof DeviceNode) { if(device.hasValue()) { this.sendDeviceUpdateWeb(endpoint, device); // this.sendTroubledUpdateWeb(endpoint,device); } } } } ISYServer.prototype.buildVariableUpdate = function(variable) { var timeStamp = dateFormat(variable.getTs(), "yyyymmdd hh:MM:ss"); var updateData = '<?xml version="1.0"?><Event seqnum="'; updateData += this.getNextSequenceNumber(); updateData += '" sid="uuid:48"><control>_1</control>'; updateData += '<action>6</action>'; updateData += '<node></node>'; updateData += '<eventInfo>'; updateData += '<var type="' + variable.getType() + '" id="'+variable.getId()+'">\n<val>' + variable.getValue() + '</val>\n<ts>'+timeStamp+'</ts></var>'; updateData += '</eventInfo>'; updateData += '</Event>'; return updateData; } ISYServer.prototype.sendVariableUpdate = function(ws, variable) { var updateData = this.buildVariableUpdate(variable); if(this.getConfigSetting(this.CONFIG_LOG_WEBSOCKET_NOTIFICATION)) { log('WEBSOCKET: NOTIFICATION: '+updateData); } ws.send(updateData); } ISYServer.prototype.sendVariableUpdateWeb = function(subscribeUrl,variable) { var updateData = this.buildVariableUpdate(variable); if(this.getConfigSetting(this.CONFIG_LOG_WEB_NOTIFICATION)) { log('WEB: NOTIFICATION: Target URL: '+subscribeUrl); log('WEB: NOTIFICATION: '+updateData); } var options = { data: updateData, headers: { 'CONTENT-TYPE': 'text/xml' } } restler.post(subscribeUrl, options).on('error', function() { log('WEB: Notification failed to: '+subscribeUrl); }); } ISYServer.prototype.handleVariableGetRequest = function(req,res) { var variableType = req.params.type; var variableId = req.params.id; this.logRequestStartDetails(req); if(this.getConfigSetting(this.CONFIG_FAIL_VARIABLE_CALLS)) { this.buildCommandResponseEmpty(res, 404); this.logRequestEndDetails(res); return; } if(variableType != '1' && variableType != '2') { this.buildCommandResponse(res, false, 500, 'Invalid variable type specified'); this.logRequestEndDetails(res); return; } var variableList = this.variables[variableType]; var isFound = false; for(var variableIndex = 0; variableIndex < variableList.length; variableIndex++) { var variable = variableList[variableIndex]; if(variable.getId() == variableId) { var result = ""; this.setupResponseHeaders(res, 200); var responseDoc = this.createResponseDocument(); responseDoc.appendChild(variable.getStateXml(responseDoc)); var result = responseDoc.toString(); if(this.getConfigSetting(this.CONFIG_LOG_RESPONSE_BODY)) { log(result); } res.send(result); this.logRequestEndDetails(res); return; } } this.buildCommandResponse(res, false, 404, 'Specified variable was not found'); this.logRequestEndDetails(res); return; } ISYServer.prototype.sendVariableUpdateToAll = function(variable) { for(var socketIndex = 0; socketIndex < this.webSocketClientList.length; socketIndex++) { this.sendVariableUpdate(this.webSocketClientList[socketIndex],variable); } var tempWebList = this.webSubscriptions.slice() for(var webIndex = 0; webIndex < tempWebList.length; webIndex++) { this.sendVariableUpdateWeb(tempWebList[webIndex],variable); } } ISYServer.prototype.handleVariableSetRequest = function(req,res) { var variableType = req.params.type; var variableId = req.params.id; var variableNewValue = req.params.value; this.logRequestStartDetails(req); if(variableType != '1' && variableType != '2') { this.buildCommandResponse(res, false, 500, 'Invalid variable type specified'); this.logRequestEndDetails(res); return; } var variableList = this.variables[variableType]; var isFound = false; for(var variableIndex = 0; variableIndex < variableList.length; variableIndex++) { var variable = variableList[variableIndex]; if(variable.getId() == variableId) { var didChange = variableNewValue != variable.getValue(); variable.setValue(variableNewValue); this.buildCommandResponse(res, true, 200, "Variable value set"); this.logRequestEndDetails(res); if(didChange) { this.sendVariableUpdateToAll(variable); } return; } } this.buildCommandResponse(res, false, 404, 'Specified variable was not found'); this.logRequestEndDetails(res); return; } ISYServer.prototype.handleVariableGetListRequest = function(req,res) { var variableType = req.params.type; this.logRequestStartDetails(req); if(this.getConfigSetting(this.CONFIG_FAIL_VARIABLE_CALLS)) { this.buildCommandResponseEmpty(res, 404); this.logRequestEndDetails(res); return; } if(variableType != '1' && variableType != '2') { this.buildCommandResponse(res, false, 500, 'Invalid variable type specified'); this.logRequestEndDetails(res); return; } var variableList = this.variables[variableType]; var result = ""; this.setupResponseHeaders(res, 200); var responseDoc = this.createResponseDocument(); var varsElement = responseDoc.createElement('vars'); for(var varIndex = 0; varIndex < variableList.length; varIndex++) { varsElement.appendChild(variableList[varIndex].getStateXml(responseDoc)); } responseDoc.appendChild(varsElement); var result = responseDoc.toString(); if(this.getConfigSetting(this.CONFIG_LOG_RESPONSE_BODY)) { log(result); } res.send(result); this.logRequestEndDetails(res); } ISYServer.prototype.handleVariableDefinitionsRequest = function(req,res) { var variableType = req.params.type; this.logRequestStartDetails(req); if(this.getConfigSetting(this.CONFIG_FAIL_VARIABLE_CALLS)) { this.buildCommandResponseEmpty(res, 404); this.logRequestEndDetails(res); return; } if(variableType != '1' && variableType != '2') { this.buildCommandResponse(res, false, 500, 'Invalid variable type specified'); this.logRequestEndDetails(res); return; } this.logRequestStartDetails(req); this.setupResponseHeaders(res,200); if(this.getConfigSetting(this.CONFIG_LOG_RESPONSE_BODY)) { log(this.varData[variableType].toString()); } res.send(this.varData[variableType].toString()); this.logRequestEndDetails(res); } ISYServer.prototype.configureRoutes = function() { var that = this; this.app.get('/config/reset', function(req, res) { that.handleResetNodesRequest(req,res); }); this.app.get('/config/:configName/:configValue', function(req, res) { that.handleConfigureRequest(req,res); }); this.app.get('/rest/nodes/scenes', this.authHandler.bind(this), function (req, res) { that.handleSceneRequest(req,res); }); this.app.get('/rest/nodes/:address/cmd/:command/:parameter', this.authHandler.bind(this), function (req, res) { that.handleCommandRequest(req,res); }); this.app.get('/rest/nodes/:address/cmd/:command', this.authHandler.bind(this), function (req, res) { that.handleCommandRequest(req,res); }); this.app.get('/rest/nodes/:address', this.authHandler.bind(this), function (req, res) { that.handleNodeRequest(req,res); }); this.app.get('/rest/nodes', this.authHandler.bind(this), function (req, res) { that.handleNodesRequest(req,res); }); this.app.get('/rest/vars/get/:type/:id', this.authHandler.bind(this), function (req, res) { that.handleVariableGetRequest(req,res); }); this.app.get('/rest/vars/set/:type/:id/:value', this.authHandler.bind(this), function (req, res) { that.handleVariableSetRequest(req,res); }); this.app.get('/rest/vars/get/:type', this.authHandler.bind(this), function (req, res) { that.handleVariableGetListRequest(req,res); }); this.app.get('/rest/vars/definitions/:type', this.authHandler.bind(this), function (req, res) { that.handleVariableDefinitionsRequest(req,res); }); this.app.get('/rest/status', this.authHandler.bind(this), function (req, res) { that.handleStatusRequest(req,res); }); this.app.post('/services', this.authHandler.bind(this), function (req, res) { that.handleAddWebSubscription(req,res) }); this.app.get('/rest/elk/get/topology', this.authHandler.bind(this), function (req, res) { if(!that.getConfigSetting(that.CONFIG_ELK_ENABLED)) { res.status(500).send('Elk is disabled'); } else { that.handleElkTopologyRequest(req,res); } }); this.app.get('/rest/elk/get/status', this.authHandler.bind(this), function (req,res) { if(!that.getConfigSetting(that.CONFIG_ELK_ENABLED)) { res.status(500).send('Elk is disabled'); } else { that.handleElkStatusRequest(req,res); } }); } ISYServer.prototype.removeSocket = function(ws) { for(var socketIndex = 0; socketIndex < this.webSocketClientList.length; socketIndex++) { if(this.webSocketClientList[socketIndex] == ws) { this.webSocketClientList.splice(socketIndex,1); return; } } } ISYServer.prototype.start = function() { var that = this; this.configureRoutes(); var server = this.app.listen(this.port, function () { var host = server.address().address; var port = server.address().port; this.ssdpServer = new Ssdp(); this.ssdpServer.announce({ name: 'urn:udi-com:device:X_Insteon_Lighting_Device:1', port: port}); console.log('fake-isy-994i app listening at http://%s:%s', host, port); server.on('upgrade', function(request, socket, body) { log('WEBSOCKET: Incoming upgrade request..'); if (WebSocket.isWebSocket(request)) { var ws = new WebSocket(request, socket, body); log('WEBSOCKET: Incoming websocket connection request ver='+ws.version+' proto='+ws.protocol); ws.on('close', function(event) { log('WEBSOCKET: close event code='+event.code+" reason="+event.reason); that.removeSocket(ws); ws = null; }); that.webSocketClientList.push(ws); that.sendInitialState(ws); } else { log('WEBSOCKET: IGNORED: Upgrade request ignored, not a websocket'); } }); }); } exports.ISYServer = ISYServer;