UNPKG

node-red-contrib-felix

Version:

Node-RED palette of nodes for sending data to the Felix platform.

304 lines (278 loc) 11.4 kB
/** * Copyright IOT Technology Solutions * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * NOTES * ===== * - API Key is stored as a credential, so it is not included in exports * (see https://nodered.org/docs/creating-nodes/credentials) * * - Incoming message can specify 'msg.apiKey', 'msg.dvcId', 'msg.channel', * 'msg.units', and 'msg.valueTag' to override the corresponding configured values. * * TODOs * ===== * - TODO: Allow multiple Tag Values (and channel name and units for each) to be reported from incoming message in a * single post (see Edit Dropdown Node in Dashboard palette for example of adding multiple config entries). * * - TODO: Allow setting a rate limit, and queue data values internally until time to send, but then send all values * (and possibly provide ability to limit to only include last value). * * - TODO: Extract Felix URL, TLS setting, device and APIKey into a named config object/node that can be shared across * Felix nodes. * * - TODO: Allow other Felix headers to be configured and/or overridden (device type, manufacturer, protocol, version). * **/ module.exports = function(RED) { "use strict"; var http = require("follow-redirects").http; var https = require("follow-redirects").https; var urllib = require("url"); var hashSum = require("hash-sum"); function felixDataNode(config) { RED.nodes.createNode(this,config); var node = this; var nodeUrl = config.url; var nodeApiKey = this.credentials.apikey; var nodeDevice = config.device; var nodeChannel = config.channel; var nodeUnits = config.units; var nodeValTag = config.valtag; // if configured to use SSL/TLS, get TLS Configuration node if (config.tls) { var tlsNode = RED.nodes.getNode(config.tls); } this.reqTimeout = 10000; var prox, noprox; if (process.env.http_proxy != null) { prox = process.env.http_proxy; } if (process.env.HTTP_PROXY != null) { prox = process.env.HTTP_PROXY; } if (process.env.no_proxy != null) { noprox = process.env.no_proxy.split(","); } if (process.env.NO_PROXY != null) { noprox = process.env.NO_PROXY.split(","); } this.on("input",function(msg) { var preRequestTimestamp = process.hrtime(); var url = nodeUrl; if (!url) { node.error("Data Collection URL of Felix platform must be configured"); node.status({fill:"red",shape:"ring",text:"URL Required"}); return; } // url must start http:// or https:// so assume http:// if not set if (url.indexOf("://") !== -1 && url.indexOf("http") !== 0) { node.warn("Invalid Transport specified in URL, only http or https supported"); node.status({fill:"red",shape:"ring",text:"Invalid Transport"}); return; } if (!((url.indexOf("http://") === 0) || (url.indexOf("https://") === 0))) { if (tlsNode) { url = "https://"+url; } else { url = "http://"+url; } } // make sure URL ends in a / if (url.lastIndexOf('/') !== url.length - 1) { url =+ '/'; } var useApiKey = nodeApiKey; if ("undefined" !== typeof msg.apiKey && 0 < msg.apiKey.length) { useApiKey = msg.apiKey; } if (!useApiKey) { node.error("Felix API Key must be configured, or specified as 'apiKey' field of input message"); node.status({fill:"red",shape:"ring",text:"API Key Required"}); return; } var useDevice = nodeDevice; if ("undefined" !== typeof msg.dvcId && 0 < msg.dvcId.length) { useDevice = msg.dvcId; } if (!useDevice) { node.error("Device ID must be configured, or specified as 'dvcId' field of input message"); node.status({fill:"red",shape:"ring",text:"Device ID Required"}); return; } var useChannel = nodeChannel; if ("undefined" !== typeof msg.channel && 0 < msg.channel.length) { useChannel = msg.channel; } if (!useChannel) { node.error("Channel Path must be configured, or specified as 'channel' field of input message"); node.status({fill:"red",shape:"ring",text:"Channel Path Required"}); return; } url = url + useDevice; var opts = urllib.parse(url); opts.method = "POST"; opts.headers = { "x-api-key" : useApiKey, "x-dvc-type-cd" : "PLC-Gateway", "x-mfr-id" : "Kepware", "x-proto-id" : "IOT-KEPWARE", "x-proto-version" : 1 }; var ctSet = "Content-Type"; // set default camel case var clSet = "Content-Length"; var payload = null; var ts = (new Date()).getTime(); var useUnits = nodeUnits; if (null !== msg.units) { useUnits = msg.units; } if (!useUnits) { useUnits = ""; } else { useUnits = 'u:"'+useUnits+'",'; } var useValueTag = nodeValTag; if (null !== msg.valueTag) { useValueTag = msg.valueTag; } if (typeof msg.payload !== "undefined") { if (typeof msg.payload === "string" || Buffer.isBuffer(msg.payload)) { payload = '{timestamp:'+ts+',values:[{id:"'+useChannel+'",v:"'+msg.payload+'",'+useUnits+'q:true,t:'+ts+'}]}'; } else if (typeof msg.payload == "number") { payload = '{timestamp:'+ts+',values:[{id:"'+useChannel+'",v:'+msg.payload+','+useUnits+'q:true,t:'+ts+'}]}'; } else { var val = msg.payload[useValueTag]; if (typeof val !== "undefined") { if (typeof val === "string") { payload = '{timestamp:'+ts+',values:[{id:"'+useChannel+'",v:"'+val+'",'+useUnits+'q:true,t:'+ts+'}]}'; } else if (typeof val == "number") { payload = '{timestamp:'+ts+',values:[{id:"'+useChannel+'",v:'+val+','+useUnits+'q:true,t:'+ts+'}]}'; } else if (typeof val == "boolean") { payload = '{timestamp:'+ts+',values:[{id:"'+useChannel+'",v:'+val+',q:true,t:'+ts+'}]}'; } else { node.error("Unsupported value type: " + typeof val); node.status({fill:"red",shape:"ring",text:"Unsupported value type: " + typeof val}); return; } } else { // field not present in message node.status({fill:"yellow",shape:"dot",text:"No Data"}); return; } } opts.headers[ctSet] = "application/json"; if (opts.headers['content-length'] == null) { if (Buffer.isBuffer(payload)) { opts.headers[clSet] = payload.length; } else { opts.headers[clSet] = Buffer.byteLength(payload); } } } //this.log("Payload:"+payload); var urltotest = url; var noproxy; if (noprox) { for (var i in noprox) { if (url.indexOf(noprox[i]) !== -1) { noproxy=true; } } } if (prox && !noproxy) { var match = prox.match(/^(http:\/\/)?(.+)?:([0-9]+)?/i); if (match) { //opts.protocol = "http:"; //opts.host = opts.hostname = match[2]; //opts.port = (match[3] != null ? match[3] : 80); opts.headers['Host'] = opts.host; var heads = opts.headers; var path = opts.pathname = opts.href; opts = urllib.parse(prox); opts.path = opts.pathname = path; opts.headers = heads; opts.method = method; urltotest = match[0]; if (opts.auth) { opts.headers['Proxy-Authorization'] = "Basic "+new Buffer(opts.auth).toString('Base64') } } else { node.warn("Bad proxy url: "+process.env.http_proxy); } } if (tlsNode) { tlsNode.addTLSOptions(opts); } msg.requestHeaders = opts.headers; var req = ((/^https/.test(urltotest))?https:http).request(opts,function(res) { // Force NodeJs to return a Buffer (instead of a string) // See https://github.com/nodejs/node/issues/6038 res.setEncoding(null); delete res._readableState.decoder; msg.statusCode = res.statusCode; msg.headers = res.headers; msg.responseUrl = res.responseUrl; msg.payload = []; msg.headers['x-node-red-request-node'] = hashSum(msg.headers); if (200 === res.statusCode || 202 === res.statusCode) { node.status({fill:"green",shape:"dot",text:"Response: " + res.statusCode}); } else { node.status({fill:"red",shape:"dot",text:"Response: " + res.statusCode}); } res.on('data',function(chunk) { if (!Buffer.isBuffer(chunk)) { // if the 'setEncoding(null)' fix above stops working in // a new Node.js release, throw a noisy error so we know // about it. throw new Error("HTTP Request data chunk not a Buffer"); } msg.payload.push(chunk); }); res.on('end',function() { if (node.metric()) { // Calculate request time var diff = process.hrtime(preRequestTimestamp); var ms = diff[0] * 1e3 + diff[1] * 1e-6; var metricRequestDurationMillis = ms.toFixed(3); node.metric("duration.millis", msg, metricRequestDurationMillis); if (res.client && res.client.bytesRead) { node.metric("size.bytes", msg, res.client.bytesRead); } } // Check that msg.payload is an array - if the req error // handler has been called, it will have been set to a string // and the error already handled - so no further action should // be taken. #1344 if (Array.isArray(msg.payload)) { // Convert the payload to the required return type msg.payload = Buffer.concat(msg.payload); // bin msg.payload = msg.payload.toString('utf8'); // txt node.send(msg); //node.status({}); } }); }); req.setTimeout(node.reqTimeout, function() { node.error("No Response from Felix",msg); setTimeout(function() { node.status({fill:"red",shape:"ring",text:"No Response from Felix"}); },5); req.abort(); }); req.on('error',function(err) { node.error(err,msg); msg.payload = err.toString() + " : " + url; msg.statusCode = err.code; node.send(msg); node.status({fill:"red",shape:"ring",text:err.code}); }); if (payload) { req.write(payload); msg.requestPayload = payload; } req.end(); }); this.on("close",function() { node.status({}); }); } RED.nodes.registerType("felix-data", felixDataNode, {credentials: {apikey:{type:"password"}}} ); }