UNPKG

node-red-contrib-snap4city-d3-dashboard-widgets

Version:

Nodes for Snap4city project for D3 Dashboard Widgets

512 lines (442 loc) 20.7 kB
/* NODE-RED-CONTRIB-SNAP4CITY-USER Copyright (C) 2018 DISIT Lab http://www.disit.org - University of Florence This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ module.exports = function (RED) { function Snap4D3Dashboard(config) { RED.nodes.createNode(this, config); var node = this; var WebSocket = require('ws'); var s4cUtility = require("./snap4city-utility.js"); const logger = s4cUtility.getLogger(RED, node); const uid = s4cUtility.retrieveAppID(RED); logger.debug("Snap4D3Dashboard"); const wsServer = s4cUtility.settingUrl(RED,node, "wsServerUrl", "wss://www.snap4city.org", "/wsserver", true); const wsServerHttpOrigin = s4cUtility.settingUrl(RED,node, "wsServerHttpOrigin", "wss://www.snap4city.org", "", true); node.ws = null; node.notRestart = false; //Meccanismo di passaggio dei valori tra il menu di add/edit node e il codice del nodo node.name = "NR_" + node.id.replace(".", "_"); node.widgetTitle = config.name; node.username = config.username; node.flowName = config.flowName; node.selectedDashboardId = config.selectedDashboardId; node.dashboardId = config.dashboardId; node.valueType = "String"; //config.valueType; node.startValue = 0; node.minValue = null; node.maxValue = null; node.offValue = null; node.onValue = null; node.domain = "singleNumericValue"; node.httpRoot = null; node.metricName = "NR_" + node.id.replace(".", "_"); node.metricType = "Series"; node.metricShortDesc = config.metricName; node.metricFullDesc = config.metricName; node.getD3Configuration = function(msg,config){ logger.info("\n getD3Configuration"); return msg.configuration && msg.configuration.trim().length!=0 ? msg.configuration : config.d3Configuration; } node.senDataToWidget = function(msg,timeout){ logger.info("\n senDataToWidget"); const d3Configuration = node.getD3Configuration(msg,config); logger.info("\n d3Configuration"); logger.info(d3Configuration); logger.info("\n"); const newMetricData = { msgType: "AddMetricData", nodeId: node.id, metricName: encodeURIComponent(node.metricName), metricType: node.metricType, newValue: { d3Configuration: d3Configuration, payload: msg.payload }, appId: uid, user: node.username, flowId: node.z, flowName: node.flowName, accessToken: s4cUtility.retrieveAccessToken(RED, node, config.authentication, uid), }; const metricDataAsJson = JSON.stringify(newMetricData); logger.info(metricDataAsJson); setTimeout(function () { try { logger.info("Sending inside timeout"); node.ws.send(metricDataAsJson); } catch (e) { logger.error("Error sending data to WebSocket for Snap4D3 node " + node.name + ": " + e); } }, timeout); s4cUtility.eventLog(RED, msg, newMetricData, config, "Node-Red", "Dashboard", wsServer, "TX"); } node.on('input', function (msg) { logger.info("Flow input received for Snap4D3 node " + node.name + ": " + JSON.stringify(msg)); let timeout = 0; if ((new Date().getTime() - node.wsStart) > parseInt(RED.settings.wsReconnectTimeout ? RED.settings.wsReconnectTimeout : 1200) * 1000) { if (node.ws != null) { node.ws.removeListener('error', node.wsErrorCallback); node.ws.removeListener('open', node.wsOpenCallback); node.ws.removeListener('message', node.wsMessageCallback); node.ws.removeListener('close', node.wsCloseCallback); node.ws.removeListener('pong', node.wsHeartbeatCallback); node.notRestart = true; node.ws.terminate(); node.ws = null; } else { logger.info("Why ws is null? I am in node.on('input'"); } node.ws = new WebSocket(wsServer, { origin: wsServerHttpOrigin }); node.ws.on('error', node.wsErrorCallback); node.ws.on('open', node.wsOpenCallback); node.ws.on('message', node.wsMessageCallback); node.ws.on('close', node.wsCloseCallback); node.ws.on('pong', node.wsHeartbeatCallback); logger.info("Snap4D3 node " + node.name + " is reconnetting to open WebSocket"); timeout = 1000; } node.wsStart = new Date().getTime(); node.senDataToWidget(msg,timeout); }); node.on('close', function (removed, closedDoneCallback) { if (removed) { // Cancellazione nodo logger.info("Snap4D3 node " + node.name + " is being removed from flow"); node.deleteEmitter(); node.deleteMetric(); } else { // Riavvio nodo logger.info("Snap4D3 node " + node.name + " is being rebooted"); node.notRestart = true; node.ws.terminate(); } clearInterval(node.pingInterval); closedDoneCallback(); }); node.registerSnap4D3Node = function(){ const accessToken = s4cUtility.retrieveAccessToken(RED, node, config.authentication, uid); // register Snap4D3 node as emitter to receive data from widget const payloadEmitter = { msgType: "AddEmitter", name: node.name, valueType: node.valueType, user: node.username, startValue: node.startValue, domainType: node.domain, offValue: node.offValue, onValue: node.onValue, minValue: node.minValue, maxValue: node.maxValue, endPointPort: (RED.settings.externalPort ? RED.settings.externalPort : 1895), endPointHost: (RED.settings.dashInNodeBaseUrl ? RED.settings.dashInNodeBaseUrl : "'0.0.0.0'"), httpRoot: node.httpRoot, appId: uid, flowId: node.z, flowName: node.flowName, nodeId: node.id, widgetType: "widgetSnap4D3", widgetTitle: node.widgetTitle, dashboardTitle: "", dashboardId: node.dashboardId, accessToken: accessToken }; // register Snap4D3 node as metric manager to send data to widget const payloadMetric = { msgType: "AddEditMetric", metricName: encodeURIComponent(node.metricName), metricType: node.metricType, nodeId: node.id, startValue: node.startValue, user: node.username, metricShortDesc: node.metricShortDesc, metricFullDesc: node.metricFullDesc, appId: uid, flowId: node.z, flowName: node.flowName, widgetType: "widgetSnap4D3", widgetTitle: node.widgetTitle, dashboardTitle: '', dashboardId: node.dashboardId, httpRoot: node.httpRoot, accessToken: accessToken }; if (node.pingInterval == null) { node.pingInterval = setInterval(function () { if (node.ws != null) { try { node.ws.ping(); // KEEP ALIVE LOGIC } catch (e) { logger.info("Errore on Ping " + e); } } }, 30000); } logger.info("Snap4D3 node " + node.name + " IS GOING TO CONNECT WS"); if (payloadMetric.accessToken != "") { setTimeout(function () { node.ws.send(JSON.stringify(payloadMetric)); node.ws.send(JSON.stringify(payloadEmitter)); }, Math.random() * 2000); } else { node.status({ fill: "red", shape: "dot", text: "Authentication Problem" }); } } node.wsOpenCallback = function () { if (node.dashboardId != null && node.dashboardId != "") { node.status({ fill: "green", shape: "dot", text: "connected to " + wsServer }); if (RED.settings.hasOwnProperty('httpRoot')) { if (RED.settings.httpRoot !== '/') { node.httpRoot = RED.settings.httpRoot; } else { node.httpRoot = null; } } node.registerSnap4D3Node(); } else { node.status({ fill: "red", shape: "dot", text: "No dashboard selected" }); } }; node.sendDataToNextNode = function(data){ node.status({ fill: "green", shape: "dot", text: "received data from widget" }); const message = { payload: data }; node.send(message); // sends the data received from the widget to the next node return message; } node.parseJSONOrReturnNull = function(jsonData){ try{ return JSON.parse(jsonData); }catch(e){ return null; } } node.wsMessageCallback = function (data) { const response = JSON.parse(data); logger.info(response); switch (response.msgType) { case "AddEmitter": case "AddEditMetric": if (response.result === "Ok") { node.widgetUniqueName = response.widgetUniqueName; logger.info("WebSocket server correctly added/edited emitter type or metric for Snap4D3 node " + node.name + ": " + response.result); node.status({ fill: "green", shape: "dot", text: "Created on dashboard" }); if (node.intervalID != null) { clearInterval(node.intervalID); node.intervalID = null; } } else { logger.info("WebSocket server could not add/edit emitter type or metric for Snap4D3 node " + node.name + ": " + response.result); node.status({ fill: "red", shape: "dot", text: response.error }); node.error(response.error); } break; case "DelEmitter": case "DelMetric": if (response.result === "Ok") { logger.info("WebSocket server correctly deleted emitter type or metric for Snap4D3 node " + node.name + ": " + response.result); } else { logger.info("WebSocket server could not delete emitter type or metric for Snap4D3 node " + node.name + ": " + response.result); } logger.info("Closing webSocket server for Snap4D3 node " + node.name); node.notRestart = true; if(node.ws!=null){ node.ws.terminate(); } break; case "DataToEmitter": logger.info("[DATA TO EMITTER] "+data); if(response.newValue == "dashboardDeleted"){ node.status({ fill: "red", shape: "dot", text: "Dashboard deleted" }); }else{ node.status({ fill: "green", shape: "dot", text: "connected to " + wsServer }); let outputMessage = {}; const messageFromWidget = node.parseJSONOrReturnNull(response.newValue); if(messageFromWidget!=null && messageFromWidget.widgetOperation== "SendToSnap4D3"){ // received data from widget outputMessage = node.sendDataToNextNode(messageFromWidget.widgetData); } node.ws.send(JSON.stringify({ msgType: "DataToEmitterAck", widgetUniqueName: node.widgetUniqueName, result: "Ok", msgId: response.msgId, accessToken: s4cUtility.retrieveAccessToken(RED, node, config.authentication, uid) })); s4cUtility.eventLog(RED, response, outputMessage, config, "Node-Red", "Dashboard", RED.settings.httpRoot + node.name, "RX"); } break; default: logger.info(response.msgType); break; } }; node.wsCloseCallback = function (e) { logger.info("Snap4D3 node " + node.name + " closed WebSocket"); logger.info("Snap4D3 closed reason " + e); if (!(node.dashboardId != null && node.dashboardId != "")) { node.status({ fill: "red", shape: "dot", text: "No dashboard selected" }); } else { node.status({ fill: "red", shape: "ring", text: "lost connection from " + wsServer }); } if (node.ws != null) { node.ws.removeListener('error', node.wsErrorCallback); node.ws.removeListener('open', node.wsOpenCallback); node.ws.removeListener('message', node.wsMessageCallback); node.ws.removeListener('close', node.wsCloseCallback); node.ws.removeListener('pong', node.wsHeartbeatCallback); node.ws = null; } else { logger.info("Why ws is null? I am in node.wsCloseCallback"); } const wsServerRetryActive = (RED.settings.wsServerRetryActive ? RED.settings.wsServerRetryActive : "yes"); const wsServerRetryTime = (RED.settings.wsServerRetryTime ? RED.settings.wsServerRetryTime : 30); if (wsServerRetryActive === 'yes' && !node.notRestart) { const reconnectionRetryTime = parseInt(wsServerRetryTime); node.status({ fill: "red", shape: "ring", text: "lost connection from " + wsServer + " will try to reconnect in " + reconnectionRetryTime + "s" }); logger.info("Snap4D3 node " + node.name + " will try to reconnect to WebSocket in " + reconnectionRetryTime + "s"); if (!node.intervalID) { node.intervalID = setInterval(node.wsInit, reconnectionRetryTime * 1000); } } node.notRestart = false; }; node.wsErrorCallback = function (e) { logger.info("Snap4D3 node " + node.name + " got WebSocket error: " + e); }; node.deleteEmitter = function () { logger.info("Deleting emitter via webSocket for Snap4D3 node " + node.name); const newMsg = { msgType: "DelEmitter", nodeId: node.id, user: node.username, appId: uid, flowId: node.z, flowName: node.flowName, accessToken: s4cUtility.retrieveAccessToken(RED, node, config.authentication, uid) }; try { node.ws.send(JSON.stringify(newMsg)); } catch (e) { logger.info("Error deleting emitter via webSocket for Snap4D3 node " + node.name + ": " + e); } }; node.deleteMetric = function () { logger.info("Deleting metric via webSocket for Snap4D3 node " + node.name); const newMsg = { msgType: "DelMetric", nodeId: node.id, metricName: encodeURIComponent(node.metricName), metricType: node.metricType, user: node.username, appId: uid, flowId: node.z, flowName: node.flowName, accessToken: s4cUtility.retrieveAccessToken(RED, node, config.authentication, uid) }; try { node.ws.send(JSON.stringify(newMsg)); } catch (e) { logger.info("Error deleting metric via webSocket for Snap4D3 node " + node.name + ": " + e); } }; node.wsHeartbeatCallback = function () { logger.silly("heartbeat callback"); }; //Lasciarlo, altrimenti va in timeout!!! https://nodered.org/docs/creating-nodes/node-js#closing-the-node node.closedDoneCallback = function () { logger.info("Snap4D3 node " + node.name + " has been closed"); }; node.wsInit = function (e) { logger.info("Snap4D3 node " + node.name + " is trying to open WebSocket"); try { node.status({ fill: "yellow", shape: "dot", text: "connecting to " + wsServer }); if (node.ws == null) { node.ws = new WebSocket(wsServer, { origin: wsServerHttpOrigin }); node.ws.on('error', node.wsErrorCallback); node.ws.on('open', node.wsOpenCallback); node.ws.on('message', node.wsMessageCallback); node.ws.on('close', node.wsCloseCallback); node.ws.on('pong', node.wsHeartbeatCallback); node.wsStart = new Date().getTime(); } else { logger.info("Snap4D3 node " + node.name + " already open WebSocket"); } } catch (e) { logger.info("Snap4D3 node " + node.name + " could not open WebSocket"); node.status({ fill: "red", shape: "ring", text: "unable to connect to " + wsServer }); node.wsCloseCallback(); } }; //Inizio del "main" try { node.wsInit(); logger.info("\n senDataToWidget"); } catch (e) { logger.info("Snap4D3 node " + node.name + " got main exception connecting to WebSocket"); } } RED.nodes.registerType("Snap4D3", Snap4D3Dashboard); };