UNPKG

node-red-contrib-opcua

Version:

A Node-RED node to communicate via OPC UA based on node-opcua library.

1,158 lines (1,068 loc) 119 kB
/** Copyright 2018 Valmet Automation Inc. 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. **/ const { promoteToMultiStateValueDiscrete } = require("node-opcua"); const { parseArgs } = require("util"); module.exports = function (RED) { "use strict"; var chalk = require("chalk"); // const osLocale = require("os-locale"); // const getUserLocale = require("get-user-locale"); var opcua = require('node-opcua'); const { NodeCrawler } = require("node-opcua-client-crawler"); // Legacy support // var nodeId = require("node-opcua-nodeid"); var opcuaBasics = require('./opcua-basics'); var crypto_utils = opcua.crypto_utils; // var UAProxyManager = require("node-opcua-client-proxy").UAProxyManager; // var coerceNodeId = require("node-opcua-nodeid").coerceNodeId; var fileTransfer = require("node-opcua-file-transfer"); var basicTypes = require("node-opcua-basic-types"); var async = require("async"); // var treeify = require('treeify'); // var Map = require('es6-map'); // es6-map 0.1.5 not needed anymore var path = require("path"); var fs = require("fs"); var os = require("os"); var cloneDeep = require('lodash.clonedeep'); var DataType = opcua.DataType; var AttributeIds = opcua.AttributeIds; // var read_service = require("node-opcua-service-read"); var TimestampsToReturn = opcua.TimestampsToReturn; // var subscription_service = require("node-opcua-service-subscription"); const { createClientCertificateManager } = require("./utils"); const { dumpCertificates } = require("./dump_certificates"); const {parse, stringify} = require('flatted'); function OpcUaClientNode(n) { RED.nodes.createNode(this, n); this.name = n.name; this.action = n.action; var originalAction = n.action; this.time = n.time; this.timeUnit = n.timeUnit; this.deadbandtype = n.deadbandtype; this.deadbandvalue = n.deadbandvalue; this.certificate = n.certificate; // n == NONE, l == Local file, e == Endpoint, u == Upload this.localfile = n.localfile; // Local certificate file this.localkeyfile = n.localkeyfile; // Local private key file this.folderName4PKI = n.folderName4PKI; // Storage folder for PKI and certificates this.useTransport = n.useTransport; this.maxChunkCount = n.maxChunkCount; this.maxMessageSize = n.maxMessageSize this.receiveBufferSize = n.receiveBufferSize; this.sendBufferSize = n.sendBufferSize; // this.upload = n.upload; // Upload // this.certificate_filename = n.certificate_filename; // this.certificate_data = n.certificate_data; var node = this; var opcuaEndpoint = RED.nodes.getNode(n.endpoint); var userIdentity = { type: opcua.UserTokenType.Anonymous }; // Initialize with Anonymous var connectionOption = {}; var cmdQueue = []; // queue msgs which can currently not be handled because session is not established, yet and currentStatus is 'connecting' var currentStatus = ''; // the status value set set by node.status(). Didn't find a way to read it back. var multipleItems = []; // Store & read multiple nodeIds var writeMultipleItems = []; // Store & write multiple nodeIds & values // connectionOption.securityPolicy = opcua.SecurityPolicy[opcuaEndpoint.securityPolicy] || opcua.SecurityPolicy.None; console.log(chalk.yellow("Node securityPolicy: ") + chalk.cyan(opcuaEndpoint.securityPolicy)); // var enumValue = opcua.fromURI(opcua.coerceSecurityPolicy(opcuaEndpoint.securityPolicy)) || opcua.SecurityPolicy.None; // connectionOption.securityPolicy = enumValue; // console.log(chalk.yellow("Enum securityPolicy: ") + chalk.cyan(connectionOption.securityPolicy)); connectionOption.securityPolicy = opcuaEndpoint.securityPolicy connectionOption.securityMode = opcua.MessageSecurityMode[opcuaEndpoint.securityMode] || opcua.MessageSecurityMode.None; var userCertificate = opcuaEndpoint.userCertificate; var userPrivatekey = opcuaEndpoint.userPrivatekey; if (node.folderName4PKI && node.folderName4PKI.length>0) { verbose_log("Node: " + node.name + " using own PKI folder:" + node.folderName4PKI); } connectionOption.clientCertificateManager = createClientCertificateManager(true, node.folderName4PKI); // AutoAccept certificates, TODO add to client node as parameter if really needed if (node.certificate === "l" && node.localfile) { verbose_log("Using 'own' local certificate file " + node.localfile); // User must define absolute path var certfile = node.localfile; var keyfile = node.localkeyfile; connectionOption.certificateFile = certfile; connectionOption.privateKeyFile = keyfile; if (!fs.existsSync(certfile)) { node_error("Local certificate file not found: " + certfile) } if (!fs.existsSync(keyfile)) { node_error("Local private key file not found: " + keyfile) } } // Moved needed options to client create connectionOption.requestedSessionTimeout = opcuaBasics.calc_milliseconds_by_time_and_unit(300, "s"); // DO NOT USE must be NodeOPCUA-Client !! connectionOption.applicationName = node.name; // Application name connectionOption.clientName = node.name; // This is used for the session names connectionOption.endpointMustExist = false; connectionOption.defaultSecureTokenLifetime = 40000 * 5; // From the node UI, keep min values! /* // Needed or not? if (node.maxChunkCount < 1) node.maxChunkCount = 1; if (node.maxMessageSize < 8192) node.maxMessageSize = 8192; if (node.receiveBufferSize < 8 * 1024) node.receiveBufferSize = 8 * 1024; if (node.sendBufferSize < 8 * 1024) node.sendBufferSize = 8 * 1024; */ var transportSettings = { maxChunkCount: node.maxChunkCount, // Default 1 maxMessageSize: node.maxMessageSize, // should be at least 8192 receiveBufferSize: node.receiveBufferSize, // 8 * 1024, sendBufferSize: node.sendBufferSize // 8 * 1024 }; if (node.useTransport) { verbose_log(chalk.yellow("Using, transport settings: ") + chalk.cyan(JSON.stringify(transportSettings))); connectionOption.transportSettings = transportSettings; } // connectionOption.transportSettings.maxChunkCount = transportSettings.maxChunkCount; // connectionOption.transportSettings.maxMessageSize = transportSettings.maxMessageSize; // connectionOption.transportSettings.receiveBufferSize = transportSettings.receiveBufferSize; // connectionOption.transportSettings.sendBufferSize = transportSettings.sendBufferSize; connectionOption.connectionStrategy = { maxRetry: 10512000, // Limited to max 10 ~5min // 10512000, // 10 years should be enough. No infinite parameter for backoff. initialDelay: 5000, // 5s maxDelay: 30 * 1000 // 30s }; // connectionOption.keepSessionAlive = true; // Not to be used anymore? NOTE: commented out: issue #599 // verbose_log("Connection options:" + JSON.stringify(connectionOption)); // verbose_log("EndPoint: " + JSON.stringify(opcuaEndpoint)); // Ensure Anonymous login if (connectionOption.securityMode === opcua.MessageSecurityMode.None || opcuaEndpoint.none === true) { userIdentity = { type: opcua.UserTokenType.Anonymous }; } if (opcuaEndpoint.login === true && opcuaEndpoint.usercert === true) { userIdentity = { type: opcua.UserTokenType.Anonymous }; } if (opcuaEndpoint.login === true && opcuaEndpoint.usercert === true) { node.error("Cannot use username & password & user certificate at the same time!"); } // Username & password with securityMode None is allowed if (opcuaEndpoint.login === true) { // } && connectionOption.securityMode != opcua.MessageSecurityMode.None) { userIdentity = { type: opcua.UserTokenType.UserName, userName: opcuaEndpoint.credentials.user.toString(), password: opcuaEndpoint.credentials.password.toString() }; verbose_log(chalk.green("Using UserName & password: ") + chalk.cyan(JSON.stringify(userIdentity))); // verbose_log(chalk.green("Connection options: ") + chalk.cyan(JSON.stringify(connectionOption))); // .substring(0,75) + "..."); } else if (opcuaEndpoint.usercert === true) { if (!fs.existsSync(userCertificate)) { node.error("User certificate file not found: " + userCertificate); } const certificateData = crypto_utils.readCertificate(userCertificate); if (!fs.existsSync(userPrivatekey)) { node.error("User private key file not found: " + userPrivatekey); } const privateKey = crypto_utils.readPrivateKeyPEM(userPrivatekey); userIdentity = { certificateData, privateKey, type: opcua.UserTokenType.Certificate // User certificate }; // console.log("CASE User certificate UserIdentity: " + JSON.stringify(userIdentity)); // connectionOption = {}; // connectionOption.endpointMustExist = false; } else { userIdentity = { type: opcua.UserTokenType.Anonymous }; // console.log("CASE Anonymous UserIdentity: " + JSON.stringify(userIdentity)); // console.log(" connection options: " + JSON.stringify(connectionOption).substring(0,75) + "..."); } verbose_log(chalk.green("UserIdentity: ") + chalk.cyan(JSON.stringify(userIdentity))); var items = []; var subscription; // only one subscription needed to hold multiple monitored Items var monitoredItems = new Map(); function node_error(err) { //console.error(chalk.red("Client node error on: " + node.name + " error: " + stringify(err))); node.error(chalk.red("Client node error on: " + node.name + " error: " + stringify(err))); } function verbose_warn(logMessage) { //if (RED.settings.verbose) { // console.warn(chalk.yellow((node.name) ? node.name + ': ' + logMessage : 'OpcUaClientNode: ' + logMessage)); node.warn((node.name) ? node.name + ': ' + logMessage : 'OpcUaClientNode: ' + logMessage); //} } function verbose_log(logMessage) { //if (RED.settings.verbose) { // console.log(chalk.cyan(logMessage)); // node.log(logMessage); // settings.js log level info node.debug(logMessage); //} } async function getBrowseName(_session, nodeId) { const dataValue = await _session.read({ attributeId: AttributeIds.BrowseName, nodeId }); if (dataValue.statusCode.isGood()) { const browseName = dataValue.value.value.name; return browseName; } else { return "???"; } } // Fields selected alarm fields // EventFields same order returned from server array of variants (filled or empty) async function __dumpEvent(node, session, fields, eventFields, _callback) { var msg = {}; msg.payload = {}; verbose_log(chalk.yellow("Event Fields: ") + chalk.cyan(JSON.stringify(eventFields))); set_node_status_to("active event"); for (var i = 0; i < eventFields.length; i++) { var variant = eventFields[i]; var fieldName = fields[i]; verbose_log(chalk.yellow("Event Field: ") + chalk.cyan(fieldName) + " " + chalk.cyan(stringify(variant))); // Check if variant is NodeId and then get qualified name (browseName) if (variant && variant.dataType && variant.dataType === DataType.NodeId) { fieldName = await getBrowseName(session, variant.value); } if (!variant || variant.dataType === DataType.Null || !variant.value) { verbose_log(chalk.red("No variant or variant dataType is Null or no variant value! Variant: ") + chalk.cyan(JSON.stringify(variant))); } else { if (fieldName === "EventId" && variant && variant.value) { msg.payload[fieldName] = "0x" + variant.value.toString("hex"); // As in UaExpert msg.payload["_" + fieldName] = variant; // Keep as ByteString } else { // Added handling for LocalizedText to use OS locale / node-red __language__ if (fieldName === "Message") { const locale = Intl.DateTimeFormat().resolvedOptions().locale; console.log(chalk.yellow("OS Locale: ") + chalk.cyan(locale)); console.log(chalk.yellow("Locale: ") + chalk.cyan(variant.value.locale)); console.log(chalk.yellow("Message: ") + chalk.cyan(variant.value.text)); // Change according locale if available if (variant.value.length > 1) { var i = 0; while (i < variant.value.length) { var localText = variant.value[i]; // Change according locale if (localText.locale === locale) { variant.value = localText; break; } i++; } } } msg.payload[fieldName] = opcuaBasics.clone_object(variant.value); } // if available, needed for Acknowledge function in client if (fieldName === "ConditionId" && variant && variant.value) { msg.topic = variant.value.toString(); } } } // Set message topic if (eventFields.length === 0) { msg.topic="No EventFields"; } // if available, needed for Acknowledge function in client else if (msg.payload.ConditionId) { msg.topic=msg.payload.ConditionId.toString(); } else if (msg.payload.EventId) { msg.topic=msg.payload.EventId.toString(); // Set then this can be used to Acknowledge event } else { if (msg.payload.EventType) { msg.topic=msg.payload.EventType.toString(); } } verbose_log(chalk.yellow("Event message topic: ") + chalk.cyan(msg.topic)); node.send(msg); _callback(); } var eventQueue = new async.queue(function (task, callback) { __dumpEvent(task.node, task.session, task.fields, task.eventFields, callback); }); var eventQueue = new async.queue(function (task, callback) { __dumpEvent(task.node, task.session, task.fields, task.eventFields, callback); }); function dumpEvent(node, session, fields, eventFields, _callback) { eventQueue.push({ node: node, session: session, fields: fields, eventFields: eventFields, _callback: _callback }); } // Listener functions that can be removed on reconnect const reestablish = function () { // verbose_warn(" !!!!!!!!!!!!!!!!!!!!!!!! CONNECTION RE-ESTABLISHED !!!!!!!!!!!!!!!!!!! Node: " + node.name); set_node_status2_to("connected", "re-established"); }; const backoff = function (attempt, delay) { // verbose_warn("backoff attempt #" + attempt + " retrying in " + delay / 1000.0 + " seconds. Node: " + node.name + " " + opcuaEndpoint.endpoint); var msg = {}; msg.error = {}; msg.error.message = "reconnect"; msg.error.source = this; node.error("reconnect", msg); set_node_status2_to("reconnect", "attempt #" + attempt + " retry in " + delay / 1000.0 + " sec"); }; const reconnection = function () { // verbose_warn(" !!!!!!!!!!!!!!!!!!!!!!!! Starting Reconnection !!!!!!!!!!!!!!!!!!! Node: " + node.name); set_node_status2_to("reconnect", "starting..."); }; function create_opcua_client(callback) { verbose_warn("Creating OPCUA CLIENT ") node.client = null; // verbose_log("Create Client: " + stringify(connectionOption).substring(0,75) + "..."); try { // Use empty 0.0.0.0 address as "no client" initial value if (opcuaEndpoint.endpoint.indexOf("opc.tcp://0.0.0.0") == 0) { verbose_warn(`close opcua client ${node.client} userIdentity ${userIdentity}`); if(node.client){ close_opcua_client("connection error: no session", 0); } items = []; node.items = items; set_node_status_to("no client"); if (callback) { callback(); } return; } // Normal client // verbose_log(chalk.green("1) CREATE CLIENT: ") + chalk.cyan(stringify(connectionOption))); // .substring(0,75) + "...")); let options = { securityMode: connectionOption.securityMode, securityPolicy: connectionOption.securityPolicy, defaultSecureTokenLifetime: connectionOption.defaultSecureTokenLifetime, endpointMustExist: connectionOption.endpointMustExist, connectionStrategy: connectionOption.connectionStrategy, // keepSessionAlive: true, // TODO later make it possible to disable NOTE: commented out: issue #599 requestedSessionTimeout: 60000 * 5, // 5min, default 1min // transportSettings: transportSettings // Some }; if (node.useTransport === true) { options.transportSettings = transportSettings; } verbose_log(chalk.green("1) CREATE CLIENT: ") + chalk.cyan(stringify(options))); // node.client = opcua.OPCUAClient.create(connectionOption); // Something extra? node.client = opcua.OPCUAClient.create(options); node.client.on("connection_reestablished", reestablish); node.client.on("backoff", backoff); node.client.on("start_reconnection", reconnection); } catch(err) { node_error("Cannot create client, check connection options: " + stringify(options)); // connectionOption } items = []; node.items = items; set_node_status_to("create client"); if (callback) { callback(); } } function reset_opcua_client(callback) { if (node.client) { node.client.disconnect(function () { verbose_log("Client disconnected!"); create_opcua_client(callback); }); } } function close_opcua_client(message, error) { verbose_warn(`closing opcua client ${opcua.client == null} userIdentity ${JSON.stringify(userIdentity)}`) if (node.client) { node.client.removeListener("connection_reestablished", reestablish); node.client.removeListener("backoff", backoff); node.client.removeListener("start_reconnection", reconnection); try { if(!node.client.isReconnecting){ node.client.disconnect(function () { node.client = null; verbose_log("Client disconnected!"); if (error === 0) { set_node_status_to("closed"); } else { set_node_errorstatus_to(message, error) node.error("Client disconnected & closed: " + message + " error: " + error.toString()); } }); } else { node.client = null; set_node_status_to("closed"); } } catch (err) { node_error("Error on disconnect: " + stringify(err)); } } } function set_node_status_to(statusValue) { verbose_log(chalk.yellow("Client status: ") + chalk.cyan(statusValue)); var statusParameter = opcuaBasics.get_node_status(statusValue); currentStatus = statusValue; node.status({ fill: statusParameter.fill, shape: statusParameter.shape, text: statusParameter.status }); } function set_node_status2_to(statusValue, message) { verbose_log(chalk.yellow("Client status: ") + chalk.cyan(statusValue)); var statusParameter = opcuaBasics.get_node_status(statusValue); currentStatus = statusValue; node.status({ fill: statusParameter.fill, shape: statusParameter.shape, text: statusParameter.status + " " + message }); } function set_node_errorstatus_to(statusValue, error) { verbose_log("Client status: " + statusValue); var statusParameter = opcuaBasics.get_node_status(statusValue); currentStatus = statusValue; if (!error) { error = ""; } node.status({ fill: statusParameter.fill, shape: statusParameter.shape, text: statusParameter.status + " " + error }); } async function connect_opcua_client() { verbose_warn(`connect_opcua_client ${node.client ==null} userIdentity ${JSON.stringify(userIdentity)}`); if (opcuaEndpoint.login === true) { verbose_log(chalk.green("Using UserName & password: ") + chalk.cyan(JSON.stringify(userIdentity))); if(opcuaEndpoint.credentials && opcuaEndpoint['user'] && opcuaEndpoint['password']){ userIdentity = { type: opcua.UserTokenType.UserName, userName: opcuaEndpoint.credentials.user.toString(), password: opcuaEndpoint.credentials.password.toString() }; } else if(opcuaEndpoint['user'] && opcuaEndpoint['password']) { userIdentity = { type: opcua.UserTokenType.UserName, userName: opcuaEndpoint.user.toString(), password: opcuaEndpoint.password.toString() };} else { node_error("Please enter user or password in credentiasl or same level as login") } // verbose_log(chalk.green("Connection options: ") + chalk.cyan(JSON.stringify(connectionOption))); // .substring(0,75) + "..."); } else if (opcuaEndpoint.usercert === true) { if (!fs.existsSync(userCertificate)) { node.error("User certificate file not found: " + userCertificate); } const certificateData = crypto_utils.readCertificate(userCertificate); if (!fs.existsSync(userPrivatekey)) { node.error("User private key file not found: " + userPrivatekey); } const privateKey = crypto_utils.readPrivateKeyPEM(userPrivatekey); userIdentity = { certificateData, privateKey, type: opcua.UserTokenType.Certificate // User certificate }; // console.log("CASE User certificate UserIdentity: " + JSON.stringify(userIdentity)); // connectionOption = {}; // connectionOption.endpointMustExist = false; } else { verbose_warn("userIdentity is ANONYMOUS ") userIdentity = { type: opcua.UserTokenType.Anonymous }; } // Refactored from old async Javascript to new Typescript with await var session; // STEP 1 // First connect to server´s endpoint if (opcuaEndpoint && opcuaEndpoint.endpoint) { verbose_log(chalk.yellow("Connecting to endpoint: ") + chalk.cyan(opcuaEndpoint.endpoint)); } else { node_error("No client endpoint listed! Waiting..."); return; } if (opcuaEndpoint.endpoint.indexOf("opc.tcp://0.0.0.0") === 0) { // if(node.client){ // verbose_warn(`close opcua client in connect ${node.client}`); // close_opcua_client("connection error: no session", 0); // } set_node_status_to("no client") } else { set_node_status_to("connecting"); } if (!node.client) { verbose_log("No client to connect..."); return; } verbose_log(chalk.yellow("Exact endpointUrl: ") + chalk.cyan(opcuaEndpoint.endpoint) + chalk.yellow(" hostname: ") + chalk.cyan(os.hostname())); try { console.log(chalk.yellowBright("Certificate manager initialization for node: ") + chalk.cyan(n.name)); await node.client.clientCertificateManager.initialize(); console.log(chalk.green("Certificate manager initialized for node: ") + chalk.cyan(n.name)); } catch (error1) { console.log(chalk.red("Certificate manager error: ") + chalk.cyan(error1.message)); set_node_status_to("invalid certificate"); var msg = {}; msg.error = {}; msg.error.message = "Certificate error: " + error1.message; msg.error.source = this; node.error("Certificate error", msg); } try { // verbose_log(chalk.green("Client node parameters: ") + chalk.cyan(JSON.stringify(opcuaEndpoint))); verbose_log(chalk.green("2) Connecting using endpoint: ") + chalk.cyan(opcuaEndpoint.endpoint) + chalk.green(" securityMode: ") + chalk.cyan(connectionOption.securityMode) + chalk.green(" securityPolicy: ") + chalk.cyan(connectionOption.securityPolicy)); console.log(chalk.yellowBright("Client connecting, node: ") + chalk.cyan(n.name)); await node.client.connect(opcuaEndpoint.endpoint); console.log(chalk.green("Client connected, node: ") + chalk.cyan(n.name)); } catch (err) { console.log(chalk.red("Client connect error: ") + chalk.cyan(err.message)); verbose_warn("Case A: Endpoint does not contain, 1==None 2==Sign 3==Sign&Encrypt, using securityMode: " + stringify(connectionOption.securityMode)); verbose_warn(" using securityPolicy: " + stringify(connectionOption.securityPolicy)); verbose_warn("Case B: UserName & password does not match to server (needed by Sign or SignAndEncrypt), check username: " + userIdentity.userName + " and password: " + userIdentity.password); verbose_warn("Case C: With Sign you cannot use SecurityPolicy None!!"); verbose_warn("Invalid endpoint parameters: ", err); node_error("Wrong endpoint parameters: " + JSON.stringify(opcuaEndpoint)); set_node_status_to("Invalid endpoint, check that server has security policy: " + stringify(connectionOption.securityPolicy)); var msg = {}; msg.error = {}; msg.error.message = "Invalid endpoint: " + err; msg.error.source = this; node.error("Invalid endpoint", msg); return; } verbose_log(chalk.green("Connected to endpoint: ") + chalk.cyan(opcuaEndpoint.endpoint)); // verbose_log("Endpoint parameters: " + JSON.stringify(opcuaEndpoint)); // verbose_log("Connection options: " + stringify(connectionOption)); // STEP 2 // This will succeed first time only if security policy and mode are None // Later user can use path and local file to access server certificate file if (!node.client) { node_error("Client not yet created & connected, cannot getEndpoints!"); return; } // dumpCertificates(node.client); // TODO Wrong folder or something to solve // STEP 3 // verbose_log("Create session..."); try { verbose_warn(`Create session with userIdentity node.client ${node.client ==null} userIdentity ${JSON.stringify(userIdentity)}`) verbose_log(chalk.green("3) Create session with userIdentity at: ") + chalk.cyan(JSON.stringify(userIdentity))); // {"clientName": "Node-red OPC UA Client node " + node.name}, // sessionName = "Node-red OPC UA Client node " + node.name; if (!node.client) { node_error("Client not yet created, cannot create session"); close_opcua_client("connection error: no client", 0); return; } console.log(chalk.yellowBright("Create session for node: ") + chalk.cyan(n.name)); session = await node.client.createSession(userIdentity); console.log(chalk.green("Session created for node: ") + chalk.cyan(n.name)); if (!session) { node_error("Create session failed!"); verbose_warn(`Create session failed!`) close_opcua_client("connection error: no session", 0); return; } node.session = session; // verbose_log("session active"); set_node_status_to("session active"); for (var i in cmdQueue) { processInputMsg(cmdQueue[i]); } cmdQueue = []; } catch (err) { console.log(chalk.red("Error on create session for node: ") + chalk.cyan(n.name)); node_error(node.name + " OPC UA connection error: " + err.message); verbose_log(err); node.session = null; close_opcua_client("connection error", err); } } function make_subscription(callback, msg, parameters) { var newSubscription = null; if (!node.session) { verbose_log("Subscription without session"); return newSubscription; } if (!parameters) { verbose_log("Subscription without parameters"); return newSubscription; } verbose_log("Publishing interval " + stringify(parameters)); newSubscription = opcua.ClientSubscription.create(node.session, parameters); verbose_log("Subscription " + newSubscription.toString()); newSubscription.on("initialized", function () { verbose_log("Subscription initialized"); set_node_status_to("initialized"); }); newSubscription.on("started", function () { verbose_log("Subscription subscribed ID: " + newSubscription.subscriptionId); set_node_status_to("subscribed"); // monitoredItems = new Map(); monitoredItems.clear(); callback(newSubscription, msg); }); newSubscription.on("keepalive", function () { verbose_log("Subscription keepalive ID: " + newSubscription.subscriptionId); set_node_status_to("keepalive"); }); newSubscription.on("terminated", function () { verbose_log("Subscription terminated ID: " + newSubscription.subscriptionId); set_node_status_to("terminated"); subscription = null; // monitoredItems = new Map(); monitoredItems.clear(); }); return newSubscription; } if (!node.client) { create_opcua_client(connect_opcua_client); } function processInputMsg(msg) { if (msg.action === "reconnect") { cmdQueue = []; // msg.endpoint can be used to change endpoint msg.action = ""; // var msg = {}; // msg.error = {}; // msg.error.message = "reconnect"; // msg.error.source = this; // node.error("reconnect", msg); reconnect(msg); return; } if (msg.action === "connect") { cmdQueue = []; // msg.endpoint can be used to change endpoint msg.action = ""; connect_action_input(msg); return; } if (msg.action && msg.action.length > 0) { verbose_log("Override node action by msg.action: " + msg.action); node.action = msg.action; } else { verbose_log(chalk.green("Using node action: ") + chalk.cyan(originalAction)); node.action = originalAction; // Use original action from the node } // With new node-red easier to set action into payload if (msg.payload && msg.payload.action && msg.payload.action.length > 0) { verbose_log("Override node action by msg.payload.action:" + msg.payload.action); node.action = msg.payload.action; } if (!node.action) { verbose_warn("Can't work without action (read, write, browse ...)"); //node.send(msg); // do not send in case of error return; } if (!node.client || !node.session) { verbose_log("Not connected, current status: " + currentStatus); // Added statuses when msg must be put to queue // Added statuses when msg must be put to queue const statuses = ['', 'create client', 'connecting', 'reconnect']; if (statuses.includes(currentStatus)) { cmdQueue.push(msg); } else { verbose_warn(`can't work without OPC UA client ${node.client} client ${node.session}`); reset_opcua_client(connect_opcua_client); } //node.send(msg); // do not send in case of error return; } // node.warn("secureChannelId:" + node.session.secureChannelId); if (!node.session.sessionId == "terminated") { verbose_warn("terminated OPC UA Session"); reset_opcua_client(connect_opcua_client); // node.send(msg); // do not send in case of error return; } if (msg.action && (msg.action === "connect" || msg.action === "disconnect")) { // OK msg.action = ""; } else { if (!msg.topic) { verbose_warn("can't work without OPC UA NodeId - msg.topic empty"); // node.send(msg); // do not send in case of error return; } } verbose_log(chalk.yellow("Action on input: ") + chalk.cyan(node.action) + chalk.yellow(" Item from Topic: ") + chalk.cyan(msg.topic) + chalk.yellow(" session Id: ") + chalk.cyan(node.session.sessionId)); switch (node.action) { case "connect": connect_action_input(msg); break; case "disconnect": disconnect_action_input(msg); break; case "reconnect": reconnect(msg); break; case "register": register_action_input(msg); break; case "unregister": unregister_action_input(msg); break; case "read": read_action_input(msg); break; case "history": readhistory_action_input(msg); break; case "info": info_action_input(msg); break; case "build": build_extension_object_action_input(msg); break; case "write": write_action_input(msg); break; case "subscribe": subscribe_action_input(msg); break; case "monitor": monitor_action_input(msg); break; case "unsubscribe": unsubscribe_action_input(msg); break; case "deletesubscribtion": // miss-spelled, this allows old flows to work case "deletesubscription": delete_subscription_action_input(msg); break; case "browse": browse_action_input(msg); break; case "events": subscribe_events_input(msg); break; case "readmultiple": readmultiple_action_input(msg); break; case "writemultiple": writemultiple_action_input(msg) break; case "acknowledge": acknowledge_input(msg); break; case "readfile": read_file(msg); break; case "writefile": write_file(msg); break; case "method": method_action_input(msg); break; default: verbose_warn("Unknown action: " + node.action + " with msg " + stringify(msg)); break; } //node.send(msg); // msg.payload is here actual inject caused wrong values } node.on("input", processInputMsg); async function acknowledge_input(msg) { // msg.topic is nodeId of the alarm object like Prosys ns=6;s=MyLevel.Alarm // msg.conditionId is actual conditionObejct that contains ns=6;s=MyLevel.Alarm/0:EventId current/latest eventId will be read // msg.comment will be used as comment in the acknowledge var eventId; if (msg.conditionId) { const dataValue = await node.session.read({ nodeId: msg.conditionId, attributeId: AttributeIds.Value }); eventId = dataValue.value.value; verbose_log(chalk.yellow("Acknowledge (alarm object == topic): ") + chalk.cyan(msg.topic) + chalk.yellow(" conditionObject (nodeId of eventId): ") + chalk.cyan(msg.conditionId) + chalk.yellow(" value of eventId: 0x") + chalk.cyan(eventId.toString("hex")) + chalk.yellow(" comment: ") + chalk.cyan(msg.comment)); } // If actual eventId provided use it if (msg.eventId) { eventId = msg.eventId; } if (eventId) { try { const ackedState = await node.session.read({ nodeId: msg.topic + "/0:AckedState/0:Id", attributeId: AttributeIds.Value }); node.debug(chalk.yellow("EVENT ACKED STATE: ") + chalk.cyan(ackedState)); if (ackedState && ackedState.statusCode === opcua.StatusCodes.Good && ackedState.value.value === true) { node.status({ fill: "yellow", shape: "dot", text: "Event: " + msg.topic + " already acknowledged" }); } else { const status = await node.session.acknowledgeCondition(msg.topic, eventId, msg.comment); if (status !== opcua.StatusCodes.Good) { node_error(node.name + "Error at acknowledge, status: " + status.toString()); set_node_errorstatus_to("error", status.toString()); } else { node.status({ fill: "green", shape: "dot", text: "Event: " + msg.topic + " acknowledged" }); } } } catch(err) { node_error(node.name + "Error at acknowledge: " + msg.topic + " eventId: " + eventId + " error: " + err); set_node_errorstatus_to("error", err); } } else { node_error(node.name + " error at acknowledge, no eventId, possible wrong msg.conditionId " + msg.conditionId); } } async function read_file(msg) { verbose_log("Read file, nodeId: " + msg.topic.toString()); var file_node = opcua.coerceNodeId(msg.topic); if (node.session) { try { const clientFile = new fileTransfer.ClientFile(node.session, file_node); fileTransfer.ClientFile.useGlobalMethod = true; // Given that the file is opened in ReadMode Only await clientFile.open(fileTransfer.OpenFileMode.Read); // Read file size // const dataValue = await node.session.read({nodeId: file_node.toString() + "-Size"}); // node-opcua specific way to name nodeId /* let dataValue; const browsePath = opcua.makeBrowsePath(file_node, ".Size"); const results = await node.session.translateBrowsePath(browsePath); if (results && results.statusCode === opcua.StatusCodes.Good && results.targets && results.targets[0].targetId) { var sizeNodeId = results.targets[0].targetId; dataValue = await node.session.read({nodeId: sizeNodeId}); } else { verbose_warn("Cannot translate browse path for file node: size"); } if (dataValue && dataValue.statusCode === opcua.StatusCodes.Good) { // Size is UInt64 // const size = dataValue.value.value[1] + dataValue.value.value[0] * 0x100000000; */ try { const size = await clientFile.size(); // This should read size from the file itself let buf = await clientFile.read(size); // node-opcua-file-transfer takes care of the whole file reading from v2.94.0 await clientFile.close(); msg.payload = buf; // Debug purpose, show content verbose_log("File content: " + buf.toString()); } catch(err) { msg.payload = ""; node_error(node.name + " failed to read file, nodeId: " + msg.topic + " error: " + err); set_node_errorstatus_to("error", "Cannot read file!"); } /* } else { // File size not available msg.payload = ""; node_error(node.name + " failed get file size, nodeId: " + msg.topic + " error: " + err); set_node_errorstatus_to("error", "Cannot read file size"); } */ node.send(msg); } catch(err) { node_error(node.name + " failed to read fileTransfer, nodeId: " + msg.topic + " error: " + err); set_node_errorstatus_to("error", err.toString()); } } else { verbose_warn("No open session to read file!"); } } async function write_file(msg) { verbose_log("Write file, nodeId: " + msg.topic.toString()); var file_node = opcua.coerceNodeId(msg.topic); if (node.session) { try { let buf; if (msg.payload && msg.payload.length > 0) { buf = msg.payload; } if (msg.fileName) { verbose_log("Uploading file: " + msg.fileName); buf = fs.readFileSync(msg.fileName); } const clientFile = new fileTransfer.ClientFile(node.session, file_node); clientFile.useGlobalMethod = true; // Given that the file is opened in WriteMode await clientFile.open(fileTransfer.OpenFileMode.Write); verbose_log("Local file content: " + buf.toString()); verbose_log("Writing file to server..."); await clientFile.write(buf); await clientFile.close(); verbose_log("Write done!"); } catch(err) { node.error(chalk.red("Cannot write file, error: " + err.message)); } } else { verbose_warn("No open session to write file!"); } } async function method_action_input(msg) { verbose_log("Calling method: " + JSON.stringify(msg)); if (node.session) { try { var status = await callMethod(msg); if (status === opcua.StatusCodes.Good) { node.status({ fill: "green", shape: "dot", text: "Method executed" }); } } catch(err) { node.error(chalk.red("Cannot call method, error: " + err.message)); } } else { verbose_warn("No open session to call method!"); } } async function callMethod(msg) { if (msg.methodId && msg.inputArguments) { verbose_log("Calling method: " + JSON.stringify(msg.methodId)); verbose_log("InputArguments: " + JSON.stringify(msg.inputArguments)); verbose_log("OutputArguments: " + JSON.stringify(msg.outputArguments)); try { var i = 0; var arg; while (i < msg.inputArguments.length) { arg = msg.inputArguments[i]; if (arg.dataType === "NodeId") { arg.value = opcua.coerceNodeId(arg.value); } if (arg.dataType === "ExtensionObject") { var extensionobject = null; if (arg.typeid) { extensionObject = await node.session.constructExtensionObject(opcua.coerceNodeId(arg.typeid), {}); // TODO make while loop to enable await } verbose_log("ExtensionObject=" + stringify(extensionobject)); Object.assign(extensionobject, arg.value); arg.value = new opcua.Variant({ dataType: opcua.DataType.ExtensionObject, value: extensionobject }); } i++; } } catch (err) { var msg = {}; msg.error = {}; msg.error.message = "Invalid NodeId: " + err; msg.error.source = this; node.error("Invalid NodeId: ", msg); return opcua.StatusCodes.BadNodeIdUnknown; } verbose_log("Updated InputArguments: " + JSON.stringify(msg.inputArguments)); var callMethodRequest; var diagInfo; try { callMethodRequest = new opcua.CallMethodRequest({ objectId: opcua.coerceNodeId(msg.objectId), methodId: opcua.coerceNodeId(msg.methodId), inputArgumentDiagnosticInfos: diagInfo, inputArguments: msg.inputArguments, outputArguments: msg.outputArguments }); } catch (err) { set_node_status_to("error: " + err.message) node.error("Build method request failed, error: " + err.message); } verbose_log("Call request: " + callMethodRequest.toString()); verbose_log("Calling: " + callMethodRequest); try { const result = await node.session.call(callMethodRequest); if (diagInfo) { verbose_log("Diagn. info: " + JSON.stringify(diagInfo)); } verbose_log("Output args: " + JSON.stringify(msg.outputArguments)); verbose_log("Results: " + JSON.stringify(result)); msg.result = result; if (result && result.statusCode === opcua.StatusCodes.Good) { var i = 0; msg.output = result.outputArguments; // Original outputArguments msg.payload = []; // Store values back to array if (result.outputArguments.length == 1) { verbose_log("Value: " + result.outputArguments[i].value); msg.payload = result.outputArguments[0].value; // Return only if one output argument } else { while (result.outputArguments.length > i) { verbose_log("Value[" + i + "]:" + result.outputArguments[i].toString()); msg.payload.push(result.outputArguments[i].value); // Just copy result value to payload[] array, actual value needed mostly i++; } } } else { set_node_status_to("error: " + result.statusCode.description) node.error("Execute method result, error:" + result.statusCode.description); return result.statusCode; } node.send(msg); return opcua.StatusCodes.Good; } catch (err) { set_node_status_to("Method execution error: " + err.message) node.error("Method execution error: " + err.message); return opcua.StatusCodes.BadMethodInvalid; } } } async function connect_action_input(msg) { verbose_log("Connecting..."); if (msg && msg.OpcUaEndpoint) { // Remove listeners if existing if (node.client) { verbose_log("Cleanup old listener events... before connecting to new client"); verbose_log("All event names:" + node.client.eventNames()); verbose_log("Connection_reestablished event count:" + node.client.listenerCount("connection_reestablished")); node.client.removeListener("connection_reestablished", reestablish); verbose_log("Backoff event count:" + node.client.listenerCount("backoff")); node.client.removeListener("backoff", backoff); verbose_log("Start reconnection event count:" + node.client.listenerCount("start_reconnection")); node.client.removeListener("start_reconnection", reconnection); } opcuaEndpoint = {}; // Clear opcuaEndpoint = msg.OpcUaEndpoint; // Check all parameters! connectionOption.securityPolicy = opcua.SecurityPolicy[opcuaEndpoint.securityPolicy]; // || opcua.SecurityPolicy.None; connectionOption.securityMode = opcua.MessageSecurityMode[opcuaEndpoint.securityMode]; // || opcua.MessageSecurityMode.None; verbose_log("NEW connectionOption security parameters, policy: " + connectionOption.securityPolicy + " mode: " + connectionOption.securityMode); if (opcuaEndpoint.login === true) { userIdentity = { userName: opcuaEndpoint.user, password: opcuaEndpoint.password, type: opcua.UserTokenType.UserName }; verbose_log("NEW UserIdentity: " + JSON.stringify(userIdentity)); } verbose_log("Using new endpoint:" + stringify(opcuaEndpoint)); } else { verbose_log("Using endpoint:" + stringify(opcuaEndpoint)); } if (!node.client) { create_opcua_client(connect_opcua_client); } } function disconnect_action_input(msg) { verbose_log("Closing session..."); if (node.session) { node.session.close(function(err) { if (err) { node_error("Session close error: " + err); } else {