@metz-connect/node-red-ewio2
Version:
Provides nodes to access EWIO2 interfaces like IOs and metering - from Node-RED
798 lines (777 loc) • 44.3 kB
JavaScript
/**
* @file Library used by Node-RED to handle connection object.
* @author Metz Connect
*/
/**
* Config namespace.
* @namespace ewio2ConnObj
*/
const ConnectedState = require("./connectedState");
const {log, pubSub} = require("./general");
/**
* Timeout for websocket messages in milli-seconds
* @memberof Ewio2ConnObj
* @type {int}
*/
const wsMsgTimeout = 10000;
class Ewio2Connection {
configNodeId;
host;
user;
pw;
enc;
tlsConfig;
generalLivedataConfigMenu;
requestedIoType;
requestedIoPort;
connPromises;
getPromises;
wsPromise;
tan;
ws;
connState;
values;
registeredNodes;
timeoutId;
constructor(identifier, ioType, ioPort, tlsConfig, generalLivedataForConfigMenu, RED) {
this.configNodeId = identifier.configNodeId;
this.host = identifier.host;
this.user = identifier.user;
this.pw = identifier.pw;
this.enc = identifier.enc;
this.tlsConfig = {};
this.generalLivedataConfigMenu = generalLivedataForConfigMenu;
// set encryption values to connection object, if encryption is selected by customer
if (this.enc && tlsConfig) {
// decide whether to use backend data or frontend data
let tlsBackendNode = RED.nodes.getNode(tlsConfig.tlsConfigId);
// try to use backend data (if available)
if (tlsBackendNode) {
this.tlsConfig.tlsConfigId = tlsBackendNode.tlsConfigId;
this.tlsConfig.credentials = tlsBackendNode.credentials;
this.tlsConfig.verifyservercert = (tlsBackendNode.verifyservercert === "true"); // convert to bool
this.tlsConfig.servername = tlsBackendNode.servername;
this.tlsConfig.alpnprotocol = tlsBackendNode.alpnprotocol;
}
// overwrite backend data with frontend data (if available). Frontend data may be "newer" than backend data
if (tlsConfig) {
this.tlsConfig.tlsConfigId = tlsConfig.tlsConfigId;
if (tlsConfig.credentials) {
this.tlsConfig.credentials = tlsConfig.credentials;
}
this.tlsConfig.verifyservercert = (tlsConfig.verifyservercert === "true"); // convert to bool
this.tlsConfig.servername = tlsConfig.servername;
this.tlsConfig.alpnprotocol = tlsConfig.alpnprotocol;
}
}
this.connPromises = [];
this.getPromises = [];
this.requestedIoType = ioType;
this.requestedIoPort = ioPort;
this.connState = ConnectedState.IDLE;
this.values = {};
this.registeredNodes = new Set();
}
/**
* Establishes connection to EWIO2.
* Connects to REST API, to get session ID and establishes connnection to EWIO2 via websocket protocol.
* Handles errors and outputs error messages to Node-RED frontend and Node-RED backend.
* @memberof Ewio2ConnObj
* @param {Object} RED - Node-RED "infrastructure", used to publish events to frontend.
*/
async establishConnection(RED) {
const node = RED.nodes.getNode(this.configNodeId);
try {
var tan = await this.connectRestApi(RED, null);
// set the received TAN
this.tan = tan;
log(node, 'connect REST API done for: ' + this.host + ', TAN: ' + this.tan)
// for IO type measurements only REST API connection is necessary (no further websocker connection)
if (this.requestedIoType === "measurements") {
// resolve all connection promises
this.#resolveAllStoredConnPromises(RED);
}
else {
await this.#connectWebsocket(RED);
log(node, 'connect websocket done');
}
}
catch (error) {
log(node, 'establishConnection fct error: ' + error);
this.closeAndDeleteWs(RED);
let connectError = error.code;
let errorMsg = "";
// this errors are available with their name in translation files
if (error === "notAuthorized" || error === "missingCertificate" || error === "tlsConfigMissing" ) {
connectError = error;
}
// Node.JS error see: https://betterstack.com/community/guides/scaling-nodejs/nodejs-errors/
// addr/endpoint/service not reachable wrong IP??? wrong network? DNS error (e.g. EWIO2 host "10.10.x.y" OR EWIO2 host "myEWIO2" (which is not in /etc/hosts) and Node-RED running on EWIO2)
// DNS loopup timeout (e.g. EWIO2 host "myEWIO2" (which is not in /etc/hosts) and Node-RED running on PC OR error in /etc/hosts)
else if (connectError === "ECONNREFUSED" || connectError === "EHOSTUNREACH" || connectError === "ENETUNREACH" || connectError === "ENOTFOUND" || connectError === "EAI_AGAIN") {
errorMsg = connectError;
connectError = "notReachable";
}
// timeout connection closed (e.g. changed EWIO2s subnet, e.g. from 192.168.a.b to 10.10.x.y)
else if (connectError === "ETIMEDOUT" || connectError === "ECONNRESET") {
errorMsg = connectError;
connectError = "timeout";
}
// e.g. EWIO2 host "23436547546345345"
else if (connectError === "ERR_INVALID_URL") {
connectError = "invalidURL";
}
else if (connectError === "ENOENT") {
connectError = "missingCertificate"
}
else if (connectError === "DEPTH_ZERO_SELF_SIGNED_CERT") {
connectError = "selfSignedCertificate";
}
else if (connectError === "ERR_TLS_CERT_ALTNAME_INVALID") {
errorMsg = error.message;
connectError = "invalidCertificateAltname";
}
else {
errorMsg = connectError || error;
connectError = "unhandeledError";
}
const elem = {ioType: "", address: this.requestedIoPort, value: "ERROR", error: connectError, errorMsg: errorMsg};
this.publishIoPortStatus(elem, RED, true, false);
this.#resolveAllStoredConnPromises(RED);
this.#resolveNextStoredWsPromise(RED);
}
}
/**
* Send message to EWIO2 via websocket protocol.
* Waits until no predecessor message is in "waiting for response" state.
* Creates and stores a promise with timeout when websocket message is send to EWIO2.
* On timeout error is published, connection is closed, already stored values are deleted and promises are resovled to a secure state.
* @memberof Ewio2ConnObj
* @param {string} msg - Message which is send to EWIO2 via websocket.
* @param {Object} RED - Node-RED "infrastructure", used to publish events to frontend.
* @return {string} Value which was setted at EWIO2.
*/
async sendWsMessage(msg, RED) {
const connObj = this;
const node = RED.nodes.getNode(this.configNodeId);
// "queue" that waits until wsPromise for previous websocket msg was resolved
while (this.wsPromise) {
log(node, 'sendWsMessage, wait for predecessor msg');
await this.wsPromise.promise;
log(node, 'sendWsMessage, wait for predecessor msg done');
}
log(node, 'sendWsMessage, add promise for first msg');
let wsPromiseResolveTemp;
let wsPromiseRejectTemp;
// create new wsPromise when websocket msg is send
const wsPromiseTemp = new Promise((resolve, reject) => {
wsPromiseResolveTemp = resolve;
wsPromiseRejectTemp = reject;
this.timeoutId = setTimeout(function(){
log(node, "sendWsMessage timeout: " + msg);
const elem = {ioType: "", address: this.requestedIoPort, value: "ERROR", error: "timeoutWsMsg"};
connObj.publishIoPortStatus(elem, RED, true, false);
connObj.closeAndDeleteWs(RED);
connObj.deleteAllStoredValues();
connObj.#resolveAllStoredConnPromises(RED);
connObj.#resolveNextStoredWsPromise(RED);
}, wsMsgTimeout);
// check for existing websocket. If a msg gets a timeout, other pending msgs are not tried to send via closed websocket
if (this.ws) {
log(node, 'sendWsMessage: ' + msg);
this.ws.send(msg);
}
});
// store resolve function, reject function and promise itself as current wsPromise
this.wsPromise = {resolve: wsPromiseResolveTemp, reject: wsPromiseRejectTemp, promise: wsPromiseTemp};
log(node, 'sendWsMessage, wait for current msg');
// wait until wsPromise msg is resolved (correct response received)
const settedVal = await this.wsPromise.promise;
log(node, 'sendWsMessage, wait for current msg done');
return settedVal;
}
/**
* Establish REST API connection to EWIO, to request session-ID (TAN). Connection can be encrypted (https) or not (http).
* @memberof Ewio2ConnObj
* @param {Object} RED - Node-RED "infrastructure", used to publish events to frontend.
* @param {Object} measurementSettings - Settings required to get historic measurement data out of EWIO2 database.
* @return {Promise} Connection status to EWIO2 REST API, resolve (with received TAN) or reject (with error).
*/
connectRestApi(RED, measurementSettings = null) {
this.connState = ConnectedState.REST_API_CONNECTING;
const node = RED.nodes.getNode(this.configNodeId);
log(node, 'connectRestApi, host: ' + this.host);
const connObj = this;
var connectRestApiPromise = new Promise((resolve, reject) => {
log(node, 'enc: ' + this.enc + "; type: " + typeof this.enc);
// HTTPS
if (this.enc) {
// TLS config available (Object not empty)
if (Object.keys(connObj.tlsConfig).length > 0) {
const https = require('https');
let opts = {
method: 'GET',
host: connObj.host,
port: 443,// 443 is default
path: "/cgi-bin/getParamFromServer.cgi?username=" + connObj.user + "&password=" + connObj.pw + "&type=single_id&module=login",
ca: connObj.tlsConfig.credentials.cadata,
rejectUnauthorized: connObj.tlsConfig.verifyservercert,
cert: connObj.tlsConfig.credentials.certdata,
key: connObj.tlsConfig.credentials.keydata,
passphrase: connObj.tlsConfig.credentials.passphrase,
servername: connObj.tlsConfig.servername,
ALPNProtocols: connObj.tlsConfig.alpnprotocol
};
if (measurementSettings) {
opts.path = "/cgi-bin/getParamFromServer.cgi?username=" + connObj.user + "&password=" + measurementSettings.pw + "&type=db_data&module=" + measurementSettings.dp + "_" + measurementSettings.range + "_" + measurementSettings.quantity;
}
log(node, 'opts.path: ' + opts.path);
var reqHttps = https.request(opts, function(res){
var completeMsg = "";
res.on("data", function (chunk) {
completeMsg += chunk;
});
res.on("end", function() {
connObj.connState = ConnectedState.REST_API_CONNECTED;
resolve((connObj.requestedIoType === "measurements") ? completeMsg : connObj.#calcTanMd5Hash(completeMsg, connObj.pw));
})
}).end();
}
// TLS config missing
else {
log(node, "no TLS configuration available");
reject("tlsConfigMissing");
}
reqHttps.on("error", function(err) {
reject (err);
});
}
// HTTP
else {
const http = require('http');
var url = "http://" + this.host + "/cgi-bin/getParamFromServer.cgi?username=" + this.user + "&password=" + this.pw + "&type=single_id&module=login";
if (measurementSettings) {
url = "http://" + this.host + "/cgi-bin/getParamFromServer.cgi?username=" + this.user + "&password=" + measurementSettings.pw + "&type=db_data&module=" + measurementSettings.dp + "_" + measurementSettings.range + "_" + measurementSettings.quantity;
}
log(node, 'url: ' + url);
var req = http.get(url, function (response) {
var completeMsg = "";
response.on("data", function (chunk) {
completeMsg += chunk;
});
response.on("end", function() {
connObj.connState = ConnectedState.REST_API_CONNECTED;
resolve((connObj.requestedIoType === "measurements") ? completeMsg : connObj.#calcTanMd5Hash(completeMsg, connObj.pw));
})
});
req.on("error", function(err) {
reject (err);
});
}
});
return connectRestApiPromise;
}
/**
* Calculates the REST API TAN.
* @memberof Ewio2ConnObj
* @param {Object} data - Received response from REST API.
* @param {Object} pw - Hashed password for connection to EWIO2.
* @return {string} Calculated TAN-MD5-Hash.
*/
#calcTanMd5Hash(data, pw) {
const sessionId = data.toString();
const tanMd5Hash = pw + "1" + sessionId;
return tanMd5Hash;
}
/**
* Establishes connection to ioManager websocket of EWIO2. Connection can be encrypted (wss) or not (ws).
* This websocket connection is stored in connection object.
* When websocket connection is established, TAN is sent to websocket, to start initialisation.
* When a websocket message is received from EWIO2, the message handling start.
* This contains send next request to EWIO2, handle / resolve promise or publish data to Node-RED frontend or backend.
* @memberof Ewio2ConnObj
* @param {Object} RED - Node-RED "infrastructure", used to publish events to frontend.
* @return {Promise} Resolved when TAN is sent and rejected on error.
*/
#connectWebsocket(RED) {
const node = RED.nodes.getNode(this.configNodeId);
log(node, 'connectWebsocket, host: ' + this.host);
const websocketPromise = new Promise((resolve, reject) => {
const connObj = this;
const WebSocket = require("ws");
var ws;
//const ewioConnection = ewio2Connections[JSON.stringify(keyEwio2Data)];
if (this.enc) {
// TLS config available (Object not empty)
if (Object.keys(connObj.tlsConfig).length > 0) {
log(node, 'connectWebsocket(), to host: wss://' + this.host);
ws = new WebSocket("wss://" + this.host + "/iomanager", {
ca: connObj.tlsConfig.credentials.cadata,
rejectUnauthorized: connObj.tlsConfig.verifyservercert,
cert: connObj.tlsConfig.credentials.certdata,
key: connObj.tlsConfig.credentials.keydata,
passphrase: connObj.tlsConfig.credentials.passphrase,
servername: connObj.tlsConfig.servername,
ALPNProtocols: connObj.tlsConfig.alpnprotocol
});
}
// TLS config missing
else {
log(node, "no TLS configuration available");
reject("tlsConfigMissing");
}
}
else {
log(node, 'connectWebsocket(), to host: ws://' + this.host);
ws = new WebSocket("ws://" + this.host + "/iomanager");
}
this.ws = ws;
this.ws.on("open", async function() {
connObj.connState = ConnectedState.WEBSOCKET_OPEN;
log(node, 'connectWebsocket fct to: ' + connObj.host + ' send tan: ' + "!" + connObj.tan);
await connObj.sendWsMessage("!" + connObj.tan, RED);
resolve();
});
this.ws.on("message", async function(data) {
var strData = data.toString();
log(node, 'onMessage: ' + strData + ' from: ' + connObj.host + ' (user: ' + connObj.user + ')');
// authentication ok -> send message to EWIO2 which fits to current state
if (strData == "read") {
// resolve all connection promises
connObj.#resolveAllStoredConnPromises(RED);
connObj.#resolveNextStoredWsPromise(RED);
}
// read initially all IOs of the given type --> open livedata channel
else if (strData == "done") {
connObj.#resolveNextStoredWsPromise(RED);
}
// livedata channel opened -> resolve promise, to proceed execution
else if (strData.startsWith("done®livedata®")) {
connObj.connState = ConnectedState.CONNECTED;
// resolve next wsPromse
connObj.#resolveNextStoredWsPromise(RED);
}
else if (strData.startsWith("do®") || strData.startsWith("di®") || strData.startsWith("ao®") || strData.startsWith("ai®")) {
const parts = strData.split("®");
const recvIoTypeAddr = parts[0] + "®" + parts[1];
// the msg must contain at least 7 elemtens (7 for e.g. setValue response; 8 for livedata (contains also moduleName))
if (parts.length >= 7) {
// response of setValue and setConfig (analog input config) have 7 elements, response of setS0 (pulse counter active) and setPC (pulse counter value) have 9 elements
if (parts.length === 7 || parts.length === 9) {
connObj.#resolveNextStoredWsPromise(RED, parts[2]);
}
// store received data in ewio2Connections map (ioType®address as key; e.g. "do®01_0")
// idx 0 1 2 3 4 5 6 7
// DO: do®port®state®record® portname®description®manualMode®extensionModule , e.g. do®01_2®0®0®REL2 (21/22/24)®Light®0®MR-DIO4/2
// AO: ao®port®value®recordRange® portname®description®manualMode®extensionModule , e.g. ao®03_5®®0®AO4 (4/C2)®®0®MR-AO4
// idx 0 1 2 3 4 5 6 7 8 9
// DI: di®port®state®record®pulseCntrVisible®pulseCntrChecked®pulseCntrValue®portname®description® extensionModule , e.g. di®03_2®0®0®1®0®0®DI4 (4/C1)®Door®MR-DIO4/2
// idx 0 1 2 3 4 5 6 7
// AI: ai®port®value®recordRange®config® portname®description® extensionModule , e.g. ai®03_6®3.84®0®6®CI4 (I4/4-)®Poti®MR-CI4
let element;
// bitfield with 3 bits. All values can be changed!
// Bit 0: signal value changed
// Bit 1: pulse counter value changed
// Bit 2: pulse counter checked (activated) changed
let updatedSetting = 0;
// digital input
if (parts[0] === "di") {
let lastValue;
let lastPulseCntrValue;
let lastPulseCntrChecked;
if (connObj.values[recvIoTypeAddr]) {
// last value available in connection object
lastValue = connObj.values[recvIoTypeAddr].value;
// last pulse counter value available in connection object
lastPulseCntrValue = connObj.values[recvIoTypeAddr].pulseCntrValue;
// last pulse counter checked (activated) available in connection object
lastPulseCntrChecked = connObj.values[recvIoTypeAddr].pulseCntrChecked;
}
// only if value, pulse counter value or pulse counter checked (activated) has changed
if ((lastValue != parts[2]) || lastPulseCntrValue != parts[6] || lastPulseCntrChecked != parts[5]) {
element = {"ioType": parts[0], "address": parts[1], "value": parts[2], "record": parts[3], "pulseCntrVisible": parts[4], "pulseCntrChecked": parts[5], "pulseCntrValue": parts[6], "name": parts[7], "description": parts[8], "moduleName": parts[9]};
// signal value changed
if (lastValue != parts[2]) {
updatedSetting += 1;
}
// pulse counter value changed
if (lastPulseCntrValue != parts[6]) {
updatedSetting += 2;
}
// pulse counter checked (activated) changed
if (lastPulseCntrChecked != parts[5]) {
updatedSetting += 4;
}
}
}
// analog input
else if (parts[0] === "ai") {
let lastValue;
let lastAiConfig;
if (connObj.values[recvIoTypeAddr]) {
// last value available in connection object
lastValue = connObj.values[recvIoTypeAddr].value;
// last analog input configuration available in connection object
lastAiConfig = connObj.values[recvIoTypeAddr].config;
}
// only if value or analog input configuration has changed
if ((lastValue != parts[2]) || (lastAiConfig != parts[4])) {
element = {"ioType": parts[0], "address": parts[1], "value": parts[2], "record": parts[3], "config": parts[4], "name": parts[5], "description": parts[6], "moduleName": parts[7]};
// value changed
if (lastValue != parts[2]) {
updatedSetting += 1;
}
// analog input configuration changed
if (lastAiConfig != parts[4]) {
updatedSetting += 2;
}
}
}
// digital output, analog output
else {
element = {"ioType": parts[0], "address": parts[1], "value": parts[2], "record": parts[3], "name": parts[4], "description": parts[5], "manualMode": parts[6], "moduleName": parts[7]};
}
// only if element is created (new data available)
if (element) {
connObj.values[recvIoTypeAddr] = element;
// set additional property, which is required by frontend, to decide if value or pulse counter value has changed
element["updatedInput"] = updatedSetting;
if (parts[2] === "ERROR") {
element["error"] = "portCommError";
}
// only publish received livedata (no ws-setup promise (getPromise) available)
if (connObj.#noGetPromisePending(element.ioType)) {
connObj.publishIoPortStatus(element, RED, true, true);
}
}
}
}
// all "active" counters received
else if (strData === "counter®config®done") {
RED.comms.publish("publish/ewio2/config/done", "counter");
connObj.#resolveNextStoredWsPromise(RED);
}
// receive "active" counter
else if (strData.startsWith("counter®config")) {
const parts = strData.split("®");
// the msg must contain complete counter datasets
if (parts.length === 5) {
const counterElement = {"ioType": parts[0], "id": parts[2], "name": parts[3], "bus": parts[4]};
const recvCntrAddr = parts[0] + "®" + parts[2];
connObj.values[recvCntrAddr] = counterElement;
// only publish received livedata (no ws-setup promise (getPromise) available)
if (connObj.#noGetPromisePending(counterElement.ioType)) {
connObj.publishIoPortStatus(counterElement, RED, true, false);
}
}
// received status information for a counter
else if (parts.length === 4) {
const counterElement = {"ioType": parts[0], "address": parts[2], "status": parts[3]};
// only publish received livedata (no ws-setup promise (getPromise) available)
if (connObj.#noGetPromisePending(counterElement.ioType)) {
connObj.publishIoPortStatus(counterElement, RED, true, false);
}
}
// metering error received (no metering model)
else if (parts.length === 3) {
RED.comms.publish("publish/ewio2/error/" + connObj.configNodeId, {code: ("status." + parts[2])});
}
}
// all "active" datapoints received
else if (strData === "datapoints®config®done") {
RED.comms.publish("publish/ewio2/config/done", "datapoints");
connObj.#resolveNextStoredWsPromise(RED);
}
// receive "active" datapoints
else if (strData.startsWith("datapoints®config")) {
const parts = strData.split("®");
let datapointsElement = {};
// the msg must contain complete datapoints datasets (length 10: with additional status)
if (parts.length >= 9) {
var receivedStatus = "";
if (parts.length === 10) {
receivedStatus = parts[9];
}
datapointsElement = {"ioType": parts[0], "counterId": parts[2], "id": parts[3], "name": parts[4], "range": parts[5], "timestamp": parts[6], "flags": parts[7], "value": parts[8], "source": "requested", "status": receivedStatus};
const recvCntrDpAddr = parts[0] + "®" + parts[2] + "®" + parts[3];
connObj.values[recvCntrDpAddr] = datapointsElement;
// only publish received livedata (no ws-setup promise (getPromise) available)
if (connObj.#noGetPromisePending(datapointsElement.ioType + "®" + datapointsElement.counterId)) {
connObj.publishIoPortStatus(datapointsElement, RED, true, false);
}
}
// no counter data available
else if (parts.length === 6) {
const elem = {ioType: "", address: connObj.requestedIoPort, value: "ERROR", error: "noData"};
connObj.publishIoPortStatus(elem, RED, true, false);
}
}
// received counter livedata value
else if (strData.startsWith("datapoints®livedata®")) {
const parts = strData.split("®");
const recvCntrDpAddr = "datapoints®" + parts[2] + "®" + parts[3];
let storedRange = "";
let storedName = "";
// range and name are not updated via livedata!!!
if (connObj.values[recvCntrDpAddr]) {
storedRange = connObj.values[recvCntrDpAddr].range;
storedName = connObj.values[recvCntrDpAddr].name;
}
let datapointsElement = {};
if (parts.length === 7) {
datapointsElement = {"ioType": "datapoints", "counterId": parts[2], "id": parts[3], "name": storedName, "range": storedRange, "timestamp": parts[4], "flags": parts[5], "value": parts[6], source: "livedata"};
}
else if (parts.length === 8) {
datapointsElement = {"ioType": "datapoints", "counterId": parts[2], "id": parts[3], "name": storedName, "range": storedRange, "timestamp": parts[4], "flags": parts[5], "value": parts[6], "reason": parts[7], source: "livedata"};
}
if (datapointsElement) {
connObj.values[recvCntrDpAddr] = datapointsElement;
connObj.publishIoPortStatus(datapointsElement, RED, true, true);
}
}
// check authorization
else if (strData.startsWith("unauthorized")) {
connObj.ws.close();
// "notAuthorized" is the same name as in language files!!!
reject ("notAuthorized");
}
// check access rights (must be Operator or Administrator)
else if (strData == "insufficientAccessRights") {
connObj.#resolveNextStoredWsPromise(RED);
// "insufficientAccessRights" is the same name as in language files!!!
pubSub.publish("show-status", {"color": "red", "shape": "ring", "message": "@metz-connect/node-red-ewio2/ewio2:status." + strData, "configNodeId": connObj.configNodeId});
}
// check if bacnet server is running and digital OUT or analog OUT should be set (put®...®setValue®... command to EWIO2)
else if (strData == "bacnetServerActive") {
connObj.#resolveNextStoredWsPromise(RED);
// "bacnetServerActive" is the same name as in language files!!!
pubSub.publish("show-status", {"color": "red", "shape": "ring", "message": "@metz-connect/node-red-ewio2/ewio2:status." + strData, "configNodeId": connObj.configNodeId});
}
});
this.ws.on("close", function() {
log(node, 'websocket closed');
});
this.ws.on("error", function(error){
log(node, 'onError connectWebsocket(): ' + error);
//resolves.length = 0;
reject (error);
});
});
return websocketPromise;
}
/**
* Resolves all stored connection promises.
* This is done if connection is established or when establishing connection fails (error already published in another function)
* @memberof Ewio2ConnObj
* @param {Object} RED - Node-RED "infrastructure", used to publish events to frontend.
*/
#resolveAllStoredConnPromises(RED) {
const node = RED.nodes.getNode(this.configNodeId);
log(node, "resolveAllStoredConnPromises: nr of conn promises: " + this.connPromises.length);
var prom;
// resolve all pending promises
while (prom = this.connPromises.shift()) {
log(node, "resolveAllStoredConnPromises: resolved connPromise");
prom.resolve(this);
}
}
/**
* Resolves all stored get value and start livedata channel promises.
* @memberof Ewio2ConnObj
* @param {string} ioType - Which type of IO should be get or set (e.g. "do", "ao").
* @param {Object} RED - Node-RED "infrastructure", used to publish events to frontend.
*/
resolveAllStoredGetPromises(ioType, RED) {
const node = RED.nodes.getNode(this.configNodeId);
var prom;
log(node, "resolveAllStoredGetPromises: nr of get promises: " + this.getPromises[ioType].length);
// resolve all pending getPromises
while (prom = this.getPromises[ioType].shift()) {
log(node, "resolveAllStoredGetPromises: resolved getPromise!!!");
prom.resolve();
}
}
/**
* Resolves next stored websocket promise and cleares timeout, to deactivate timeout handing.
* @memberof Ewio2ConnObj
* @param {Object} RED - Node-RED "infrastructure", used to publish events to frontend.
* @param {string} setVal - Value which was setted to EWIO2 (only valid when setting value).
*/
#resolveNextStoredWsPromise(RED, setVal) {
const node = RED.nodes.getNode(this.configNodeId);
var prom = this.wsPromise;
// resolve next pending wsPromises
if (prom) {
// response received or error --> clear timeout
clearTimeout(this.timeoutId);
this.timeoutId = undefined;
log(node, "resolveNextStoredWsPromise: resolved wsPromise!!!");
// reset wsPromise, to be able to store next "enqueued" (with while loop) wsPromise
this.wsPromise = undefined;
prom.resolve(setVal);
}
}
/**
* Publishes the current status of IO port to frontend (RED.comms.publish) and to backend (pubSub.publish).
* Status can either be successfully connected, manual mode active or an error status.
* It is also possible to publish a value to a Node-RED node´s output ("send-value-...").
* @memberof Ewio2ConnObj
* @param {Object} element - IO port message which was received from EWIO2 or stored in ewio2Connections map.
* @param {Object} RED - Node-RED "infrastructure", used to publish events to frontend.
* @param {bool} backendPublish - Flag that is set, if events to backend should be published.
* @param {bool} sendInputPublish - Flag that is set, if input node (analog, digital) should send a msg to node output.
*/
publishIoPortStatus(element, RED, backendPublish, sendInputPublish) {
// add host, which is needed by frontend, to distinguish between values in dropdown
element["host"] = this.host;
let ioPort;
if (element.ioType === "datapoints") {
ioPort = {nodeId: this.configNodeId, host: this.host, dp: element.id, cntr: element.counterId};
}
else {
ioPort = {nodeId: this.configNodeId, host: this.host, addr: element.address};
}
if (element.value === "ERROR" && element.error) {
if (backendPublish) {
if (element.error === "portCommError") {
pubSub.publish("show-status-" + element.ioType, {"color": "red", "shape": "ring", "message": "@metz-connect/node-red-ewio2/ewio2:status." + element.error, "configNodeId": this.configNodeId, "addr": JSON.stringify(ioPort)});
}
else {
pubSub.publish("show-status", {"color": "red", "shape": "ring", "message": "@metz-connect/node-red-ewio2/ewio2:status." + element.error, "configNodeId": this.configNodeId});
}
}
// check for portCommError if publishing for selected port (which is not available, for example if extension module is missing)
if (element.error === "portCommError") {
if (this.requestedIoPort !== "undefined" && this.requestedIoPort && (JSON.parse(this.requestedIoPort).addr === element.address)) {
RED.comms.publish("publish/ewio2/error/" + this.configNodeId, {code: ("status." + element.error)});
}
}
// errors different to portCommError
else {
RED.comms.publish("publish/ewio2/error/" + this.configNodeId, {code: ("status." + element.error), message: element.errorMsg});
}
}
else {
// publish IO data
RED.comms.publish("publish/ewio2/config/" + element.ioType + "/" + this.configNodeId + "/" + this.requestedIoPort + "/" + this.user, element);
if (backendPublish) {
// check and publish manual mode
if (element.manualMode == "1") {
pubSub.publish("show-status-" + element.ioType, {"color": "yellow", "shape": "ring", "message": "@metz-connect/node-red-ewio2/ewio2:status.manualModeActive", "configNodeId": this.configNodeId, "addr": JSON.stringify(ioPort)});
}
else if (element.status && element.status !== "1") {
pubSub.publish("show-status-" + element.ioType, {"color": "yellow", "shape": "ring", "message": "@metz-connect/node-red-ewio2/ewio2:status.counterDeactivated", "configNodeId": this.configNodeId, "addr": JSON.stringify(ioPort)});
}
else {
pubSub.publish("show-status-" + element.ioType, {"color": "green", "shape": "dot", "message": "@metz-connect/node-red-ewio2/ewio2:status.connected", "configNodeId": this.configNodeId, "addr": JSON.stringify(ioPort)});
}
// flag to publish data to node output is set and not a websocket connection from config menu initiated
if (sendInputPublish && !this.generalLivedataConfigMenu) {
if (element.ioType === "di" || element.ioType === "ai") {
pubSub.publish("send-value-" + element.ioType, {configNodeId: this.configNodeId, addr: JSON.stringify(ioPort), value: element.value, pulseCntrValue: element.pulseCntrValue, pulseCntrChecked: element.pulseCntrChecked, updated: element.updatedInput});
}
else if (element.ioType === "datapoints") {
// add configNodeId, which is needed by backend, to show values for selected EWIO2 connection
element["configNodeId"] = this.configNodeId;
pubSub.publish("send-value-" + element.ioType, element);
}
}
}
}
}
/**
* Checks if values of ioType are already stored in connection object. This is an indication if values of ioType must be retrieved from EWIO2 or not.
* @memberof Ewio2ConnObj
* @param {string} ioType - Which type of IO should be checked for availability (e.g. "do", "ao").
* @return {bool} Flag if ioType values already known.
*/
isIoTypeValueKnown(ioType) {
var ioTypeKnown = false;
for (var ioKey in this.values) {
if (this.values.hasOwnProperty(ioKey) && ioKey.toString().startsWith(ioType + "®")) {
ioTypeKnown = true;
break;
}
}
return ioTypeKnown;
}
/**
* Deletes all values of given ioType or deletes all values of ioTypes "di", "ai", "do" and "ao".
* @memberof Ewio2ConnObj
* @param {string} ioType - Which type of IO should be deleted (e.g. "do", "ao"). If not present, all values of ioTypes "di", "ai", "do" and "ao" are deleted.
*/
deleteAllStoredValues(ioType) {
// check in descending order, to savely delete values
for (var ioNumber = Object.keys(this.values).length - 1; ioNumber >= 0; ioNumber--) {
let ioKey = Object.keys(this.values)[ioNumber];
// delete values of given ioType
if (ioType) {
if (this.values.hasOwnProperty(ioKey) && ioKey.toString().startsWith(ioType + "®")) {
delete this.values[ioKey];
}
}
// delete values of all ioTypes (digital IN, analog IN, digital OUT and analog OUT)
else {
if (this.values.hasOwnProperty(ioKey) && (ioKey.toString().startsWith("di®") || ioKey.toString().startsWith("ai®") || ioKey.toString().startsWith("do®") || ioKey.toString().startsWith("ao®"))) {
delete this.values[ioKey];
}
}
}
}
/**
* Checks if one or more getPromises of ioType are pending. Needed to decide if data must be published (livedata received) or not (values requested).
* @memberof Ewio2ConnObj
* @param {string} ioType - Which type of IO should be checked for existance of getPromise (e.g. "do", "ao").
* @return {bool} Flag if no getPromise is pending (true).
*/
#noGetPromisePending(ioType) {
let returnVal = false;
if (!this.getPromises[ioType] || this.getPromises[ioType].length === 0) {
returnVal = true;
}
return returnVal;
}
/**
* Closes websocket connection and deletes complete connection entry from ewio2Connections map.
* @memberof Ewio2ConnObj
* @param {Object} RED - Node-RED "infrastructure", used to publish events to frontend.
*/
closeAndDeleteWs(RED) {
const node = RED.nodes.getNode(this.configNodeId);
log(node, 'closeAndDeleteWs fct reached');
if (this.ws) {
this.ws.close();
}
this.ws = undefined;
log(node, 'closeAndDeleteWs fct done');
}
/**
* Deletes the given node Id from registerd nodes (for connection to EWIO2).
* If no other node is registerd to this EWIO2 websocket connection, the websocket is closed.
* @memberof Ewio2ConnObj
* @param {string} nodeId - Identification of the node, which should be de-registered.
* @param {Object} RED - Node-RED "infrastructure", used to publish events to frontend.
* @return {bool} Flag if websocket was closed and deleted.
*/
deregisterNodeFromConnObj(nodeId, RED) {
const node = RED.nodes.getNode(this.configNodeId);
let wsClosedAndDeleted = false;
log(node, 'deregisterNodeFromConnObj fct reached, deregister: ' + nodeId);
if (this.registeredNodes.size) {
log(node, 'registeredNodes: ' + new Array(...this.registeredNodes).join(' '));
// deregister node
const deletedNodeStatus = this.registeredNodes.delete(nodeId);
log(node, 'deleted nodeID: ' + nodeId + ' from registerdNodes: ' + deletedNodeStatus);
if (this.registeredNodes.size === 0) {
wsClosedAndDeleted = true;
this.closeAndDeleteWs(RED);
}
}
else {
log(node, 'NO registeredNodes (not connected to any EWIO2 via websocket?)');
}
log(node, 'deregisterNodeFromConnObj fct done');
return wsClosedAndDeleted;
}
}
module.exports = Ewio2Connection