@t0a5tbr0t/node-red-contrib-opcua 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/LICENSE +20 -0
  3. package/Readme.md +239 -0
  4. package/images/PROSYS-OPC-UA-EXAMPLE2.png +0 -0
  5. package/images/nodeopcua64.png +0 -0
  6. package/images/opcuanode.png +0 -0
  7. package/images/opcuanodeLogo64.png +0 -0
  8. package/opcua/101-opcuaitem.html +106 -0
  9. package/opcua/101-opcuaitem.js +82 -0
  10. package/opcua/102-opcuaclient.html +274 -0
  11. package/opcua/102-opcuaclient.js +2741 -0
  12. package/opcua/103-opcuabrowser.html +71 -0
  13. package/opcua/103-opcuabrowser.js +230 -0
  14. package/opcua/104-opcuaserver.html +261 -0
  15. package/opcua/104-opcuaserver.js +1844 -0
  16. package/opcua/105-opcuaendpoint.html +224 -0
  17. package/opcua/105-opcuaendpoint.js +64 -0
  18. package/opcua/106-opcuaevent.html +110 -0
  19. package/opcua/106-opcuaevent.js +50 -0
  20. package/opcua/107-opcuamethod.html +244 -0
  21. package/opcua/107-opcuamethod.js +521 -0
  22. package/opcua/108-opcuarights.html +223 -0
  23. package/opcua/108-opcuarights.js +156 -0
  24. package/opcua/109-opcuadiscovery.html +51 -0
  25. package/opcua/109-opcuadiscovery.js +180 -0
  26. package/opcua/dump_certificates.js +55 -0
  27. package/opcua/icons/opcuanode.png +0 -0
  28. package/opcua/icons/opcuanodeLogo.png +0 -0
  29. package/opcua/opcua-basics.js +1061 -0
  30. package/opcua/public/vendor/opc-foundation/binary/OPC.ISA95.Types.bsd.xml +141 -0
  31. package/opcua/public/vendor/opc-foundation/binary/Opc.Ua.Adi.Types.bsd.xml +76 -0
  32. package/opcua/public/vendor/opc-foundation/binary/Opc.Ua.Di.Types.bsd.xml +11 -0
  33. package/opcua/public/vendor/opc-foundation/binary/Opc.Ua.Gds.Types.bsd.xml +25 -0
  34. package/opcua/public/vendor/opc-foundation/binary/Opc.Ua.Types.bsd.xml +2391 -0
  35. package/opcua/public/vendor/opc-foundation/csv/AttributeIds.csv +22 -0
  36. package/opcua/public/vendor/opc-foundation/csv/NodeIds.csv +5746 -0
  37. package/opcua/public/vendor/opc-foundation/csv/Opc.Ua.StatusCodes.csv +227 -0
  38. package/opcua/public/vendor/opc-foundation/csv/OpcUaDiModel.csv +1523 -0
  39. package/opcua/public/vendor/opc-foundation/csv/OpcUaGdsModel.csv +400 -0
  40. package/opcua/public/vendor/opc-foundation/csv/ServerCapabilities.csv +13 -0
  41. package/opcua/public/vendor/opc-foundation/csv/ServerCapabilityIdentifiers.csv +13 -0
  42. package/opcua/public/vendor/opc-foundation/csv/StatusCode.csv +227 -0
  43. package/opcua/public/vendor/opc-foundation/csv/StatusCodes.csv +227 -0
  44. package/opcua/public/vendor/opc-foundation/schema/OPC.ISA95.Types.xsd +163 -0
  45. package/opcua/public/vendor/opc-foundation/schema/OPCBinarySchema.xsd +119 -0
  46. package/opcua/public/vendor/opc-foundation/schema/Opc.Ua.Types.xsd +3938 -0
  47. package/opcua/public/vendor/opc-foundation/schema/SecuredApplication.xsd +106 -0
  48. package/opcua/public/vendor/opc-foundation/schema/UANodeSet.xsd +420 -0
  49. package/opcua/public/vendor/opc-foundation/xml/OPC.ISA95.NodeSet2.Nov52013.xml +3293 -0
  50. package/opcua/public/vendor/opc-foundation/xml/Opc.ISA95.NodeSet2.xml +3589 -0
  51. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.Adi.NodeSet2.xml +8301 -0
  52. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.Adi.Types.bsd.xml +76 -0
  53. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.Adi.Types.xsd +157 -0
  54. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.AutoID.NodeSet2.xml +6062 -0
  55. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.Di.NodeSet2.xml +5066 -0
  56. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.Di.PredefinedNodes.xml +11495 -0
  57. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.Di.Types.bsd +86 -0
  58. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.Di.Types.bsd.xml +11 -0
  59. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.Di.Types.xsd +155 -0
  60. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.Gds.NodeSet.xml +15226 -0
  61. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.Gds.NodeSet2.xml +6214 -0
  62. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.Gds.Types.bsd.xml +25 -0
  63. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.Gds.Types.xsd +30 -0
  64. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.NodeSet-2.xml +134881 -0
  65. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.NodeSet-3.xml +134881 -0
  66. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.NodeSet.xml +134881 -0
  67. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.NodeSet2.Part10.xml +832 -0
  68. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.NodeSet2.Part11.xml +793 -0
  69. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.NodeSet2.Part12.xml +3877 -0
  70. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.NodeSet2.Part13.xml +344 -0
  71. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.NodeSet2.Part3.xml +1103 -0
  72. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.NodeSet2.Part4.xml +3091 -0
  73. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.NodeSet2.Part5.xml +17092 -0
  74. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.NodeSet2.Part8.xml +525 -0
  75. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.NodeSet2.Part9.xml +2049 -0
  76. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.NodeSet2.Part999.xml +134 -0
  77. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.NodeSet2.xml +67169 -0
  78. package/opcua/public/vendor/opc-foundation/xml/Opc.Ua.PredefinedNodes.xml +64500 -0
  79. package/opcua/public/vendor/opc-foundation/xml/OpcUaDiModel.xml +1239 -0
  80. package/opcua/utils.js +55 -0
  81. package/package.json +68 -0
  82. package/users.json +22 -0
@@ -0,0 +1,2741 @@
1
+ /**
2
+
3
+ Copyright 2018 Valmet Automation Inc.
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Unless required by applicable law or agreed to in writing, software
12
+ distributed under the License is distributed on an "AS IS" BASIS,
13
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ See the License for the specific language governing permissions and
15
+ limitations under the License.
16
+
17
+ **/
18
+
19
+ require("node-opcua");
20
+ require("util");
21
+
22
+ module.exports = function (RED) {
23
+ "use strict";
24
+ let chalk = require("chalk");
25
+ let opcua = require('node-opcua');
26
+ const { NodeCrawler } = require("node-opcua-client-crawler"); // Legacy support
27
+ let opcuaBasics = require('./opcua-basics');
28
+ let crypto_utils = require("node-opcua-crypto"); // opcua.crypto_utils;
29
+ let fileTransfer = require("node-opcua-file-transfer");
30
+ let async = require("async");
31
+ let fs = require("fs");
32
+ let os = require("os");
33
+ let cloneDeep = require('lodash.clonedeep');
34
+ let DataType = opcua.DataType;
35
+ let AttributeIds = opcua.AttributeIds;
36
+ let TimestampsToReturn = opcua.TimestampsToReturn;
37
+
38
+ const { createClientCertificateManager } = require("./utils");
39
+
40
+ const { stringify } = require('flatted');
41
+
42
+ function OpcUaClientNode(n) {
43
+ RED.nodes.createNode(this, n);
44
+ this.name = n.name;
45
+ this.action = n.action;
46
+ let originalAction = n.action;
47
+ this.time = n.time;
48
+ this.timeUnit = n.timeUnit;
49
+ this.deadbandtype = n.deadbandtype;
50
+ this.deadbandvalue = n.deadbandvalue;
51
+ this.certificate = n.certificate; // n == NONE, l == Local file, e == Endpoint, u == Upload
52
+ this.localfile = n.localfile; // Local certificate file
53
+ this.localkeyfile = n.localkeyfile; // Local private key file
54
+ this.useTransport = n.useTransport;
55
+ this.maxChunkCount = n.maxChunkCount;
56
+ this.maxMessageSize = n.maxMessageSize;
57
+ this.receiveBufferSize = n.receiveBufferSize;
58
+ this.sendBufferSize = n.sendBufferSize;
59
+ this.setstatusandtime = n.setstatusandtime;
60
+ this.keepsessionalive = n.keepsessionalive;
61
+ this.applicationName = n.applicationName;
62
+ this.applicationUri = n.applicationUri;
63
+ let node = this;
64
+ let opcuaEndpoint = RED.nodes.getNode(n.endpoint); // Use as global for the node
65
+ let userIdentity = { type: opcua.UserTokenType.Anonymous }; // Initialize with Anonymous
66
+ let connectionOption = {};
67
+ let cmdQueue = []; // queue msgs which can currently not be handled because session is not established, yet and currentStatus is 'connecting'
68
+ let currentStatus = ''; // the status value set set by node.status(). Didn't find a way to read it back.
69
+ let multipleItems = []; // Store & read multiple nodeIds
70
+ let writeMultipleItems = []; // Store & write multiple nodeIds & values
71
+ let options;
72
+ connectionOption.securityPolicy = opcuaEndpoint.securityPolicy
73
+ connectionOption.securityMode = opcua.MessageSecurityMode[opcuaEndpoint.securityMode] || opcua.MessageSecurityMode.None;
74
+ let userCertificate = opcuaEndpoint.userCertificate;
75
+ let userPrivatekey = opcuaEndpoint.userPrivatekey;
76
+
77
+ connectionOption.clientCertificateManager = createClientCertificateManager();
78
+
79
+ if (node.certificate === "l" && node.localfile) {
80
+ verbose_log("Using 'own' local certificate file " + node.localfile);
81
+ // User must define absolute path
82
+ let certfile = node.localfile;
83
+ let keyfile = node.localkeyfile;
84
+ connectionOption.certificateFile = certfile;
85
+ connectionOption.privateKeyFile = keyfile;
86
+
87
+ if (!fs.existsSync(certfile)) {
88
+ node_error("Local certificate file not found: " + certfile)
89
+ }
90
+ if (!fs.existsSync(keyfile)) {
91
+ node_error("Local private key file not found: " + keyfile)
92
+ }
93
+ }
94
+ if (node.applicationName) connectionOption.applicationName = node.applicationName;
95
+ if (node.applicationUri) connectionOption.applicationUri = node.applicationUri;
96
+ // Moved needed options to client create
97
+ connectionOption.requestedSessionTimeout = opcuaBasics.calc_milliseconds_by_time_and_unit(300, "s");
98
+ // DO NOT USE must be NodeOPCUA-Client !! connectionOption.applicationName = node.name; // Application name
99
+ connectionOption.clientName = node.name; // This is used for the session names
100
+ connectionOption.endpointMustExist = false;
101
+ connectionOption.defaultSecureTokenLifetime = 40000 * 5;
102
+ // From the node UI, keep min values!
103
+ // Needed or not?
104
+ if (!node.maxChunkCount || parseInt(node.maxChunkCount) < 1) node.maxChunkCount = 1;
105
+ if (!node.maxMessageSize || parseInt(node.maxMessageSize) < 8192) node.maxMessageSize = 8192;
106
+ if (!node.receiveBufferSize || parseInt(node.receiveBufferSize) < 8 * 1024) node.receiveBufferSize = 8 * 1024;
107
+ if (!node.sendBufferSize || parseInt(node.sendBufferSize) < 8 * 1024) node.sendBufferSize = 8 * 1024;
108
+
109
+ let transportSettings = {
110
+ maxChunkCount: parseInt(node.maxChunkCount), // Default 1
111
+ maxMessageSize: parseInt(node.maxMessageSize), // should be at least 8192
112
+ receiveBufferSize: parseInt(node.receiveBufferSize), // 8 * 1024,
113
+ sendBufferSize: parseInt(node.sendBufferSize) // 8 * 1024
114
+ };
115
+
116
+ if (node.useTransport === true) {
117
+ connectionOption["transportSettings"] = { ...transportSettings };
118
+ verbose_log(chalk.red("Using, transport settings: ") + chalk.cyan(JSON.stringify(connectionOption["transportSettings"])));
119
+ }
120
+
121
+ connectionOption.connectionStrategy = {
122
+ maxRetry: 10512000, // Limited to max 10 ~5min // 10512000, // 10 years should be enough. No infinite parameter for backoff.
123
+ initialDelay: 5000, // 5s
124
+ maxDelay: 30 * 1000 // 30s
125
+ };
126
+
127
+ // Ensure Anonymous login
128
+ if (connectionOption.securityMode === opcua.MessageSecurityMode.None || opcuaEndpoint.none === true) {
129
+ userIdentity = { type: opcua.UserTokenType.Anonymous };
130
+ }
131
+ if (opcuaEndpoint.login === true && opcuaEndpoint.usercert === true) {
132
+ userIdentity = { type: opcua.UserTokenType.Anonymous };
133
+ }
134
+ if (opcuaEndpoint.login === true && opcuaEndpoint.usercert === true) {
135
+ node.error("Cannot use username & password & user certificate at the same time!");
136
+ }
137
+ // Username & password with securityMode None is allowed
138
+ if (opcuaEndpoint.login === true) { // } && connectionOption.securityMode != opcua.MessageSecurityMode.None) {
139
+ userIdentity = {
140
+ type: opcua.UserTokenType.UserName,
141
+ userName: opcuaEndpoint.credentials.user.toString(),
142
+ password: opcuaEndpoint.credentials.password ? opcuaEndpoint.credentials.password.toString() : ""
143
+ };
144
+ verbose_log(chalk.green("Using UserName & password: ") + chalk.cyan(JSON.stringify(userIdentity)));
145
+ }
146
+ else if (opcuaEndpoint.usercert === true) {
147
+ if (!fs.existsSync(userCertificate)) {
148
+ node.error("User certificate file not found: " + userCertificate);
149
+ }
150
+ const certificateData = crypto_utils.readCertificate(userCertificate);
151
+
152
+ if (!fs.existsSync(userPrivatekey)) {
153
+ node.error("User private key file not found: " + userPrivatekey);
154
+ }
155
+ const privateKey = crypto_utils.readPrivateKeyPEM(userPrivatekey);
156
+ userIdentity = {
157
+ certificateData,
158
+ privateKey,
159
+ type: opcua.UserTokenType.Certificate // User certificate
160
+ };
161
+ }
162
+ else {
163
+ userIdentity = { type: opcua.UserTokenType.Anonymous };
164
+ }
165
+
166
+ verbose_log(chalk.green("UserIdentity: ") + chalk.cyan(JSON.stringify(userIdentity)));
167
+ let items = [];
168
+ let subscription; // only one subscription needed to hold multiple monitored Items
169
+
170
+ let monitoredItems = new Map();
171
+
172
+
173
+ function node_error(err) {
174
+ node.error(chalk.red("Client node error on: " + node.name + " error: " + stringify(err)));
175
+ }
176
+
177
+ function verbose_warn(logMessage) {
178
+ if (opcuaEndpoint?.name && node.name) { // Optional name
179
+ console.warn(chalk.cyan(`${opcuaEndpoint?.name}`) + chalk.yellow(":") + chalk.cyan(node.name) ? chalk.cyan(node.name) + chalk.yellow(': ') + chalk.cyan(logMessage) : chalk.yellow('OpcUaClientNode: ') + chalk.cyan(logMessage));
180
+ }
181
+ node.warn(logMessage);
182
+ }
183
+
184
+ function verbose_log(logMessage) {
185
+ if (RED.settings.verbose) {
186
+ node.debug(chalk.yellow(logMessage));
187
+ }
188
+ }
189
+
190
+ async function getBrowseName(_session, nodeId) {
191
+ const dataValue = await _session.read({
192
+ attributeId: AttributeIds.BrowseName,
193
+ nodeId
194
+ });
195
+ if (dataValue.statusCode.isGood()) {
196
+ const browseName = dataValue?.value?.value?.name || "UnknownEvent";
197
+ return browseName;
198
+ } else {
199
+ return "???";
200
+ }
201
+ }
202
+ // Fields selected alarm fields
203
+ // EventFields same order returned from server array of variants (filled or empty)
204
+ async function __dumpEvent(node, session, fields, eventFields, _callback) {
205
+ let msg = {};
206
+ msg.payload = {};
207
+ verbose_log(chalk.yellow("Event Fields: ") + chalk.cyan(JSON.stringify(eventFields)));
208
+ set_node_status_to("active event");
209
+
210
+ for (let i = 0; i < eventFields.length; i++) {
211
+ let variant = eventFields[i];
212
+ let fieldName = fields[i];
213
+ verbose_log(chalk.yellow("Event Field: ") + chalk.cyan(fieldName) + " " + chalk.cyan(stringify(variant)));
214
+ // Check if variant is NodeId and then get qualified name (browseName)
215
+ if (variant?.dataType && variant.dataType === DataType.NodeId) {
216
+ fieldName = await getBrowseName(session, variant.value);
217
+ }
218
+ if (!variant || variant.dataType === DataType.Null || !variant.value) {
219
+ verbose_log(chalk.red("No variant or variant dataType is Null or no variant value! Variant: ") + chalk.cyan(JSON.stringify(variant)));
220
+ } else {
221
+ if (fieldName === "EventId" && variant?.dataType) {
222
+ msg.payload[fieldName] = "0x" + variant.value.toString("hex"); // As in UaExpert
223
+ msg.payload["_" + fieldName] = variant; // Keep as ByteString
224
+ } else {
225
+ // Added handling for LocalizedText to use OS locale / node-red __language__
226
+ if (fieldName === "Message") {
227
+ const locale = Intl.DateTimeFormat().resolvedOptions().locale;
228
+ if (variant.value.length > 1) {
229
+ let i = 0;
230
+ while (i < variant.value.length) {
231
+ let localText = variant.value[i];
232
+ // Change according locale
233
+ if (localText.locale === locale) {
234
+ variant.value = localText;
235
+ break;
236
+ }
237
+ i++;
238
+ }
239
+ }
240
+ }
241
+ msg.payload[fieldName] = opcuaBasics.clone_object(variant.value);
242
+ }
243
+ // if available, needed for Acknowledge function in client
244
+ if (fieldName === "ConditionId" && variant?.dataType) {
245
+ msg.topic = variant.value.toString();
246
+ }
247
+ }
248
+ }
249
+
250
+ // Set message topic
251
+ if (eventFields.length === 0) {
252
+ msg.topic = "No EventFields";
253
+ }
254
+ // if available, needed for Acknowledge function in client
255
+ else if (msg.payload.ConditionId) {
256
+ msg.topic = msg.payload.ConditionId.toString();
257
+ }
258
+ else if (msg.payload.EventId) {
259
+ msg.topic = msg.payload.EventId.toString(); // Set then this can be used to Acknowledge event
260
+ }
261
+ else if (msg.payload.EventType) {
262
+ msg.topic = msg.payload.EventType.toString();
263
+ }
264
+
265
+ verbose_log(chalk.yellow("Event message topic: ") + chalk.cyan(msg.topic));
266
+ node.send([msg, null, null]);
267
+
268
+ _callback();
269
+ }
270
+
271
+ let eventQueue = new async.queue(function (task, callback) {
272
+ __dumpEvent(task.node, task.session, task.fields, task.eventFields, callback);
273
+ });
274
+
275
+ function dumpEvent(node, session, fields, eventFields, _callback) {
276
+ eventQueue.push({
277
+ node: node,
278
+ session: session,
279
+ fields: fields,
280
+ eventFields: eventFields,
281
+ _callback: _callback
282
+ });
283
+ }
284
+
285
+ // Listener functions that can be removed on reconnect
286
+ const reestablish = function () {
287
+ set_node_status2_to("connected", "re-established");
288
+ };
289
+ const backoff = function (attempt, delay) {
290
+ let msg = {};
291
+ msg.error = {};
292
+ msg.error.message = "reconnect";
293
+ msg.error.source = this;
294
+ // node.error("reconnect", msg);
295
+ verbose_log(chalk.red("reconnect")) // + chalk.cyan(stringify(msg))); // msg is TOO big to show
296
+ set_node_status2_to("reconnect", "attempt #" + attempt + " retry in " + delay / 1000.0 + " sec");
297
+ };
298
+ const reconnection = function () {
299
+ set_node_status2_to("reconnect", "starting...");
300
+ };
301
+
302
+ function create_opcua_client(callback) {
303
+ node.client = null;
304
+ options = {
305
+ securityMode: connectionOption.securityMode,
306
+ securityPolicy: connectionOption.securityPolicy,
307
+ defaultSecureTokenLifetime: connectionOption.defaultSecureTokenLifetime,
308
+ endpointMustExist: connectionOption.endpointMustExist,
309
+ connectionStrategy: connectionOption.connectionStrategy,
310
+ clientCertificateManager: connectionOption.clientCertificateManager,
311
+ clientName: node.name, // Fix for #664 sessionName
312
+ keepSessionAlive: node.keepsessionalive,
313
+ requestedSessionTimeout: 60000 * 5, // 5min, default 1min
314
+ automaticallyAcceptUnknownCertificate: true,
315
+ // transportSettings: transportSettings // Some
316
+ applicationName: connectionOption.applicationName,
317
+ applicationUri: connectionOption.applicationUri
318
+ };
319
+ try {
320
+ // Use empty 0.0.0.0 address as "no client" initial value
321
+ if (opcuaEndpoint?.endpoint?.indexOf("opc.tcp://0.0.0.0") == 0) {
322
+ verbose_warn(`close opcua client ${node.client} userIdentity ${userIdentity.type}`);
323
+ if (node.client) {
324
+ close_opcua_client("connection error: no session", 0);
325
+ }
326
+ items = [];
327
+ node.items = items;
328
+ set_node_status_to("no client");
329
+ if (callback) {
330
+ callback();
331
+ }
332
+ return;
333
+ }
334
+ // Normal client
335
+ if (!node.keepsessionalive) {
336
+ node.keepsessionalive = false;
337
+ }
338
+ verbose_log(chalk.yellow("Keep session alive: ") + chalk.cyan(node.keepsessionalive));
339
+ if (node.useTransport === true) {
340
+ options["transportSettings"] = JSON.parse(JSON.stringify(connectionOption.transportSettings));
341
+ verbose_log(chalk.red("NOTE: Using transport settings: " + chalk.cyan(stringify(options))));
342
+ }
343
+ verbose_log(chalk.green("1) CREATE CLIENT: ") + chalk.cyan(stringify(options)));
344
+ node.client = opcua.OPCUAClient.create(options);
345
+ node.client.on("connection_reestablished", reestablish);
346
+ node.client.on("backoff", backoff);
347
+ node.client.on("start_reconnection", reconnection);
348
+ node.client.clientCertificateManager = connectionOption.clientCertificateManager; // FIX connection issue
349
+ }
350
+ catch (err) {
351
+ node_error("Cannot create client, check connection options: " + stringify(options)); // connectionOption
352
+ set_node_status_to("Cannot create client");
353
+ }
354
+ items = [];
355
+ node.items = items;
356
+ set_node_status_to("create client");
357
+ if (callback) {
358
+ callback();
359
+ }
360
+ }
361
+
362
+ function reset_opcua_client(callback) {
363
+ if (node.client) {
364
+ node.client.disconnect(function () {
365
+ verbose_log("Client disconnected!");
366
+ create_opcua_client(callback);
367
+ });
368
+ }
369
+ }
370
+
371
+ function close_opcua_client(message, error) {
372
+ // verbose_warn(`closing opcua client ${opcua.client == null} userIdentity ${JSON.stringify(userIdentity)}`)
373
+ if (node.client) {
374
+ node.client.removeListener("connection_reestablished", reestablish);
375
+ node.client.removeListener("backoff", backoff);
376
+ node.client.removeListener("start_reconnection", reconnection);
377
+ try {
378
+ // Always call disconnect() regardless of isReconnecting state.
379
+ // Skipping disconnect when isReconnecting=true leaves the TCP socket
380
+ // open on the PLC, causing connections to accumulate over time.
381
+ node.client.disconnect(function () {
382
+ node.client = null;
383
+ verbose_log("Client disconnected!");
384
+ if (error === 0) {
385
+ set_node_status_to("closed");
386
+ }
387
+ else {
388
+ set_node_error_status_to(message, error)
389
+ node.error("Client disconnected & closed: " + message + " error: " + error.toString());
390
+ }
391
+ });
392
+ }
393
+ catch (err) {
394
+ node.client = null;
395
+ node_error("Error on disconnect: " + stringify(err));
396
+ }
397
+ }
398
+ }
399
+
400
+ function set_node_status_to(statusValue) {
401
+ verbose_log(chalk.yellow("Client status: ") + chalk.cyan(statusValue));
402
+ let statusParameter = opcuaBasics.get_node_status(statusValue);
403
+ currentStatus = statusValue;
404
+ let endpoint = "";
405
+ if (opcuaEndpoint && opcuaEndpoint.endpoint) {
406
+ endpoint = opcuaEndpoint.endpoint
407
+ }
408
+ node.status({
409
+ fill: statusParameter.fill,
410
+ shape: statusParameter.shape,
411
+ text: statusParameter.status,
412
+ endpoint: `${endpoint}`
413
+ });
414
+ node.send([null, { error: null, endpoint: `${endpoint}`, status: currentStatus }, null])
415
+
416
+ }
417
+
418
+ function set_node_status2_to(statusValue, message) {
419
+ verbose_log(chalk.yellow("Client status: ") + chalk.cyan(statusValue));
420
+ let statusParameter = opcuaBasics.get_node_status(statusValue);
421
+ currentStatus = statusValue;
422
+ let endpoint = "";
423
+ if (opcuaEndpoint?.endpoint) {
424
+ endpoint = opcuaEndpoint.endpoint
425
+ }
426
+ node.status({
427
+ fill: statusParameter.fill,
428
+ shape: statusParameter.shape,
429
+ text: statusParameter.status + " " + message,
430
+ endpoint: `${endpoint}`
431
+ });
432
+ node.send([null, { error: null, endpoint: `${endpoint}`, status: currentStatus }, null]);
433
+ }
434
+
435
+ function set_node_error_status_to(statusValue, error) {
436
+ verbose_log(chalk.yellow("Client status: ") + chalk.cyan(statusValue));
437
+ let statusParameter = opcuaBasics.get_node_status(statusValue);
438
+ currentStatus = statusValue;
439
+ let endpoint = "";
440
+ if (opcuaEndpoint?.endpoint) {
441
+ endpoint = opcuaEndpoint.endpoint
442
+ }
443
+ if (!error) {
444
+ error = "";
445
+ }
446
+ node.status({
447
+ fill: statusParameter.fill,
448
+ shape: statusParameter.shape,
449
+ text: statusParameter.status + " " + error,
450
+ endpoint: `${endpoint}`
451
+ });
452
+ node.send([null, { error: error, endpoint: `${endpoint}`, status: currentStatus }, null]);
453
+ }
454
+
455
+ async function connect_opcua_client() {
456
+ if (opcuaEndpoint?.login === true) {
457
+ verbose_log(chalk.green("Using UserName & password: ") + chalk.cyan(JSON.stringify(userIdentity)));
458
+ if (opcuaEndpoint.credentials && opcuaEndpoint['user'] && opcuaEndpoint['password']) {
459
+ userIdentity = {
460
+ type: opcua.UserTokenType.UserName,
461
+ userName: opcuaEndpoint?.credentials?.user?.toString(),
462
+ password: opcuaEndpoint?.credentials?.password?.toString()
463
+ };
464
+ }
465
+ else if (opcuaEndpoint['user'] && opcuaEndpoint['password']) {
466
+ userIdentity = {
467
+ type: opcua.UserTokenType.UserName,
468
+ userName: opcuaEndpoint?.user?.toString(),
469
+ password: opcuaEndpoint?.password?.toString()
470
+ };
471
+ } else {
472
+ node_error("Please enter user or password in credentials or same level as login")
473
+ }
474
+ }
475
+ else if (opcuaEndpoint?.usercert === true) {
476
+ if (!fs.existsSync(userCertificate)) {
477
+ node.error("User certificate file not found: " + userCertificate);
478
+ }
479
+ const certificateData = crypto_utils.readCertificate(userCertificate);
480
+ if (!fs.existsSync(userPrivatekey)) {
481
+ node.error("User private key file not found: " + userPrivatekey);
482
+ }
483
+ const privateKey = crypto_utils.readPrivateKeyPEM(userPrivatekey);
484
+ userIdentity = {
485
+ certificateData,
486
+ privateKey,
487
+ type: opcua.UserTokenType.Certificate // User certificate
488
+ };
489
+ }
490
+ else {
491
+ verbose_warn(chalk.red("userIdentity is ANONYMOUS "));
492
+ userIdentity = { type: opcua.UserTokenType.Anonymous };
493
+ }
494
+
495
+ // Refactored from old async Javascript to new Typescript with await
496
+ let session;
497
+ // STEP 1
498
+ // First connect to server´s endpoint
499
+ if (opcuaEndpoint?.endpoint) {
500
+ verbose_log(chalk.yellow("Connecting to endpoint: ") + chalk.cyan(opcuaEndpoint?.endpoint));
501
+ }
502
+ else {
503
+ node_error("No client endpoint listed! Waiting...");
504
+ return;
505
+ }
506
+
507
+ if (opcuaEndpoint?.endpoint?.indexOf("opc.tcp://0.0.0.0") === 0) {
508
+ set_node_status_to("no client")
509
+ }
510
+ /*
511
+ else {
512
+ set_node_status_to("connecting");
513
+ }
514
+ if (!node.client) {
515
+ verbose_log("No client to connect...");
516
+ return;
517
+ }
518
+ */
519
+ verbose_log(chalk.yellow("Exact endpointUrl: ") + chalk.cyan(opcuaEndpoint?.endpoint) + chalk.yellow(" hostname: ") + chalk.cyan(os.hostname()));
520
+ try {
521
+ node.client = opcua.OPCUAClient.create(options);
522
+ node.client.clientCertificateManager = connectionOption.clientCertificateManager;
523
+ await node.client.clientCertificateManager.initialize();
524
+ }
525
+ catch (error1) {
526
+ set_node_status_to("invalid certificate");
527
+ let msg = {};
528
+ msg.error = {};
529
+ msg.error.message = "Certificate error: " + error1.message;
530
+ msg.error.source = this;
531
+ node.error("Certificate error", msg);
532
+ }
533
+ node.debug(chalk.yellow("Trusted folder: ") + chalk.cyan(node.client?.clientCertificateManager?.trustedFolder));
534
+ node.debug(chalk.yellow("Rejected folder: ") + chalk.cyan(node.client?.clientCertificateManager?.rejectedFolder));
535
+ node.debug(chalk.yellow("Crl folder: ") + chalk.cyan(node.client?.clientCertificateManager?.crlFolder));
536
+ node.debug(chalk.yellow("Issuers Cert folder: ") + chalk.cyan(node.client?.clientCertificateManager?.issuersCertFolder));
537
+ node.debug(chalk.yellow("Issuers Crl folder: ") + chalk.cyan(node.client?.clientCertificateManager?.issuersCrlFolder));
538
+
539
+ try {
540
+ verbose_log(chalk.green("2) Connecting using endpoint: ") + chalk.cyan(opcuaEndpoint?.endpoint) +
541
+ chalk.green(" securityMode: ") + chalk.cyan(connectionOption.securityMode) +
542
+ chalk.green(" securityPolicy: ") + chalk.cyan(connectionOption.securityPolicy));
543
+ await node.client.connect(opcuaEndpoint?.endpoint);
544
+ } catch (err) {
545
+ verbose_warn("Case A: Endpoint does not contain, 1==None 2==Sign 3==Sign&Encrypt, using securityMode: " + stringify(connectionOption.securityMode));
546
+ verbose_warn(" using securityPolicy: " + stringify(connectionOption.securityPolicy));
547
+ verbose_warn("Case B: UserName & password does not match to server (needed by Sign or SignAndEncrypt), check username: " + userIdentity.userName + " and password: " + userIdentity.password);
548
+ verbose_warn("Case C: With Sign you cannot use SecurityPolicy None!!");
549
+ verbose_warn("Case D: Certificate chain not valid, check server certificate chain against:");
550
+ verbose_warn(" CRL folder: " + node.client?.clientCertificateManager?.crlFolder);
551
+ verbose_warn(" Issuer folder: " + node.client?.clientCertificateManager?.issuersCertFolder);
552
+ verbose_warn(" Issuer CRL folder: " + node.client?.clientCertificateManager?.issuersCrlFolder);
553
+ // verbose_error("Invalid endpoint parameters: ", err);
554
+ node_error("Wrong endpoint parameters: " + JSON.stringify(opcuaEndpoint) + ", error: " + JSON.stringify(err));
555
+ set_node_status_to("invalid endpoint");
556
+ let msg = {};
557
+ msg.error = {};
558
+ msg.error.message = "Invalid endpoint: " + err;
559
+ msg.error.source = this;
560
+ node.error("Invalid endpoint", msg);
561
+ return;
562
+ }
563
+ verbose_log(chalk.green("Connected to endpoint: ") + chalk.cyan(opcuaEndpoint?.endpoint));
564
+
565
+ // STEP 2
566
+ // This will succeed first time only if security policy and mode are None
567
+ // Later user can use path and local file to access server certificate file
568
+
569
+ if (!node.client) {
570
+ node_error("Client not yet created & connected, cannot getEndpoints!");
571
+ return;
572
+ }
573
+
574
+ // dumpCertificates(node.client); // TODO Wrong folder or something to solve
575
+
576
+ // STEP 3
577
+ // verbose_log("Create session...");
578
+ try {
579
+ // verbose_warn(`Create session with userIdentity node.client ${node.client == null} userIdentity ${JSON.stringify(userIdentity)}`)
580
+ verbose_log(chalk.green("3) Create session with userIdentity at: ") + chalk.cyan(JSON.stringify(userIdentity)));
581
+ // {"clientName": "Node-red OPC UA Client node " + node.name},
582
+ // sessionName = "Node-red OPC UA Client node " + node.name;
583
+ if (!node.client) {
584
+ node_error("Client not yet created, cannot create session");
585
+ close_opcua_client("connection error: no client", 0);
586
+ return;
587
+ }
588
+ session = await node.client.createSession(userIdentity);
589
+ if (!session) {
590
+ node_error("Create session failed!");
591
+ verbose_warn(`Create session failed!`)
592
+
593
+ close_opcua_client("connection error: no session", 0);
594
+ return;
595
+ }
596
+ node.session = session;
597
+ set_node_status_to("session active");
598
+ for (let i in cmdQueue) {
599
+ processInputMsg(cmdQueue[i]);
600
+ }
601
+ cmdQueue = [];
602
+ } catch (err) {
603
+ node_error(node.name + " OPC UA connection error: " + err.message);
604
+ verbose_log(err);
605
+ node.session = null;
606
+ close_opcua_client("connection error", err);
607
+ }
608
+ }
609
+
610
+ function make_subscription(callback, msg, parameters) {
611
+ let newSubscription = null;
612
+
613
+ if (!node.session) {
614
+ verbose_log("Subscription without session");
615
+ return newSubscription;
616
+ }
617
+
618
+ if (!parameters) {
619
+ verbose_log("Subscription without parameters");
620
+ return newSubscription;
621
+ }
622
+ verbose_log("Publishing interval " + stringify(parameters));
623
+ newSubscription = opcua.ClientSubscription.create(node.session, parameters);
624
+ verbose_log("Subscription " + newSubscription.toString());
625
+ newSubscription.on("initialized", function () {
626
+ verbose_log("Subscription initialized");
627
+ set_node_status_to("initialized");
628
+ });
629
+
630
+ newSubscription.on("started", function () {
631
+ verbose_log("Subscription subscribed ID: " + newSubscription.subscriptionId);
632
+ set_node_status_to("subscribed");
633
+ monitoredItems.clear();
634
+ callback(newSubscription, msg);
635
+ });
636
+
637
+ newSubscription.on("keepalive", function () {
638
+ verbose_log("Subscription keepalive ID: " + newSubscription.subscriptionId);
639
+ set_node_status_to("keepalive");
640
+ });
641
+
642
+ newSubscription.on("terminated", function () {
643
+ verbose_log("Subscription terminated ID: " + newSubscription.subscriptionId);
644
+ set_node_status_to("terminated");
645
+ subscription = null;
646
+ monitoredItems.clear();
647
+ });
648
+
649
+ newSubscription.on("error", function (err) {
650
+ verbose_log("Subscription error on ID: " + newSubscription.subscriptionId + ". " + err);
651
+ set_node_error_status_to("subscription error")
652
+ subscription = null;
653
+ monitoredItems.clear();
654
+ })
655
+
656
+ return newSubscription;
657
+ }
658
+
659
+ if (!node.client) {
660
+ create_opcua_client(connect_opcua_client);
661
+ }
662
+
663
+ function processInputMsg(msg) {
664
+ if (msg.action === "reconnect") {
665
+ cmdQueue = [];
666
+ // msg.endpoint can be used to change endpoint
667
+ msg.action = "";
668
+ reconnect(msg);
669
+ return;
670
+ }
671
+ if (msg.action === "connect") {
672
+ cmdQueue = [];
673
+ // msg.endpoint can be used to change endpoint
674
+ msg.action = "";
675
+ connect_action_input(msg);
676
+ return;
677
+ }
678
+ if (msg.action && msg.action.length > 0) {
679
+ verbose_log("Override node action by msg.action: " + msg.action);
680
+ node.action = msg.action;
681
+ }
682
+ else {
683
+ verbose_log(chalk.green("Using node action: ") + chalk.cyan(originalAction));
684
+ node.action = originalAction; // Use original action from the node
685
+ }
686
+ // With new node-red easier to set action into payload
687
+ if (msg.payload?.action && msg.payload.action.length > 0) {
688
+ verbose_log("Override node action by msg.payload.action:" + msg.payload.action);
689
+ node.action = msg.payload.action;
690
+ }
691
+
692
+ if (!node.action) {
693
+ verbose_warn("Can't work without action (read, write, browse ...)");
694
+ //node.send(msg); // do not send in case of error
695
+ node.send([null, { error: "Can't work without action (read, write, browse ...)", endpoint: `${opcuaEndpoint.endpoint}`, status: currentStatus }, null]);
696
+
697
+ return;
698
+ }
699
+
700
+ if (!node.client || !node.session) {
701
+ verbose_log("Not connected, current status: " + currentStatus);
702
+ // Added statuses when msg must be put to queue
703
+ // Added statuses when msg must be put to queue
704
+ const statuses = ['', 'create client', 'connecting', 'reconnect'];
705
+ if (statuses.includes(currentStatus)) {
706
+ cmdQueue.push(msg);
707
+ } else {
708
+ verbose_warn(`can't work without OPC UA client ${node.client} client ${node.session}`);
709
+ reset_opcua_client(connect_opcua_client);
710
+ }
711
+ node.send([null, { error: "can't work without OPC UA client", endpoint: `${opcuaEndpoint?.endpoint}`, status: currentStatus }, null]);
712
+ return;
713
+ }
714
+
715
+ if (!node.session.sessionId == "terminated") {
716
+ verbose_warn("terminated OPC UA Session");
717
+ reset_opcua_client(connect_opcua_client);
718
+
719
+ node.send([null, { error: "terminated OPC UA Session", endpoint: `${opcuaEndpoint?.endpoint}`, status: currentStatus }, null]);
720
+
721
+ return;
722
+ }
723
+
724
+ if (msg.action && (msg.action === "connect" || msg.action === "disconnect")) {
725
+ // OK
726
+ msg.action = "";
727
+ } else if (!msg.topic) {
728
+ verbose_warn("can't work without OPC UA NodeId - msg.topic empty");
729
+ node.send([null, { error: "can't work without OPC UA NodeId", endpoint: `${opcuaEndpoint?.endpoint}`, status: currentStatus }, null]);
730
+ return;
731
+ }
732
+
733
+ verbose_log(chalk.yellow("Action on input: ") + chalk.cyan(node.action) +
734
+ chalk.yellow(" Item from Topic: ") + chalk.cyan(msg.topic) +
735
+ chalk.yellow(" session Id: ") + chalk.cyan(node.session.sessionId));
736
+
737
+ switch (node.action) {
738
+ case "connect":
739
+ connect_action_input(msg);
740
+ break;
741
+ case "disconnect":
742
+ disconnect_action_input(msg);
743
+ break;
744
+ case "reconnect":
745
+ reconnect(msg);
746
+ break;
747
+ case "register":
748
+ register_action_input(msg);
749
+ break;
750
+ case "unregister":
751
+ unregister_action_input(msg);
752
+ break;
753
+ case "read":
754
+ read_action_input(msg);
755
+ break;
756
+ case "history":
757
+ readhistory_action_input(msg);
758
+ break;
759
+ case "info":
760
+ info_action_input(msg);
761
+ break;
762
+ case "build":
763
+ build_extension_object_action_input(msg);
764
+ break;
765
+ case "write":
766
+ write_action_input(msg);
767
+ break;
768
+ case "subscribe":
769
+ subscribe_action_input(msg);
770
+ break;
771
+ case "monitor":
772
+ monitor_action_input(msg);
773
+ break;
774
+ case "unsubscribe":
775
+ unsubscribe_action_input(msg);
776
+ break;
777
+ case "deletesubscribtion": // miss-spelled, this allows old flows to work
778
+ case "deletesubscription":
779
+ delete_subscription_action_input(msg);
780
+ break;
781
+ case "browse":
782
+ browse_action_input(msg);
783
+ break;
784
+ case "events":
785
+ subscribe_events_input(msg);
786
+ break;
787
+ case "readmultiple":
788
+ readmultiple_action_input(msg);
789
+ break;
790
+ case "writemultiple":
791
+ writemultiple_action_input(msg)
792
+ break;
793
+ case "acknowledge":
794
+ acknowledge_input(msg);
795
+ break;
796
+ case "readfile":
797
+ read_file(msg);
798
+ break;
799
+ case "writefile":
800
+ write_file(msg);
801
+ break;
802
+ case "method":
803
+ method_action_input(msg);
804
+ break;
805
+ default:
806
+ verbose_warn("Unknown action: " + node.action + " with msg " + stringify(msg));
807
+ break;
808
+ }
809
+ }
810
+ node.on("input", processInputMsg);
811
+
812
+ async function acknowledge_input(msg) {
813
+ // msg.topic is nodeId of the alarm object like Prosys ns=6;s=MyLevel.Alarm
814
+ // msg.conditionId is actual conditionObject that contains ns=6;s=MyLevel.Alarm/0:EventId current/latest eventId will be read
815
+ // msg.comment will be used as comment in the acknowledge
816
+ let eventId;
817
+ if (msg.conditionId) {
818
+ const dataValue = await node.session.read({ nodeId: msg.conditionId, attributeId: AttributeIds.Value });
819
+
820
+ eventId = dataValue.value.value;
821
+ verbose_log(chalk.yellow("Acknowledge (alarm object == topic): ") + chalk.cyan(msg.topic) +
822
+ chalk.yellow(" conditionObject (nodeId of eventId): ") + chalk.cyan(msg.conditionId) +
823
+ chalk.yellow(" value of eventId: 0x") + chalk.cyan(eventId.toString("hex")) +
824
+ chalk.yellow(" comment: ") + chalk.cyan(msg.comment));
825
+ }
826
+ // If actual eventId provided use it
827
+ if (msg.eventId) {
828
+ eventId = msg.eventId;
829
+ }
830
+ if (eventId) {
831
+ try {
832
+ const ackedState = await node.session.read({ nodeId: msg.topic + "/0:AckedState/0:Id", attributeId: AttributeIds.Value });
833
+ node.debug(chalk.yellow("EVENT ACKED STATE: ") + chalk.cyan(ackedState));
834
+ if (ackedState && ackedState.statusCode === opcua.StatusCodes.Good && ackedState.value.value === true) {
835
+ node.status({
836
+ fill: "yellow",
837
+ shape: "dot",
838
+ text: "Event: " + msg.topic + " already acknowledged"
839
+ });
840
+ }
841
+ else {
842
+ const status = await node.session.acknowledgeCondition(msg.topic, eventId, msg.comment);
843
+ if (status !== opcua.StatusCodes.Good) {
844
+ node_error(node.name + "Error at acknowledge, status: " + status.toString());
845
+ set_node_error_status_to("error", status.toString());
846
+ }
847
+ else {
848
+ node.status({
849
+ fill: "green",
850
+ shape: "dot",
851
+ text: "Event: " + msg.topic + " acknowledged"
852
+ });
853
+ }
854
+ }
855
+ }
856
+ catch (err) {
857
+ node_error(node.name + "Error at acknowledge: " + msg.topic + " eventId: " + eventId + " error: " + err);
858
+ set_node_error_status_to("error", err);
859
+ }
860
+ }
861
+ else {
862
+ node_error(node.name + " error at acknowledge, no eventId, possible wrong msg.conditionId " + msg.conditionId);
863
+ }
864
+ }
865
+
866
+ async function read_file(msg) {
867
+ verbose_log("Read file, nodeId: " + msg.topic.toString());
868
+ let file_node = opcua.coerceNodeId(msg.topic);
869
+ if (node.session) {
870
+ try {
871
+ const clientFile = new fileTransfer.ClientFile(node.session, file_node);
872
+ fileTransfer.ClientFile.useGlobalMethod = true;
873
+ // Given that the file is opened in ReadMode Only
874
+ await clientFile.open(fileTransfer.OpenFileMode.Read);
875
+
876
+ // Read file size
877
+ try {
878
+ const size = await clientFile.size(); // This should read size from the file itself
879
+ let buf = await clientFile.read(size);
880
+ // node-opcua-file-transfer takes care of the whole file reading from v2.94.0
881
+ await clientFile.close();
882
+ msg.payload = buf;
883
+ // Debug purpose, show content
884
+ verbose_log("File content: " + buf.toString());
885
+ }
886
+ catch (err) {
887
+ msg.payload = "";
888
+ node_error(node.name + " failed to read file, nodeId: " + msg.topic + " error: " + err);
889
+ set_node_error_status_to("error", "Cannot read file!");
890
+ node.send([null, { error: node.name + " failed to read file, nodeId: " + msg.topic + " error: " + err, endpoint: `${opcuaEndpoint?.endpoint}`, status: currentStatus }]);
891
+
892
+ }
893
+
894
+ node.send([msg, null]);
895
+
896
+ }
897
+ catch (err) {
898
+ node_error(node.name + " failed to read fileTransfer, nodeId: " + msg.topic + " error: " + err);
899
+ set_node_error_status_to("error", err.toString());
900
+ node.send([null, { error: node.name + " failed to read fileTransfer, nodeId: " + msg.topic + " error: " + err, endpoint: `${opcuaEndpoint?.endpoint}`, status: currentStatus }, null]);
901
+
902
+ }
903
+ }
904
+ else {
905
+ verbose_warn("No open session to read file!");
906
+ }
907
+ }
908
+
909
+ async function write_file(msg) {
910
+ verbose_log("Write file, nodeId: " + msg.topic.toString());
911
+ let file_node = opcua.coerceNodeId(msg.topic);
912
+ if (node.session) {
913
+ try {
914
+ let buf;
915
+ if (msg.payload && msg.payload.length > 0) {
916
+ buf = msg.payload;
917
+ }
918
+ if (msg.fileName) {
919
+ verbose_log("Uploading file: " + msg.fileName);
920
+ buf = fs.readFileSync(msg.fileName);
921
+ }
922
+ const clientFile = new fileTransfer.ClientFile(node.session, file_node);
923
+ clientFile.useGlobalMethod = true;
924
+ // Given that the file is opened in WriteMode
925
+ await clientFile.open(fileTransfer.OpenFileMode.Write);
926
+ verbose_log("Local file content: " + buf.toString());
927
+ verbose_log("Writing file to server...");
928
+ await clientFile.write(buf);
929
+ await clientFile.close();
930
+ verbose_log("Write done!");
931
+ }
932
+ catch (err) {
933
+ node.error(chalk.red("Cannot write file, error: " + err.message));
934
+ }
935
+ }
936
+ else {
937
+ verbose_warn("No open session to write file!");
938
+ }
939
+ }
940
+
941
+ async function method_action_input(msg) {
942
+ verbose_log("Calling method: " + JSON.stringify(msg));
943
+ if (node.session) {
944
+ try {
945
+ let status = await callMethod(msg);
946
+ if (status === opcua.StatusCodes.Good) {
947
+ node.status({
948
+ fill: "green",
949
+ shape: "dot",
950
+ text: "Method executed"
951
+ });
952
+ }
953
+ else {
954
+ node.error("Failed, method result: ", status.description);
955
+ }
956
+ }
957
+ catch (err) {
958
+ node.error(chalk.red("Cannot call method, error: " + err.message));
959
+ }
960
+ }
961
+ else {
962
+ verbose_warn("No open session to call method!");
963
+ }
964
+ }
965
+
966
+ async function callMethod(msg) {
967
+ if (msg.methodId && msg.inputArguments) {
968
+ verbose_log("Calling method: " + JSON.stringify(msg.methodId));
969
+ verbose_log("InputArguments: " + JSON.stringify(msg.inputArguments));
970
+ if (msg.outputArguments) {
971
+ verbose_log("OutputArguments: " + JSON.stringify(msg.outputArguments));
972
+ }
973
+ let args=[];
974
+ let i = 0;
975
+ let tmp;
976
+ set_node_status_to("building method arguments");
977
+ try {
978
+ while (i < msg.inputArguments.length) {
979
+ tmp = msg.inputArguments[i];
980
+ if (tmp.dataType === "NodeId") {
981
+ tmp.value = opcua.coerceNodeId(tmp.value);
982
+ }
983
+ if (tmp.dataType === "ExtensionObject") {
984
+ let extensionobject = null;
985
+ // tmp = {dataType: "ExtensionObject", typeid: tmp.typeid, value: tmp.value};
986
+ if (tmp.typeid) {
987
+ extensionobject = await node.session.constructExtensionObject(opcua.coerceNodeId(tmp.typeid), tmp.value); // TODO make while loop to enable await
988
+ tmp.value = extensionobject;
989
+ }
990
+ // verbose_log("ExtensionObject: " + extensionobject);
991
+ // Object.assign(extensionobject, tmp.value);
992
+ /*
993
+ verbose_log(chalk.green("ExtensionObject value: ") + JSON.stringify(tmp.value));
994
+ tmp.value = new opcua.Variant({
995
+ dataType: opcua.DataType.ExtensionObject,
996
+ value: tmp.value // JSON.stringify(tmp.value) // extensionobject
997
+ });
998
+ */
999
+ verbose_log(chalk.green("ExtensionObject: ") + JSON.stringify(tmp));
1000
+ }
1001
+ else {
1002
+ if (tmp.valueRank && tmp.valueRank >= 1) {
1003
+ tmp = {dataType: tmp.dataType, valueRank: tmp.valueRank, arrayDimensions: 1, value: tmp.value};
1004
+ }
1005
+ else {
1006
+ tmp = {dataType: tmp.dataType, value: tmp.value};
1007
+ }
1008
+ verbose_log("Basic type: " + JSON.stringify(tmp));
1009
+ }
1010
+ args.push(tmp);
1011
+ i++;
1012
+ }
1013
+ } catch (err) {
1014
+ console.log(chalk.red("Error: "), err);
1015
+ let msg = {};
1016
+ msg.error = {};
1017
+ msg.error.message = "Invalid NodeId: " + err;
1018
+ msg.error.source = this;
1019
+ node.error("Invalid argument: ", tmp);
1020
+ return opcua.StatusCodes.BadNodeIdUnknown;
1021
+ }
1022
+ // verbose_log("Updated InputArguments: " + stringify(msg.inputArguments));
1023
+ verbose_log("Updated InputArguments: " + JSON.stringify(args));
1024
+ let callMethodRequest;
1025
+ let diagInfo;
1026
+ try {
1027
+ callMethodRequest = new opcua.CallMethodRequest({
1028
+ objectId: opcua.coerceNodeId(msg.objectId),
1029
+ methodId: opcua.coerceNodeId(msg.methodId),
1030
+ inputArgumentDiagnosticInfos: diagInfo,
1031
+ inputArguments: args,
1032
+ // inputArguments: msg.inputArguments,
1033
+ outputArguments: msg.outputArguments
1034
+ });
1035
+ } catch (err) {
1036
+ set_node_status_to("call method error");
1037
+ node.error("Build method request failed, error: " + err.message);
1038
+ }
1039
+ verbose_log("Call request: " + callMethodRequest.toString());
1040
+ verbose_log("Calling: " + callMethodRequest);
1041
+ try {
1042
+ set_node_status_to("call method");
1043
+ const result = await node.session.call(callMethodRequest);
1044
+ if (diagInfo) {
1045
+ verbose_log("Diagn. info: " + stringify(diagInfo));
1046
+ }
1047
+ if (msg.outputArguments) {
1048
+ verbose_log("Output args: " + stringify(msg.outputArguments));
1049
+ }
1050
+ verbose_log("Results: " + stringify(result));
1051
+ msg.result = result;
1052
+ if (result && result.statusCode === opcua.StatusCodes.Good) {
1053
+ let i = 0;
1054
+ msg.output = result.outputArguments; // Original outputArguments
1055
+ msg.payload = []; // Store values back to array
1056
+ if (result.outputArguments.length == 1) {
1057
+ verbose_log("Value: " + result.outputArguments[i].value);
1058
+ msg.payload = result.outputArguments[0].value; // Return only if one output argument
1059
+ } else {
1060
+ while (result.outputArguments.length > i) {
1061
+ verbose_log("Value[" + i + "]:" + result.outputArguments[i].toString());
1062
+ msg.payload.push(result.outputArguments[i].value); // Just copy result value to payload[] array, actual value needed mostly
1063
+ i++;
1064
+ }
1065
+ }
1066
+ } else {
1067
+ set_node_status_to("execute method error");
1068
+ node.error("Execute method result, error:" + result.statusCode.description);
1069
+ node.send([null, { error: "Execute method result, error:" + result.statusCode.description, endpoint: `${opcuaEndpoint?.endpoint}`, status: currentStatus }]);
1070
+
1071
+ return result.statusCode;
1072
+ }
1073
+ node.send([msg, null]);
1074
+
1075
+ return opcua.StatusCodes.Good;
1076
+ } catch (err) {
1077
+ set_node_status_to("execute method error");
1078
+ node.error("Method execution error: " + err.message);
1079
+ node.send([null, { error: "Method execution error: " + err.message, endpoint: `${opcuaEndpoint?.endpoint}`, status: currentStatus }, null]);
1080
+
1081
+ return opcua.StatusCodes.BadMethodInvalid;
1082
+ }
1083
+ }
1084
+ }
1085
+
1086
+ async function connect_action_input(msg) {
1087
+ console.log("#1 ACTION Connect!");
1088
+ verbose_log("Connecting...");
1089
+ if (msg && msg.OpcUaEndpoint) {
1090
+ // Remove listeners if existing
1091
+ if (node.client) {
1092
+ verbose_log("Cleanup old listener events... before connecting to new client");
1093
+ verbose_log("All event names:" + node.client.eventNames());
1094
+ verbose_log("Connection_reestablished event count:" + node.client.listenerCount("connection_reestablished"));
1095
+ node.client.removeListener("connection_reestablished", reestablish);
1096
+ verbose_log("Backoff event count:" + node.client.listenerCount("backoff"));
1097
+ node.client.removeListener("backoff", backoff);
1098
+ verbose_log("Start reconnection event count:" + node.client.listenerCount("start_reconnection"));
1099
+ node.client.removeListener("start_reconnection", reconnection);
1100
+ }
1101
+ opcuaEndpoint = {}; // Clear
1102
+ opcuaEndpoint = msg.OpcUaEndpoint; // Check all parameters!
1103
+ connectionOption.securityPolicy = opcua.SecurityPolicy[opcuaEndpoint?.securityPolicy]; // || opcua.SecurityPolicy.None;
1104
+ connectionOption.securityMode = opcua.MessageSecurityMode[opcuaEndpoint?.securityMode]; // || opcua.MessageSecurityMode.None;
1105
+ verbose_log("NEW connectionOption security parameters, policy: " + connectionOption.securityPolicy + " mode: " + connectionOption.securityMode);
1106
+ if (opcuaEndpoint.login === true) {
1107
+ userIdentity = {
1108
+ userName: opcuaEndpoint?.user,
1109
+ password: opcuaEndpoint?.password,
1110
+ type: opcua.UserTokenType.UserName
1111
+ };
1112
+ verbose_log("NEW UserIdentity: " + JSON.stringify(userIdentity));
1113
+ }
1114
+ verbose_log("Using new endpoint:" + stringify(opcuaEndpoint));
1115
+ } else {
1116
+ verbose_log("Using endpoint:" + stringify(opcuaEndpoint));
1117
+ }
1118
+ console.log("#2 Create client");
1119
+ if (!node.client) {
1120
+ create_opcua_client(connect_opcua_client);
1121
+ }
1122
+ }
1123
+
1124
+ function disconnect_action_input(msg) {
1125
+ verbose_log("Closing session...");
1126
+ if (node.session) {
1127
+ node.session.close(function (err) {
1128
+ if (err) {
1129
+ node_error("Session close error: " + err);
1130
+ }
1131
+ else {
1132
+ verbose_log("Session closed!");
1133
+ }
1134
+ });
1135
+ }
1136
+ else {
1137
+ verbose_warn("No session to close!");
1138
+ }
1139
+
1140
+ opcuaEndpoint = {}; // Clear
1141
+ opcuaEndpoint = msg.OpcUaEndpoint;
1142
+ // Now reconnect and use msg parameters
1143
+ subscription = null;
1144
+ monitoredItems.clear();
1145
+ verbose_log("Disconnecting...");
1146
+ if (node.client) {
1147
+ node.client.removeListener("connection_reestablished", reestablish);
1148
+ verbose_log("Backoff event count:" + node.client.listenerCount("backoff"));
1149
+ node.client.removeListener("backoff", backoff);
1150
+ verbose_log("Start reconnection event count:" + node.client.listenerCount("start_reconnection"));
1151
+ node.client.removeListener("start_reconnection", reconnection);
1152
+ node.client.disconnect(function () {
1153
+ verbose_log("Client disconnected!");
1154
+ node.client = null;
1155
+ set_node_status_to("disconnected");
1156
+ });
1157
+ }
1158
+ }
1159
+
1160
+ async function register_action_input(msg) {
1161
+ verbose_log("register nodes : " + msg.payload.toString());
1162
+ // First test, let´s see if this needs some refactoring. Same way perhaps as with readMultiple
1163
+ // msg.topic not used, but cannot be empty
1164
+ // msg.payload == array of nodeIds to register
1165
+ if (msg.payload.length > 0) {
1166
+ const registeredNodes = await node.session.registerNodes(msg.payload);
1167
+ verbose_log("RegisteredNodes: " + registeredNodes.toString());
1168
+ }
1169
+ else {
1170
+ verbose_warn("No items to register in the payload! Check node:" + node.name);
1171
+ }
1172
+ }
1173
+
1174
+ async function unregister_action_input(msg) {
1175
+ verbose_log("unregister nodes : " + msg.payload.toString());
1176
+ // First test, let´s see if this needs some refactoring. Same way perhaps as with readMultiple
1177
+ // msg.topic not used, but cannot be empty
1178
+ // msg.payload == array of nodeIds to register
1179
+ if (msg.payload.length > 0) {
1180
+ const unregisteredNodes = await node.session.registerNodes(msg.payload);
1181
+ verbose_log("UnregisteredNodes: " + unregisteredNodes.toString());
1182
+ }
1183
+ else {
1184
+ verbose_warn("No items to unregister in the payload! Check node:" + node.name);
1185
+ }
1186
+ }
1187
+
1188
+ async function read_action_input(msg) {
1189
+
1190
+ verbose_log("reading");
1191
+ let item = "";
1192
+ let range = null;
1193
+ if (msg.topic) {
1194
+ let n = msg.topic.indexOf("datatype=");
1195
+ if (n > 0) {
1196
+ msg.datatype = msg.topic.substring(n + 9);
1197
+ item = msg.topic.substring(0, n - 1);
1198
+ msg.topic = item;
1199
+ verbose_log(stringify(msg));
1200
+ }
1201
+ }
1202
+ let br = "";
1203
+ // browsePath enhancement, item starts with br=/Objects/3:Simulation/3:Counter
1204
+ // console.log("ITEM: " + item + " topic: " + msg.topic);
1205
+ if (item.length > 0 && item.indexOf("br=") === 0) {
1206
+ verbose_log("Finding nodeId by browsePath: " + item.substring(3));
1207
+ br = item.substring(3);
1208
+ }
1209
+ if (msg.topic.length > 0 && msg.topic.startsWith("br=")) {
1210
+ verbose_log("Finding nodeId by browsePath: " + msg.topic.substring(3));
1211
+ br = msg.topic.substring(3);
1212
+ }
1213
+ if (br.length > 0) {
1214
+ const translate = [opcua.makeBrowsePath("RootFolder", br)];
1215
+ const results = await node.session.translateBrowsePath(translate);
1216
+ if (results && results[0].statusCode === opcua.StatusCodes.Good &&
1217
+ results[0].targets[0]?.targetId) {
1218
+ msg.topic = results[0].targets[0].targetId; // replace msg.topic with the nodeId that is get by using browsePath
1219
+ verbose_log("Found browsePath => nodeId: " + msg.topic);
1220
+ }
1221
+ else {
1222
+ node_error("Cannot translate browsePathToNodeId: " + br + "/" + translate + " error: " + results);
1223
+ }
1224
+ }
1225
+
1226
+ if (item.length > 0) {
1227
+ items[0] = item;
1228
+ } else {
1229
+ items[0] = msg.topic;
1230
+ }
1231
+
1232
+ // Added support for indexRange, payload can be just one number as string "5" or "2:5"
1233
+ if (msg.payload?.range) {
1234
+ range = new opcua.NumericRange(msg.payload.range);
1235
+ }
1236
+ if (node.session) {
1237
+ // With Single Read using now read to get sourceTimeStamp and serverTimeStamp
1238
+ verbose_log("Reading value by nodeId: " + items[0]);
1239
+ try {
1240
+ node.session.read({
1241
+ nodeId: items[0],
1242
+ attributeId: 13,
1243
+ indexRange: range,
1244
+ timestampsToReturn: opcua.TimestampsToReturn.Both
1245
+ },
1246
+ function (err, dataValue, diagnostics) {
1247
+ if (err) {
1248
+ if (diagnostics) {
1249
+ verbose_log('diagnostics:' + diagnostics);
1250
+ }
1251
+ node_error(node.name + " error at active reading: " + err.message);
1252
+ set_node_error_status_to("error", err);
1253
+ // No actual error session created, this case cause connections to server
1254
+ // reset_opcua_client(connect_opcua_client);
1255
+ } else {
1256
+ set_node_status_to("active reading");
1257
+ verbose_log("Node : " + msg.topic);
1258
+ verbose_log(dataValue.toString());
1259
+ if (dataValue) {
1260
+ try {
1261
+ verbose_log("Value : " + dataValue.value.value);
1262
+ verbose_log("DataType: " + dataValue.value.dataType + " (" + DataType[dataValue.value.dataType] + ")");
1263
+ verbose_log("Message: " + msg.topic + " (" + msg.datatype + ")");
1264
+ if (dataValue.value.dataType === opcua.DataType.UInt16) {
1265
+ verbose_log("UInt16:" + dataValue.value.value + " -> Int32:" + opcuaBasics.toInt32(dataValue.value.value));
1266
+ }
1267
+
1268
+ // clone payload via JSON stringify/parse only if datavalue is an ExtensionObject or an array of ExtensionObjects
1269
+ msg.payload = dataValue.value.dataType === opcua.DataType.ExtensionObject
1270
+ ? JSON.parse(JSON.stringify((dataValue.value.value)))
1271
+ : dataValue.value.value;
1272
+
1273
+ msg.statusCode = dataValue.statusCode;
1274
+ msg.serverTimestamp = dataValue.serverTimestamp;
1275
+ msg.sourceTimestamp = dataValue.sourceTimestamp;
1276
+
1277
+ if (dataValue.statusCode && dataValue.statusCode.isGoodish() === false) {
1278
+ verbose_warn("StatusCode: " + dataValue.statusCode.toString(16) + " " + dataValue.statusCode.description);
1279
+ }
1280
+ node.send([msg, null]);
1281
+
1282
+ } catch (e) {
1283
+ if (dataValue != null) {
1284
+ node_error("Bad read: " + (dataValue.statusCode.toString(16)));
1285
+ node_error("Message:" + msg.topic + " dataType:" + msg.datatype);
1286
+ node_error("Data:" + stringify(dataValue));
1287
+ node.send([null, { error: "Bad read: " + (dataValue.statusCode.toString(16)), endpoint: `${opcuaEndpoint?.endpoint}`, status: currentStatus }, null]);
1288
+ } else {
1289
+ node_error(e.message);
1290
+ node.send([null, { error: e.message, endpoint: `${opcuaEndpoint?.endpoint}`, status: currentStatus }, null]);
1291
+ }
1292
+ }
1293
+
1294
+ }
1295
+ }
1296
+ });
1297
+ }
1298
+ catch (error) {
1299
+ node_error("Cannot read: " + error);
1300
+ }
1301
+ } else {
1302
+ set_node_status_to("invalid session");
1303
+ node_error("Session is not active!")
1304
+ }
1305
+ }
1306
+
1307
+ async function readhistory_action_input(msg) {
1308
+ verbose_log("Read historical values from msg.topic = nodeId: " + msg.topic + " msg.aggregate (raw|min|ave|max|interpolative), aggregate: " + msg.aggregate);
1309
+ let start;
1310
+ let end = Date.now();
1311
+ if (msg.end) {
1312
+ verbose_log("msg.end : " + msg.end.toString());
1313
+ end = msg.end;
1314
+ }
1315
+ if (!msg.start) {
1316
+ start = end - (1 * 60 * 60 * 1000); // read last 1 hour of history
1317
+ }
1318
+ else {
1319
+ start = msg.start;
1320
+ verbose_log("msg.start: " + msg.start.toString());
1321
+ }
1322
+ verbose_log("Start time, msg.start or default start 1h ago, start: " + new Date(start));
1323
+ verbose_log("End time, msg.end or default to now, end: " + new Date(end));
1324
+ // For aggregates
1325
+ let processingInterval = end - start; // Whole range 10 * 1000; // 10s interval
1326
+ if (node.session) {
1327
+ if (msg.aggregate && msg.aggregate === "raw") {
1328
+ let numValues = 1000;
1329
+ if (msg.numValuesPerNode) {
1330
+ numValues = parseInt(msg.numValuesPerNode);
1331
+ }
1332
+ let returnBounds = false;
1333
+ if (msg.returnBounds) {
1334
+ returnBounds = msg.returnBounds;
1335
+ }
1336
+ // Old one: ReadRawModifiedDetails
1337
+ // No constructor: ExtraReadHistoryValueParameters
1338
+ let historyReadDetails = new Object({
1339
+ isReadModified: false, // Fixed, any need to modify?
1340
+ numValuesPerNode: numValues,
1341
+ returnBounds: returnBounds,
1342
+ timestampsToReturn: opcua.TimestampsToReturn.Both // Fixed, return always both
1343
+ });
1344
+ verbose_log("NodeId: " + msg.topic + " from: " + new Date(start) + " to: " + new Date(end) + " options: " + JSON.stringify(historyReadDetails));
1345
+ let historicalReadResult = await node.session.readHistoryValue({ nodeId: msg.topic }, new Date(start), new Date(end), historyReadDetails);
1346
+ msg.payload = historicalReadResult;
1347
+ node.send([msg, null]);
1348
+
1349
+ }
1350
+ if (msg.aggregate && msg.aggregate === "max") {
1351
+ let resultMax = await node.session.readAggregateValue(
1352
+ { nodeId: msg.topic },
1353
+ new Date(start),
1354
+ new Date(end),
1355
+ opcua.AggregateFunction.Maximum,
1356
+ processingInterval);
1357
+ msg.payload = resultMax;
1358
+ node.send([msg, null, null]);
1359
+
1360
+ if (resultMax.statusCode === opcua.StatusCodes.Good) {
1361
+ verbose_log(chalk.green("History max: ") + chalk.cyan(resultMax.historyData.dataValues[0].value.value));
1362
+ }
1363
+ }
1364
+ if (msg.aggregate && msg.aggregate === "min") {
1365
+ let resultMin = await node.session.readAggregateValue(
1366
+ { nodeId: msg.topic },
1367
+ new Date(start),
1368
+ new Date(end),
1369
+ opcua.AggregateFunction.Minimum,
1370
+ processingInterval);
1371
+ msg.payload = resultMin;
1372
+ node.send([msg, null, null]);
1373
+
1374
+ if (resultMin.statusCode === opcua.StatusCodes.Good) {
1375
+ verbose_log(chalk.green("History min: ") + chalk.cyan(resultMin.historyData.dataValues[0].value.value));
1376
+ }
1377
+ }
1378
+ if (msg.aggregate && msg.aggregate === "ave") {
1379
+ let resultAve = await node.session.readAggregateValue(
1380
+ { nodeId: msg.topic },
1381
+ new Date(start),
1382
+ new Date(end),
1383
+ opcua.AggregateFunction.Average,
1384
+ processingInterval);
1385
+ msg.payload = resultAve;
1386
+ node.send([msg, null, null]);
1387
+
1388
+ if (resultAve.statusCode === opcua.StatusCodes.Good) {
1389
+ verbose_log(chalk.green("History ave: ") + chalk.cyan(resultAve.historyData.dataValues[0].value.value));
1390
+ }
1391
+ }
1392
+ if (msg.aggregate && msg.aggregate === "interpolative") {
1393
+ let resultInter = await node.session.readAggregateValue(
1394
+ { nodeId: msg.topic },
1395
+ new Date(start),
1396
+ new Date(end),
1397
+ opcua.AggregateFunction.Interpolative,
1398
+ processingInterval);
1399
+ msg.payload = resultInter;
1400
+ node.send([msg, null, null]);
1401
+
1402
+ if (resultInter.statusCode === opcua.StatusCodes.Good) {
1403
+ verbose_log(chalk.green("History interpolative: ") + chalk.cyan(resultInter.historyData.dataValues[0].value.value));
1404
+ }
1405
+
1406
+ }
1407
+ }
1408
+ }
1409
+
1410
+ function readmultiple_action_input(msg) {
1411
+
1412
+ verbose_log("read multiple...");
1413
+ let item = "";
1414
+ //
1415
+ if (msg.topic) {
1416
+ let n = msg.topic.indexOf("datatype=");
1417
+ if (n > 0) {
1418
+ msg.datatype = msg.topic.substring(n + 9);
1419
+ item = msg.topic.substring(0, n - 1);
1420
+ msg.topic = item;
1421
+ verbose_log(stringify(msg));
1422
+ }
1423
+ }
1424
+
1425
+ // Store nodeId and browseName to read multipleItems array
1426
+ if (msg.topic !== "readmultiple" && msg.topic !== "clearitems") {
1427
+ if (item.length > 0) {
1428
+ multipleItems.push({ nodeId: item, browseName: msg.browseName, attributeId: AttributeIds.Value, TimestampsToReturn: opcua.TimestampsToReturn.Both });
1429
+ } else {
1430
+ // msg.topic
1431
+ multipleItems.push({ nodeId: msg.topic, browseName: msg.browseName, attributeId: AttributeIds.Value, TimestampsToReturn: opcua.TimestampsToReturn.Both }); // support for multiple item reading
1432
+ }
1433
+ }
1434
+
1435
+ if (msg.topic === "clearitems") {
1436
+ verbose_log("clear items...");
1437
+ multipleItems = [];
1438
+ set_node_status_to("clear items");
1439
+ return;
1440
+ }
1441
+
1442
+ if (msg.topic !== "readmultiple") {
1443
+ set_node_status_to("nodeId stored");
1444
+ return;
1445
+ }
1446
+ // Read multiple, payload contains all nodeIds that will be read
1447
+ if (node.session && msg.topic === "readmultiple" && Array.isArray(msg.payload)) {
1448
+ multipleItems = []; // Clear as payload contains always nodeIds that we want to read
1449
+ let i = 0;
1450
+ while (i < msg.payload.length) {
1451
+ multipleItems.push({ nodeId: msg.payload[i], attributeId: AttributeIds.Value, TimestampsToReturn: opcua.TimestampsToReturn.Both });
1452
+ i = i + 1;
1453
+ }
1454
+ }
1455
+
1456
+ if (node.session && msg.topic === "readmultiple") {
1457
+ // node.session.read({timestampsToReturn: TimestampsToReturn.Both, nodesToRead: multipleItems}, function (err, dataValues, diagnostics) {
1458
+ verbose_log("Reading items: " + stringify(multipleItems));
1459
+ if (multipleItems.length === 0) {
1460
+ node_error(node.name + " no items to read");
1461
+ set_node_status_to("no items to read");
1462
+ return;
1463
+ }
1464
+ node.session.read(multipleItems,
1465
+ function (err, dataValues, diagnostics) {
1466
+ if (err) {
1467
+ if (diagnostics) {
1468
+ verbose_log('diagnostics:' + diagnostics);
1469
+ }
1470
+ node_error(node.name + " error at active reading: " + err.message);
1471
+ set_node_error_status_to("error", err);
1472
+ // No actual error session existing, this case cause connections to server
1473
+ // reset_opcua_client(connect_opcua_client);
1474
+ }
1475
+ else {
1476
+ set_node_status_to("active multiple reading");
1477
+
1478
+ if (msg.payload === "ALL") {
1479
+ msg.payload = dataValues;
1480
+ msg.items = multipleItems;
1481
+ node.send([msg, null, null]);// node.send([{ "topic": "ALL", "payload": dataValues, "items": multipleItems }, null, null]);
1482
+ return;
1483
+ }
1484
+
1485
+ for (let i = 0; i < dataValues.length; i++) {
1486
+ let dataValue = dataValues[i];
1487
+ verbose_log("Node : " + msg.topic);
1488
+ verbose_log(dataValue.toString());
1489
+ if (dataValue) {
1490
+ try {
1491
+ verbose_log("Value : " + dataValue.value.value);
1492
+ verbose_log("DataType: " + dataValue.value.dataType + " (" + DataType[dataValue.value.dataType] + ")");
1493
+ if (dataValue.value.dataType === opcua.DataType.UInt16) {
1494
+ verbose_log("UInt16:" + dataValue.value.value + " -> Int32:" + opcuaBasics.toInt32(dataValue.value.value));
1495
+ }
1496
+ if (dataValue.statusCode && dataValue.statusCode.isGoodish() === false) {
1497
+ verbose_warn("StatusCode: " + dataValue.statusCode.toString(16) + " " + dataValue.statusCode.description);
1498
+ }
1499
+ let serverTs = dataValue.serverTimestamp;
1500
+ let sourceTs = dataValue.sourceTimestamp;
1501
+ if (serverTs === null) {
1502
+ serverTs = new Date();
1503
+ }
1504
+ if (sourceTs === null) {
1505
+ sourceTs = new Date();
1506
+ }
1507
+
1508
+ let value = dataValue.value.dataType === opcua.DataType.ExtensionObject
1509
+ ? JSON.parse(JSON.stringify(dataValue.value.value))
1510
+ : dataValue.value.value;
1511
+
1512
+ // Use nodeId in topic, arrays are same length
1513
+ // Output pin 1 for each value by value
1514
+ // Clone msg per item: Node-RED passes the first node.send()
1515
+ // by reference, so mutating the shared msg in this loop
1516
+ // corrupts the per-item topic of already-sent messages.
1517
+ let outMsg = RED.util.cloneMessage(msg);
1518
+ outMsg.topic = multipleItems[i].nodeId.toString();
1519
+ outMsg.payload = value;
1520
+ outMsg.statusCode = dataValue.statusCode;
1521
+ outMsg.serverTimestamp = serverTs;
1522
+ outMsg.sourceTimestamp = sourceTs;
1523
+ node.send([outMsg, null, null]);
1524
+ } catch (e) {
1525
+ if (dataValue != null) {
1526
+ node_error("Bad read, statusCode: " + (dataValue.statusCode.toString(16)));
1527
+ node_error("Data:" + stringify(dataValue));
1528
+ } else {
1529
+ node_error(e.message);
1530
+ // Output pin 2 for errors
1531
+ // verbose_log("Output pin2, error: " + e.message);
1532
+ node.send([msg, { error: e.message, endpoint: `${opcuaEndpoint?.endpoint}`, status: currentStatus }, null]);
1533
+ return;
1534
+ }
1535
+ }
1536
+ }
1537
+ }
1538
+ // Send all values in one msg to output 3
1539
+ // verbose_log("Output pin3, topic: " + multipleItems + " payload: " + dataValues);
1540
+ msg.topic = multipleItems;
1541
+ msg.payload = dataValues;
1542
+ node.send([null, null, msg]); // node.send([null, null, {topic: multipleItems, payload: dataValues}]);
1543
+ }
1544
+ });
1545
+ } else {
1546
+ set_node_status_to("invalid session");
1547
+ node_error("Session is not active!")
1548
+ }
1549
+ }
1550
+
1551
+ function info_action_input(msg) {
1552
+ verbose_log("meta-data reading");
1553
+ let item = "";
1554
+ if (msg.topic) {
1555
+ let n = msg.topic.indexOf("datatype=");
1556
+
1557
+ if (n > 0) {
1558
+ msg.datatype = msg.topic.substring(n + 9);
1559
+ item = msg.topic.substring(0, n - 1);
1560
+ msg.topic = item;
1561
+ verbose_log(stringify(msg));
1562
+ }
1563
+ }
1564
+
1565
+ if (item.length > 0) {
1566
+ items[0] = item;
1567
+ } else {
1568
+ items[0] = msg.topic;
1569
+ }
1570
+
1571
+ if (node.session) {
1572
+ // TODO this could loop through all items
1573
+ node.session.readAllAttributes(opcua.coerceNodeId(items[0]), function (err, result) {
1574
+ if (!err) {
1575
+ let newMsg = Object.assign(msg, result);
1576
+ node.send([newMsg, null, null]);
1577
+ }
1578
+ else {
1579
+ set_node_status_to("error");
1580
+ node_error("Cannot read attributes from nodeId: " + items[0])
1581
+ }
1582
+ });
1583
+ } else {
1584
+ set_node_status_to("invalid session");
1585
+ node_error("Session is not active!")
1586
+ }
1587
+ }
1588
+
1589
+ async function build_extension_object_action_input(msg) {
1590
+ verbose_log("Construct ExtensionObject from " + JSON.stringify(msg));
1591
+ let item = "";
1592
+ if (msg.topic) {
1593
+ let n = msg.topic.indexOf("datatype=");
1594
+
1595
+ if (n > 0) {
1596
+ msg.datatype = msg.topic.substring(n + 9);
1597
+ item = msg.topic.substring(0, n - 1);
1598
+ msg.topic = item;
1599
+ verbose_log(stringify(msg));
1600
+ }
1601
+ }
1602
+
1603
+ if (item.length > 0) {
1604
+ items[0] = item;
1605
+ } else {
1606
+ items[0] = msg.topic;
1607
+ }
1608
+
1609
+ if (node.session) {
1610
+ try {
1611
+ const ExtensionNodeId = opcua.coerceNodeId(items[0]);
1612
+ verbose_log("ExtensionNodeId: " + ExtensionNodeId);
1613
+ const ExtensionTypeDefinition = await node.session.read({ nodeId: ExtensionNodeId, attributeId: opcua.AttributeIds.DataTypeDefinition });
1614
+ if (ExtensionTypeDefinition.statusCode != opcua.StatusCodes.Good) {
1615
+ node_error("Failed to find extension type for nodeId: " + ExtensionNodeId + " error: " + ExtensionTypeDefinition.statusCode.description);
1616
+ return;
1617
+ }
1618
+ else {
1619
+ verbose_log("ExtensionType: " + JSON.stringify(ExtensionTypeDefinition));
1620
+ }
1621
+ let newmsg = {};
1622
+ const ExtensionData = await node.session.constructExtensionObject(ExtensionNodeId, {});
1623
+ if (ExtensionData) {
1624
+ verbose_log("ExtensionData: " + ExtensionData.toString());
1625
+ }
1626
+ // Simplified
1627
+ newmsg.topic = msg.topic;
1628
+ newmsg.payload = JSON.parse(JSON.stringify(ExtensionData)); // JSON.stringify(ExtensionData); // New value with default values
1629
+ verbose_log("Extension Object msg: " + stringify(newmsg))
1630
+ node.send([newmsg, null, null]);
1631
+ }
1632
+ catch (err) {
1633
+ if (err) {
1634
+ node_error("Failed to build ExtensionObject, error: " + err);
1635
+ set_node_status_to("extensionObject error");
1636
+ }
1637
+ }
1638
+ } else {
1639
+ set_node_status_to("invalid session");
1640
+ node_error("Session is not active!")
1641
+
1642
+ node.send([null, { error: "Session is not active!", endpoint: `${opcuaEndpoint?.endpoint}`, status: currentStatus }, null]);
1643
+
1644
+ }
1645
+ }
1646
+
1647
+ async function write_action_input(msg) {
1648
+ if (msg?.topic && msg.topic.indexOf("ns=") != 0) return; // NOT an item
1649
+
1650
+ // Topic value: ns=2;s=1:PST-007-Alarm-Level@Training?SETPOINT
1651
+ let dIndex = msg.topic.indexOf("datatype=");
1652
+ let range = null;
1653
+
1654
+ if (msg.datatype == null && dIndex > 0) {
1655
+ msg.datatype = msg.topic.substring(dIndex + 9);
1656
+ }
1657
+
1658
+ let nodeid;
1659
+ verbose_log(opcua.makeBrowsePath(msg.topic, "."));
1660
+ // TODO ns=10 TEST 2 digits namespace
1661
+ if (dIndex > 0) {
1662
+ nodeid = opcua.coerceNodeId(msg.topic.substring(0, dIndex - 1));
1663
+ }
1664
+ else {
1665
+ nodeid = opcua.coerceNodeId(msg.topic);
1666
+ }
1667
+
1668
+ // Less output
1669
+ // verbose_log("namespace=" + ns);
1670
+ // verbose_log("string=" + s);
1671
+ // verbose_log("NodeId= " + nodeid.toString + " type=" + msg.datatype);
1672
+
1673
+ let opcuaDataValue = msg.datatype && msg.datatype.indexOf("ExtensionObject") >= 0 && node.session
1674
+ ? await build_new_extensionObject_dataValue(msg.datatype, msg.topic, msg.payload, node.session)
1675
+ : opcuaBasics.build_new_dataValue(msg.datatype, msg.payload);
1676
+
1677
+ async function build_new_extensionObject_dataValue(datatype, topic, payload, session) {
1678
+ let defaultExtensionObject = null;
1679
+
1680
+ if (topic.indexOf("typeId=") > 0) {
1681
+ let typeId = topic.substring(topic.indexOf("typeId=") + 7);
1682
+ verbose_log("ExtensionObject TypeId= " + typeId);
1683
+ defaultExtensionObject = await session.constructExtensionObject(opcua.coerceNodeId(typeId), {}); // Create first with default values
1684
+ verbose_log("ExtensionObject=" + stringify(defaultExtensionObject));
1685
+ }
1686
+
1687
+ let nValue = null;
1688
+
1689
+ if (datatype.indexOf("Array") > 0) {
1690
+ // datatype is array of extension object
1691
+ payload.value.forEach(function (extensionObject, index) {
1692
+ // deep clone default extension object
1693
+ let duplicatedDefaultExtensionObject = cloneDeep(defaultExtensionObject);
1694
+ payload.value[index] = Object.assign(duplicatedDefaultExtensionObject, extensionObject);
1695
+ });
1696
+
1697
+ nValue = {
1698
+ dataType: opcua.DataType.ExtensionObject,
1699
+ value: payload.value,
1700
+ arrayType: opcua.VariantArrayType.Array
1701
+ };
1702
+ } else {
1703
+ // datatype is extension object
1704
+ verbose_log("payload: " + stringify(payload));
1705
+ let extensionObject;
1706
+ if (!defaultExtensionObject) {
1707
+ extensionObject = payload; // use payload directly, payload must be ExtensionObject ready/like not a JSON object
1708
+ }
1709
+ else {
1710
+ extensionObject = Object.assign(defaultExtensionObject, payload); // MERGE payload over default values
1711
+ }
1712
+ /* TEST
1713
+ nValue = {
1714
+ dataType: opcua.DataType.ExtensionObject,
1715
+ value: extensionObject,
1716
+ arrayType: opcua.VariantArrayType.Array // Quick FIX
1717
+ };
1718
+ console.log("New nValue: " + JSON.stringify(nValue));
1719
+ */
1720
+ if (extensionObject && extensionObject.length !== undefined) {
1721
+ nValue = {
1722
+ dataType: opcua.DataType.ExtensionObject,
1723
+ value: extensionObject,
1724
+ arrayType: opcua.VariantArrayType.Array // Quick FIX
1725
+ };
1726
+ // console.log("New nValue: " + JSON.stringify(nValue));
1727
+ }
1728
+ else {
1729
+ nValue = {
1730
+ dataType: opcua.DataType.ExtensionObject,
1731
+ value: extensionObject
1732
+ };
1733
+ }
1734
+ }
1735
+
1736
+ return nValue
1737
+ }
1738
+
1739
+ // TODO Fix object array according range
1740
+ // Added support for indexRange, payload can be just one number as string "5" or "2:5"
1741
+
1742
+ // Helper for node-red server write
1743
+ if (msg.range) {
1744
+ verbose_log(chalk.red("Range: " + msg.range));
1745
+ range = new opcua.NumericRange(msg.range);
1746
+ verbose_log(chalk.red("Range: " + JSON.stringify(range) + " values: " + JSON.stringify(opcuaDataValue)));
1747
+ // TODO write to node-red server still work to do
1748
+ }
1749
+
1750
+ let nodeToWrite;
1751
+ if (node.session && !node.session.isReconnecting && node.session.isChannelValid()) {
1752
+ if (range) {
1753
+ nodeToWrite = {
1754
+ nodeId: nodeid.toString(),
1755
+ attributeId: opcua.AttributeIds.Value,
1756
+ indexRange: range,
1757
+ value: new opcua.DataValue({ value: new opcua.Variant(opcuaDataValue) })
1758
+ };
1759
+ }
1760
+ else {
1761
+ // console.log("Writing datatype: " + opcuaDataValue.dataType + " value: " + opcuaDataValue.value);
1762
+ // Added overflow/underflow check Int32, Int16, UInt64, UInt32, UInt16, Byte, SByte
1763
+ if (opcuaDataValue.dataType === opcua.DataType.Int32) {
1764
+ if (opcuaDataValue.value > 2147483647) {
1765
+ // See note below, some servers will NOT accept statusCode write
1766
+ set_node_error_status_to("error", "overflow");
1767
+ node_error(node.name + " Cannot write value " + opcuaDataValue.value + ") to nodeId:" + nodeid.toString() + " error: overflow Writing max Int32");
1768
+ opcuaDataValue.value = 2147483647;
1769
+ }
1770
+ if (opcuaDataValue.value < -2147483648) {
1771
+ set_node_error_status_to("error", "underflow");
1772
+ node_error(node.name + " Cannot write value " + opcuaDataValue.value + ") to nodeId:" + nodeid.toString() + " error: underflow Writing min Int32");
1773
+ opcuaDataValue.value = -2147483648;
1774
+ }
1775
+ }
1776
+ if (opcuaDataValue.dataType === opcua.DataType.Int16) {
1777
+ if (opcuaDataValue.value > 32767) {
1778
+ // See note below, some servers will NOT accept statusCode write
1779
+ set_node_error_status_to("error", "overflow");
1780
+ node_error(node.name + " Cannot write value " + opcuaDataValue.value + ") to nodeId:" + nodeid.toString() + " error: overflow Writing max Int16");
1781
+ opcuaDataValue.value = 32767;
1782
+ }
1783
+ if (opcuaDataValue.value < -32768) {
1784
+ set_node_error_status_to("error", "underflow");
1785
+ node_error(node.name + " Cannot write value " + opcuaDataValue.value + ") to nodeId:" + nodeid.toString() + " error: underflow Writing min Int16");
1786
+ opcuaDataValue.value = -32768;
1787
+ }
1788
+ }
1789
+ if (opcuaDataValue.dataType === opcua.DataType.UInt64) {
1790
+ if (Array.isArray(opcuaDataValue.value)){ //64 bit ints are sometimes passed as arrays of 32 bit ints
1791
+ if(!isNaN(opcuaDataValue.value[0]) && !isNaN(opcuaDataValue.value[1])){
1792
+ opcuaDataValue.value = opcuaDataValue.value[0] * (2**32) + opcuaDataValue.value[1];
1793
+ } else if(!isNaN(opcuaDataValue.value[0])){
1794
+ opcuaDataValue.value = opcuaDataValue.value[0];
1795
+ } else if(!isNaN(opcuaDataValue.value[1])){
1796
+ opcuaDataValue.value = opcuaDataValue.value[1];
1797
+ } else {
1798
+ node.warn("Input to UInt64 write is array of null values")
1799
+ }
1800
+ }
1801
+ if (opcuaDataValue.value > 2**64 - 1) {
1802
+ // See note below, some servers will NOT accept statusCode write
1803
+ set_node_error_status_to("error", "overflow");
1804
+ node_error(node.name + " Cannot write value " + opcuaDataValue.value + ") to nodeId:" + nodeid.toString() + " error: overflow Writing max Int32");
1805
+ opcuaDataValue.value = 2**64 - 1;
1806
+ }
1807
+ if (opcuaDataValue.value < 0) {
1808
+ set_node_error_status_to("error", "underflow");
1809
+ node_error(node.name + " Cannot write value " + opcuaDataValue.value + ") to nodeId:" + nodeid.toString() + " error: underflow Writing min Int32");
1810
+ opcuaDataValue.value = 0;
1811
+ }
1812
+ }
1813
+ if (opcuaDataValue.dataType === opcua.DataType.UInt32) {
1814
+ if (opcuaDataValue.value > 4294967295) {
1815
+ // See note below, some servers will NOT accept statusCode write
1816
+ set_node_error_status_to("error", "overflow");
1817
+ node_error(node.name + " Cannot write value " + opcuaDataValue.value + ") to nodeId:" + nodeid.toString() + " error: overflow Writing max UInt32");
1818
+ opcuaDataValue.value = 4294967295;
1819
+ }
1820
+ if (opcuaDataValue.value < 0) {
1821
+ set_node_error_status_to("error", "underflow");
1822
+ node_error(node.name + " Cannot write value " + opcuaDataValue.value + ") to nodeId:" + nodeid.toString() + " error: underflow Writing min UInt32");
1823
+ opcuaDataValue.value = 0;
1824
+ }
1825
+ }
1826
+ if (opcuaDataValue.dataType === opcua.DataType.UInt16) {
1827
+ if (opcuaDataValue.value > 65535) {
1828
+ // See note below, some servers will NOT accept statusCode write
1829
+ set_node_error_status_to("error", "overflow");
1830
+ node_error(node.name + " Cannot write value " + opcuaDataValue.value + ") to nodeId:" + nodeid.toString() + " error: overflow Writing max UInt16");
1831
+ opcuaDataValue.value = 65535;
1832
+ }
1833
+ if (opcuaDataValue.value < 0) {
1834
+ set_node_error_status_to("error", "underflow");
1835
+ node_error(node.name + " Cannot write value " + opcuaDataValue.value + ") to nodeId:" + nodeid.toString() + " error: underflow Writing min UInt16");
1836
+ opcuaDataValue.value = 0;
1837
+ }
1838
+ }
1839
+ if (opcuaDataValue.dataType === opcua.DataType.Byte) {
1840
+ if (opcuaDataValue.value > 255) {
1841
+ // See note below, some servers will NOT accept statusCode write
1842
+ set_node_error_status_to("error", "overflow");
1843
+ node_error(node.name + " Cannot write value " + opcuaDataValue.value + ") to nodeId:" + nodeid.toString() + " error: overflow Writing max Byte");
1844
+ opcuaDataValue.value = 255;
1845
+ }
1846
+ if (opcuaDataValue.value < 0) {
1847
+ set_node_error_status_to("error", "underflow");
1848
+ node_error(node.name + " Cannot write value " + opcuaDataValue.value + ") to nodeId:" + nodeid.toString() + " error: underflow Writing min Byte");
1849
+ opcuaDataValue.value = 0;
1850
+ }
1851
+ }
1852
+ if (opcuaDataValue.dataType === opcua.DataType.SByte) {
1853
+ if (opcuaDataValue.value > 127) {
1854
+ // See note below, some servers will NOT accept statusCode write
1855
+ set_node_error_status_to("error", "overflow");
1856
+ node_error(node.name + " Cannot write value " + opcuaDataValue.value + ") to nodeId:" + nodeid.toString() + " error: overflow Writing max SByte");
1857
+ opcuaDataValue.value = 127;
1858
+ }
1859
+ if (opcuaDataValue.value < -128) {
1860
+ set_node_error_status_to("error", "underflow");
1861
+ node_error(node.name + " Cannot write value " + opcuaDataValue.value + ") to nodeId:" + nodeid.toString() + " error: underflow Writing min SByte");
1862
+ opcuaDataValue.value = -128;
1863
+ }
1864
+ }
1865
+
1866
+ nodeToWrite = {
1867
+ nodeId: nodeid.toString(),
1868
+ attributeId: opcua.AttributeIds.Value,
1869
+ value: new opcua.DataValue({
1870
+ value: new opcua.Variant(opcuaDataValue),
1871
+ // sourceTimestamp: new Date(), // NOTE: Some servers do NOT accept time writing
1872
+ // statusCode: opcua.StatusCodes.Good // NOTE: Same with status writing, NOT accepted always
1873
+ })
1874
+ };
1875
+ }
1876
+ if (node.setstatusandtime && msg.timestamp) {
1877
+ verbose_log("NEW sourceTimestamp: " + new Date(msg.timestamp).toISOString());
1878
+ nodeToWrite.value.sourceTimestamp = new Date(msg.timestamp).getTime();
1879
+ }
1880
+ if (node.setstatusandtime && msg.sourceTimestamp) {
1881
+ verbose_log("NEW sourceTimestamp: " + new Date(msg.sourceTimestamp).toISOString());
1882
+ nodeToWrite.value.sourceTimestamp = new Date(msg.sourceTimestamp).getTime();
1883
+ }
1884
+ if (node.setstatusandtime && msg.statusCode) {
1885
+ verbose_log("NEW statusCode: " + opcua.StatusCode.makeStatusCode(msg.statusCode));
1886
+ nodeToWrite.value.statusCode = msg.statusCode;
1887
+ }
1888
+ verbose_log("VALUE TO WRITE: " + JSON.stringify(nodeToWrite));
1889
+ set_node_status_to("writing");
1890
+ try {
1891
+ node.session.write(nodeToWrite, function (err, statusCode) {
1892
+ if (err) {
1893
+ set_node_error_status_to("error", err);
1894
+ node_error(node.name + " Cannot write value (" + stringify(msg.payload) + ") to msg.topic:" + msg.topic + " error:" + err);
1895
+ // No actual error session existing, this case cause connections to server
1896
+ // reset_opcua_client(connect_opcua_client);
1897
+ msg.payload = err;
1898
+ node.send([msg, null]);
1899
+ } else {
1900
+ set_node_status_to("value written");
1901
+ verbose_log("Value written! Result:" + statusCode + " " + statusCode.description);
1902
+ if (statusCode.isGoodish() === false) {
1903
+ verbose_warn("StatusCode: " + statusCode.toString(16) + " " + statusCode.description);
1904
+ set_node_error_status_to("error", statusCode.description);
1905
+ }
1906
+ msg.payload = statusCode;
1907
+ node.send([msg, null, null]);
1908
+ }
1909
+ });
1910
+ }
1911
+ catch(error) {
1912
+ set_node_status_to("error");
1913
+ node_error("Write failed, error: " + error);
1914
+ }
1915
+ } else {
1916
+ set_node_status_to("invalid session");
1917
+ node_error("Session is not active!")
1918
+ }
1919
+ }
1920
+
1921
+ async function writemultiple_action_input(msg) {
1922
+ verbose_log("writing multiple");
1923
+ // Store as with readmultiple item
1924
+ if (msg.topic && msg.topic !== "writemultiple" && !Array.isArray(msg.payload)) {
1925
+ // Topic value: ns=2;s=1:PST-007-Alarm-Level@Training?SETPOINT
1926
+ let dIndex = msg.topic.indexOf("datatype=");
1927
+
1928
+ if (msg.datatype == null && dIndex > 0) {
1929
+ msg.datatype = msg.topic.substring(dIndex + 9);
1930
+ }
1931
+
1932
+ // Store nodeId to read multipleItems array
1933
+ if (msg.topic !== "writemultiple" && msg.topic !== "clearitems") {
1934
+ let opcuaDataValue = opcuaBasics.build_new_dataValue(msg.datatype, msg.payload);
1935
+ let item = {
1936
+ nodeId: msg.topic,
1937
+ datatype: msg.datatype,
1938
+ attributeId: opcua.AttributeIds.Value,
1939
+ indexRange: null,
1940
+ value: new opcua.DataValue({ value: opcuaDataValue })
1941
+ };
1942
+ item.value.sourceTimestamp = new Date(msg.timestamp).getTime();
1943
+ verbose_log("ITEM: " + stringify(item));
1944
+ writeMultipleItems.push(item);
1945
+ }
1946
+
1947
+ if (msg.topic === "clearitems") {
1948
+ verbose_log("clear items...");
1949
+ writeMultipleItems = [];
1950
+ set_node_status_to("clear items");
1951
+ return;
1952
+ }
1953
+ if (msg.topic !== "writemultiple") {
1954
+ set_node_status_to("nodeId stored");
1955
+ return;
1956
+ }
1957
+ }
1958
+ if (node.session && !node.session.isReconnecting && node.session.isChannelValid() && msg.topic === "writemultiple") {
1959
+ if (Array.isArray(msg.payload)) {
1960
+ writeMultipleItems = msg.payload; // expecting payload format is correct
1961
+ }
1962
+ verbose_log("Writing items: " + stringify(writeMultipleItems));
1963
+ if (writeMultipleItems.length === 0) {
1964
+ node_error(node.name + " no items to write");
1965
+ set_node_status_to("no items to write");
1966
+ return;
1967
+ }
1968
+ await node.session.write(writeMultipleItems, function (err, statusCode) {
1969
+ if (err) {
1970
+ set_node_error_status_to("error", err);
1971
+ node_error(node.name + " Cannot write values (" + msg.payload + ") to msg.topic:" + msg.topic + " error:" + err);
1972
+ node.send([{ payload: err }, { error: `${err}`, endpoint: `${opcuaEndpoint?.endpoint}`, status: currentStatus }, null]);
1973
+
1974
+ } else {
1975
+ set_node_status_to("active writing");
1976
+ verbose_log("Values written!");
1977
+ node.send([{ payload: statusCode }, null, null]);
1978
+ set_node_status_to("values written");
1979
+ return; // Do not try to run old way
1980
+ }
1981
+ });
1982
+ return;
1983
+ }
1984
+ else {
1985
+ if (!node.session || node.session.isReconnecting || !node.session.isChannelValid()) {
1986
+ set_node_status_to("invalid session");
1987
+ node_error("Write multiple items session is not active!")
1988
+ }
1989
+ }
1990
+
1991
+ // OLD original way to use payload
1992
+ let nodesToWrite; // Define here so it can write array of values
1993
+ if (node.session && !node.session.isReconnecting && node.session.isChannelValid()) {
1994
+ if (Array.isArray(msg.payload)) {
1995
+ nodesToWrite = msg.payload.map(function (msgToWrite) {
1996
+ let opcuaDataValue = opcuaBasics.build_new_dataValue(msgToWrite.datatype || msg.datatype, msgToWrite.value);
1997
+ const nodeToWrite = {
1998
+ nodeId: msgToWrite.nodeId || (node && node.toString()),
1999
+ attributeId: opcua.AttributeIds.Value,
2000
+ indexRange: null,
2001
+ value: new opcua.DataValue({ value: opcuaDataValue })
2002
+ };
2003
+ if (msgToWrite.timestamp || msg.timestamp) {
2004
+ nodeToWrite.value.sourceTimestamp = new Date(msgToWrite.timestamp || msg.timestamp).getTime();
2005
+ }
2006
+ return nodeToWrite;
2007
+ });
2008
+ verbose_log("Writing nodes with values:" + stringify(nodesToWrite));
2009
+
2010
+ await node.session.write(nodesToWrite, function (err, statusCode) {
2011
+ if (err) {
2012
+ set_node_error_status_to("error", err);
2013
+ node_error(node.name + " Cannot write values (" + msg.payload + ") to msg.topic:" + msg.topic + " error:" + err);
2014
+ node.send([{ payload: err }, { error: `${err}`, endpoint: `${opcuaEndpoint?.endpoint}`, status: currentStatus }, null]);
2015
+ } else {
2016
+ set_node_status_to("active writing");
2017
+ verbose_log("Values written!");
2018
+ node.send([{ payload: statusCode }, null, null]);
2019
+ set_node_status_to("values written");
2020
+ }
2021
+ });
2022
+ }
2023
+ } else {
2024
+ set_node_status_to("invalid session");
2025
+ node_error("Write multiple as array session is not active!")
2026
+ }
2027
+ }
2028
+
2029
+ function subscribe_action_input(msg) {
2030
+ verbose_log("subscribing");
2031
+ if (!subscription) {
2032
+ // first build and start subscription and subscribe on its started event by callback
2033
+ let timeMilliseconds = opcuaBasics.calc_milliseconds_by_time_and_unit(node.time, node.timeUnit);
2034
+ if (msg?.interval) {
2035
+ timeMilliseconds = parseInt(msg.interval); // Use this instead of node.time and node.timeUnit
2036
+ }
2037
+ verbose_log("Using subscription with publish interval: " + timeMilliseconds);
2038
+ subscription = make_subscription(subscribe_monitoredItem, msg, opcuaBasics.getSubscriptionParameters(timeMilliseconds));
2039
+ let message = { "topic": "subscriptionId", "payload": subscription.subscriptionId };
2040
+ // node.send(message); // Make it possible to store
2041
+ node.send([message, null, null]);
2042
+ } else if (subscription.subscriptionId != "terminated") {
2043
+ // otherwise check if its terminated start to renew the subscription
2044
+ set_node_status_to("active subscribing");
2045
+ subscribe_monitoredItem(subscription, msg);
2046
+ } else {
2047
+ monitoredItems.clear();
2048
+ set_node_status_to("terminated");
2049
+ // No actual error session existing, this case cause connections to server
2050
+ }
2051
+ }
2052
+
2053
+ async function monitor_action_input(msg) {
2054
+ verbose_log("monitoring");
2055
+ if (!subscription) {
2056
+ // first build and start subscription and subscribe on its started event by callback
2057
+ let timeMilliseconds = opcuaBasics.calc_milliseconds_by_time_and_unit(node.time, node.timeUnit);
2058
+ subscription = make_subscription(monitor_monitoredItem, msg, opcuaBasics.getSubscriptionParameters(timeMilliseconds));
2059
+ } else if (subscription.subscriptionId != "terminated") {
2060
+ // otherwise check if its terminated start to renew the subscription
2061
+ set_node_status_to("active monitoring");
2062
+ await monitor_monitoredItem(subscription, msg);
2063
+ } else {
2064
+ subscription = null;
2065
+ monitoredItems.clear();
2066
+ set_node_status_to("terminated");
2067
+ // No actual error session created, this case cause connections to server
2068
+ }
2069
+ }
2070
+
2071
+ function unsubscribe_action_input(msg) {
2072
+ verbose_log("unsubscribing");
2073
+ if (!subscription) {
2074
+ // first build and start subscription and subscribe on its started event by callback
2075
+ verbose_warn("Cannot unscubscribe, no subscription");
2076
+ } else if (subscription.subscriptionId != "terminated") {
2077
+ // otherwise check if its terminated start to renew the subscription
2078
+ set_node_status_to("unsubscribing");
2079
+ unsubscribe_monitoredItem(subscription, msg); // Call to terminate monitoredItem
2080
+ } else {
2081
+ subscription = null;
2082
+ monitoredItems.clear();
2083
+ set_node_status_to("terminated");
2084
+ // No actual error session exists, this case cause connections to server
2085
+ }
2086
+ }
2087
+
2088
+ function convertAndCheckInterval(interval) {
2089
+ let n = Number(interval);
2090
+ if (isNaN(n)) {
2091
+ n = 100;
2092
+ }
2093
+ return n;
2094
+ }
2095
+
2096
+ async function subscribe_monitoredItem(subscription, msg) {
2097
+ verbose_log("Session subscriptionId: " + subscription.subscriptionId);
2098
+
2099
+ // Simplified
2100
+ if (msg.topic === "multiple") {
2101
+ verbose_log("Create monitored itemGroup for " + JSON.stringify(msg.payload));
2102
+ let interval = opcuaBasics.calc_milliseconds_by_time_and_unit(node.time, node.timeUnit);
2103
+ if (msg?.interval) {
2104
+ interval = parseInt(msg.interval);
2105
+ }
2106
+ const monitorItems = [];
2107
+ for (const element of msg.payload) {
2108
+ monitorItems.push({ attributeId: AttributeIds.Value, nodeId: element.nodeId });
2109
+ }
2110
+ verbose_log("Using samplingInterval:" + interval);
2111
+ const monitoringParameters = {
2112
+ // clientHandle?: UInt32;
2113
+ samplingInterval: interval, // read from msg.interval
2114
+ // filter?: (ExtensionObject | null);
2115
+ queueSize: 1,
2116
+ discardOldest: true
2117
+ };
2118
+ verbose_log("MONITOR ITEMS: " + JSON.stringify(monitorItems));
2119
+ const group = await subscription.monitorItems(monitorItems, monitoringParameters, TimestampsToReturn.Both);
2120
+ group.on("initialized", async () => {
2121
+ verbose_log(chalk.green("Initialized monitoredItemsGroup !"));
2122
+ });
2123
+ group.on("changed", (_monitoredItem, dataValue, index) => {
2124
+ verbose_log("Group change on item, index: " + index + " item: " + monitorItems[index].nodeId + " value: " + dataValue.value.value);
2125
+
2126
+ const nodeId = monitorItems[index].nodeId.toString();
2127
+
2128
+ let value = dataValue.value.dataType === opcua.DataType.ExtensionObject
2129
+ ? JSON.parse(JSON.stringify(dataValue))
2130
+ : dataValue;
2131
+
2132
+ if (nodeId) {
2133
+ let msg = {};
2134
+ msg.topic = nodeId;
2135
+ msg.payload = value; // if users want to get dataValue.value.value example contains function node
2136
+ node.send([msg, null, null]);
2137
+
2138
+ }
2139
+ });
2140
+ return;
2141
+ }
2142
+
2143
+ let nodeStr = msg.topic;
2144
+ if (msg?.topic) {
2145
+ if (nodeStr && nodeStr.length > 1) {
2146
+ let dTypeIndex = nodeStr.indexOf(";datatype=");
2147
+ if (dTypeIndex > 0) {
2148
+ nodeStr = nodeStr.substring(0, dTypeIndex);
2149
+ }
2150
+ }
2151
+ }
2152
+ let monitoredItem = monitoredItems.get(msg.topic);
2153
+
2154
+ if (!monitoredItem) {
2155
+ verbose_log("Msg " + stringify(msg));
2156
+ // Set as default if no payload
2157
+ let queueSize = 10;
2158
+ let interval = opcuaBasics.calc_milliseconds_by_time_and_unit(node.time, node.timeUnit); // Use value given at client node
2159
+ // Interval from the payload (old existing feature still supported), but do not accept timestamp, it is too big
2160
+ if (msg.payload && parseInt(msg.payload) > 100 && parseInt(msg.payload) < 1608935031227) {
2161
+ interval = convertAndCheckInterval(msg.payload);
2162
+ }
2163
+ if (msg.interval && parseInt(msg.interval) > 100) {
2164
+ interval = convertAndCheckInterval(msg.interval);
2165
+ }
2166
+ if (msg.queueSize && parseInt(msg.queueSize) > 0) {
2167
+ queueSize = msg.queueSize;
2168
+ }
2169
+
2170
+ verbose_log(msg.topic + " samplingInterval " + interval + " queueSize " + queueSize);
2171
+ verbose_log("Monitoring value: " + msg.topic + ' by interval of ' + interval.toString() + " ms");
2172
+
2173
+ // Validate nodeId
2174
+ try {
2175
+ let nodeId = opcua.coerceNodeId(nodeStr);
2176
+ if (nodeId?.isEmpty()) {
2177
+ node_error(" Invalid empty node in getObject");
2178
+ }
2179
+ //makeNodeId(nodeStr); // above is enough
2180
+ } catch (err) {
2181
+ node_error(err);
2182
+ return;
2183
+ }
2184
+
2185
+ try {
2186
+ monitoredItem = opcua.ClientMonitoredItem.create(subscription, {
2187
+ nodeId: nodeStr,
2188
+ attributeId: opcua.AttributeIds.Value
2189
+ }, {
2190
+ samplingInterval: interval,
2191
+ queueSize: queueSize,
2192
+ discardOldest: true
2193
+ },
2194
+ TimestampsToReturn.Both, // Other valid values: Source | Server | Neither | Both
2195
+ );
2196
+ verbose_log("Storing monitoredItem: " + nodeStr + " ItemId: " + monitoredItem.toString());
2197
+ monitoredItems.set(nodeStr, monitoredItem);
2198
+ } catch (err) {
2199
+ node_error("Check topic format for nodeId:" + msg.topic)
2200
+ node_error('subscription.monitorItem:' + err);
2201
+ }
2202
+
2203
+ monitoredItem.on("initialized", function () {
2204
+ verbose_log("initialized monitoredItem on " + nodeStr);
2205
+ });
2206
+
2207
+ monitoredItem.on("changed", function (dataValue) {
2208
+ let msgToSend = JSON.parse(JSON.stringify(msg)); // clone original msg if it contains other needed properties {};
2209
+
2210
+ set_node_status_to("active subscribed");
2211
+ // if (dataValue.statusCode != opcua.StatusCodes.Good) {
2212
+ // Skip Overflow and limitLow, limitHigh and constant bits
2213
+ if (dataValue.statusCode.isGoodish() === false) {
2214
+ verbose_warn("StatusCode: " + dataValue.statusCode.toString(16) + " " + dataValue.statusCode.description);
2215
+ }
2216
+
2217
+ msgToSend.statusCode = dataValue.statusCode;
2218
+ msgToSend.topic = msg.topic;
2219
+
2220
+ // Check if timestamps exists otherwise simulate them
2221
+ if (dataValue.serverTimestamp != null) {
2222
+ msgToSend.serverTimestamp = dataValue.serverTimestamp;
2223
+ msgToSend.serverPicoseconds = dataValue.serverPicoseconds;
2224
+ } else {
2225
+ msgToSend.serverTimestamp = new Date().getTime();;
2226
+ msgToSend.serverPicoseconds = 0;
2227
+ }
2228
+
2229
+ if (dataValue.sourceTimestamp != null) {
2230
+ msgToSend.sourceTimestamp = dataValue.sourceTimestamp;
2231
+ msgToSend.sourcePicoseconds = dataValue.sourcePicoseconds;
2232
+ } else {
2233
+ msgToSend.sourceTimestamp = new Date().getTime();;
2234
+ msgToSend.sourcePicoseconds = 0;
2235
+ }
2236
+
2237
+ msgToSend.payload = dataValue.value.value;
2238
+ node.send([msgToSend, null, null]);
2239
+
2240
+ });
2241
+
2242
+ monitoredItem.on("keepalive", function () {
2243
+ verbose_log("keepalive monitoredItem on " + nodeStr);
2244
+ });
2245
+
2246
+ monitoredItem.on("terminated", function () {
2247
+ verbose_log("terminated monitoredItem on " + nodeStr);
2248
+ if (monitoredItems.has(nodeStr)) {
2249
+ monitoredItems.delete(nodeStr);
2250
+ }
2251
+ });
2252
+ }
2253
+
2254
+ return monitoredItem;
2255
+ }
2256
+
2257
+ async function monitor_monitoredItem(subscription, msg) {
2258
+ verbose_log("Session subscriptionId: " + subscription.subscriptionId);
2259
+ let nodeStr = msg.topic;
2260
+ if (msg?.topic) {
2261
+ let dTypeIndex = nodeStr.indexOf(";datatype=");
2262
+ if (dTypeIndex > 0) {
2263
+ nodeStr = nodeStr.substring(0, dTypeIndex);
2264
+ }
2265
+ }
2266
+ let monitoredItem = monitoredItems.get(msg.topic);
2267
+ if (!monitoredItem) {
2268
+ verbose_log("Msg " + stringify(msg));
2269
+ let interval = 100; // Set as default if no payload
2270
+ let queueSize = 10;
2271
+ // Interval from the payload (old existing feature still supported)
2272
+ if (msg.payload && parseInt(msg.payload) > 100) {
2273
+ interval = convertAndCheckInterval(msg.payload);
2274
+ }
2275
+ if (msg.interval && parseInt(msg.interval) > 100) {
2276
+ interval = convertAndCheckInterval(msg.interval);
2277
+ }
2278
+ if (msg.queueSize && parseInt(msg.queueSize) > 0) {
2279
+ queueSize = msg.queueSize;
2280
+ }
2281
+ verbose_log("Monitoring " + msg.topic + " samplingInterval " + interval + "ms, queueSize " + queueSize);
2282
+ // Validate nodeId
2283
+ try {
2284
+ let nodeId = opcua.coerceNodeId(nodeStr);
2285
+ if (nodeId?.isEmpty()) {
2286
+ node_error(" Invalid empty node in getObject");
2287
+ }
2288
+ //makeNodeId(nodeStr); // above is enough
2289
+ } catch (err) {
2290
+ node_error(err);
2291
+ return;
2292
+ }
2293
+ let deadbandtype = opcua.DeadbandType.Absolute;
2294
+ // NOTE differs from standard subscription monitor
2295
+ if (node.deadbandType == "a") {
2296
+ deadbandtype = opcua.DeadbandType.Absolute;
2297
+ }
2298
+ if (node.deadbandType == "p") {
2299
+ deadbandtype = opcua.DeadbandType.Percent;
2300
+ }
2301
+ // Check if msg contains deadbandtype, use it instead of value given in client node
2302
+ if (msg.deadbandType && msg.deadbandType == "a") {
2303
+ deadbandtype = opcua.DeadbandType.Absolute;
2304
+ }
2305
+ if (msg.deadbandType && msg.deadbandType == "p") {
2306
+ deadbandtype = opcua.DeadbandType.Percent;
2307
+ }
2308
+ let deadbandvalue = node.deadbandvalue;
2309
+ // Check if msg contains deadbandValue, use it instead of value given in client node
2310
+ if (msg.deadbandValue) {
2311
+ deadbandvalue = msg.deadbandValue;
2312
+ }
2313
+ verbose_log("Deadband type (a==absolute, p==percent) " + deadbandtype + " deadband value " + deadbandvalue);
2314
+ let dataChangeFilter = new opcua.DataChangeFilter({
2315
+ trigger: opcua.DataChangeTrigger.StatusValue,
2316
+ deadbandType: deadbandtype,
2317
+ deadbandValue: deadbandvalue
2318
+ });
2319
+ const group = await subscription.monitorItems(
2320
+ [{
2321
+ nodeId: nodeStr,
2322
+ attributeId: opcua.AttributeIds.Value
2323
+ }],
2324
+ {
2325
+ samplingInterval: interval,
2326
+ queueSize: queueSize,
2327
+ discardOldest: true,
2328
+ filter: dataChangeFilter
2329
+ },
2330
+ TimestampsToReturn.Both, // Other valid values: Source | Server | Neither | Both
2331
+ );
2332
+ monitoredItems.set(nodeStr, monitoredItem);
2333
+
2334
+ group.on("err", () => {
2335
+ node.error("Monitored items error!");
2336
+ });
2337
+
2338
+ group.on("changed", (monitoredItem, dataValue, index) => {
2339
+ verbose_log(chalk.green("Received changes: " + monitoredItem + " value: " + dataValue + " index: " + index));
2340
+ set_node_status_to("active monitoring");
2341
+ verbose_log(dataValue.toString());
2342
+
2343
+ if (dataValue.statusCode.isGoodish() === false) {
2344
+ verbose_warn("StatusCode: " + dataValue.statusCode.toString(16) + " " + dataValue.statusCode.description);
2345
+ }
2346
+ let msgToSend = {};
2347
+ msgToSend.statusCode = dataValue.statusCode;
2348
+ msgToSend.topic = msg.topic;
2349
+
2350
+ // Check if timestamps exists otherwise simulate them
2351
+ if (dataValue.serverTimestamp != null) {
2352
+ msgToSend.serverTimestamp = dataValue.serverTimestamp;
2353
+ msgToSend.serverPicoseconds = dataValue.serverPicoseconds;
2354
+ } else {
2355
+ msgToSend.serverTimestamp = new Date().getTime();
2356
+ msgToSend.serverPicoseconds = 0;
2357
+ }
2358
+ if (dataValue.sourceTimestamp != null) {
2359
+ msgToSend.sourceTimestamp = dataValue.sourceTimestamp;
2360
+ msgToSend.sourcePicoseconds = dataValue.sourcePicoseconds;
2361
+ } else {
2362
+ msgToSend.sourceTimestamp = new Date().getTime();
2363
+ msgToSend.sourcePicoseconds = 0;
2364
+ }
2365
+ msgToSend.payload = dataValue.value.value;
2366
+ node.send([msgToSend, null, null]);
2367
+ });
2368
+ }
2369
+ }
2370
+
2371
+
2372
+ function unsubscribe_monitoredItem(subscription, msg) {
2373
+ verbose_log("Session subscriptionId: " + subscription.subscriptionId);
2374
+ let nodeStr = msg.topic; // nodeId needed as topic
2375
+ if (msg?.topic) {
2376
+ let dTypeIndex = nodeStr.indexOf(";datatype=");
2377
+ if (dTypeIndex > 0) {
2378
+ nodeStr = nodeStr.substring(0, dTypeIndex);
2379
+ }
2380
+ }
2381
+ let monitoredItem = monitoredItems.get(msg.topic);
2382
+ if (monitoredItem) {
2383
+ verbose_log("Got ITEM: " + monitoredItem);
2384
+ verbose_log("Unsubscribing monitored item: " + msg.topic + " item:" + monitoredItem.toString());
2385
+ monitoredItem.terminate();
2386
+ monitoredItems.delete(msg.topic);
2387
+ }
2388
+ else {
2389
+ node_error("NodeId " + nodeStr + " is not subscribed!");
2390
+ }
2391
+ }
2392
+
2393
+ function delete_subscription_action_input(msg) {
2394
+ verbose_log("delete subscription msg= " + stringify(msg));
2395
+ if (!subscription) {
2396
+ verbose_warn("Cannot delete, no subscription existing!");
2397
+ } else if (subscription.isActive) {
2398
+ // otherwise check if its terminated start to renew the subscription
2399
+
2400
+ node.session.deleteSubscriptions({
2401
+ subscriptionIds: [subscription.subscriptionId]
2402
+ }, function (err, response) {
2403
+ if (err) {
2404
+ node_error("Delete subscription error " + err);
2405
+ }
2406
+ else {
2407
+ verbose_log("Subscription deleted, response:" + stringify(response));
2408
+ subscription.terminate(); // Added to allow new subscription
2409
+ }
2410
+ });
2411
+ }
2412
+ }
2413
+
2414
+ node.add_item = function (item) {
2415
+ if (item) {
2416
+ if (!node.items) {
2417
+ node.items = new Array();
2418
+ }
2419
+ node.items.push({
2420
+ 'item': item
2421
+ });
2422
+ }
2423
+ };
2424
+
2425
+ function node_error(err) {
2426
+ node.error("Browse node error!", err);
2427
+ }
2428
+
2429
+ async function browse_action_input(msg) {
2430
+ verbose_log("browsing");
2431
+ node.items = [];
2432
+
2433
+ if (node.session) {
2434
+ const client = opcua.OPCUAClient.create(connectionOption);
2435
+ set_node_status_to("active browsing");
2436
+
2437
+ const nodeId = msg.topic;
2438
+ node.debug("nodeId: " + nodeId);
2439
+
2440
+ try {
2441
+ const browseResult = await node.session.browse(nodeId);
2442
+
2443
+ for(const reference of browseResult.references)
2444
+ {
2445
+ var ref_obj = Object.assign({}, reference);
2446
+ const dataValue = await node.session.read({nodeId: ref_obj.nodeId, attributeId: opcua.AttributeIds.Value});
2447
+ ref_obj["value"] = dataValue.value.value;
2448
+ ref_obj["dataType"] = opcua.DataType[dataValue.value.dataType];
2449
+ node.add_item(ref_obj);
2450
+ }
2451
+ msg.payload = node.items
2452
+ node.send([msg, null, null]);
2453
+ node.status({
2454
+ fill: "green",
2455
+ shape: "dot",
2456
+ text: "Items: " + node.items.length,
2457
+ source: { id: node.id, type: node.type, name: "OPC UA Browser"}
2458
+ });
2459
+ } catch (err) {
2460
+ node_error("Browse error: " + err.message);
2461
+ verbose_log(err);
2462
+ node.status({
2463
+ fill: "yellow",
2464
+ shape: "ring",
2465
+ text: "browse error",
2466
+ source: { id: node.id, type: node.type, name: "OPC UA Browser" }
2467
+ });
2468
+ }
2469
+ } else {
2470
+ node_error("Session is not active!");
2471
+ set_node_status_to("invalid session");
2472
+ reset_opcua_client(connect_opcua_client);
2473
+ }
2474
+ }
2475
+
2476
+ function subscribe_monitoredEvent(subscription, msg) {
2477
+ verbose_log("Session subscriptionId: " + subscription.subscriptionId);
2478
+
2479
+ let monitoredItem = monitoredItems.get(msg.topic);
2480
+ if (monitoredItem === undefined) {
2481
+ verbose_log("Msg " + stringify(msg));
2482
+ let interval = convertAndCheckInterval(msg.payload);
2483
+ verbose_log(msg.topic + " samplingInterval " + interval);
2484
+ verbose_log("Monitoring Event: " + msg.topic + ' by interval of ' + interval + " ms");
2485
+ // TODO read nodeId to validate it before subscription
2486
+ try {
2487
+ monitoredItem = opcua.ClientMonitoredItem.create(subscription,
2488
+ {
2489
+ nodeId: msg.topic, // serverObjectId
2490
+ attributeId: AttributeIds.EventNotifier
2491
+ }, {
2492
+ samplingInterval: interval,
2493
+ queueSize: 100000,
2494
+ filter: msg.eventFilter,
2495
+ discardOldest: true
2496
+ },
2497
+ TimestampsToReturn.Neither
2498
+ );
2499
+ } catch (err) {
2500
+ node_error('subscription.monitorEvent:' + err);
2501
+ // No actual error session exists, this case cause connections to server
2502
+ // reset_opcua_client(connect_opcua_client);
2503
+ }
2504
+ monitoredItems.set(msg.topic, monitoredItem.monitoredItemId);
2505
+ monitoredItem.on("initialized", async function () {
2506
+ if (node.session && subscription) {
2507
+ await opcua.callConditionRefresh(node.session, subscription.subscriptionId); // FIXED
2508
+ verbose_log("monitored Event initialized");
2509
+ set_node_status_to("initialized");
2510
+ }
2511
+ else {
2512
+ set_node_status_to("invalid session");
2513
+ }
2514
+ });
2515
+
2516
+ monitoredItem.on("changed", function (eventFields) {
2517
+ dumpEvent(node, node.session, msg.eventFields, eventFields, function () { });
2518
+ set_node_status_to("changed");
2519
+ });
2520
+
2521
+ monitoredItem.on("error", function (err_message) {
2522
+ verbose_log("error monitored Event on " + msg.topic);
2523
+ if (monitoredItems.has(msg.topic)) {
2524
+ monitoredItems.delete(msg.topic);
2525
+ }
2526
+
2527
+ node_error("monitored Event " + msg.eventTypeId + " ERROR" + err_message);
2528
+ set_node_error_status_to("error", err_message);
2529
+ });
2530
+
2531
+ monitoredItem.on("keepalive", function () {
2532
+ verbose_log("keepalive monitored Event on " + msg.topic);
2533
+ });
2534
+
2535
+ monitoredItem.on("terminated", function () {
2536
+ verbose_log("terminated monitored Event on " + msg.topic);
2537
+ if (monitoredItems.has(msg.topic)) {
2538
+ monitoredItems.delete(msg.topic);
2539
+ }
2540
+ });
2541
+ }
2542
+
2543
+ return monitoredItem;
2544
+ }
2545
+
2546
+ async function subscribe_events_async(msg) {
2547
+ verbose_log("subscribing events for " + msg.eventTypeIds);
2548
+ let eventTypeId = opcua.resolveNodeId(msg.eventTypeIds);
2549
+ let fields = await opcua.extractConditionFields(node.session, eventTypeId); // works with all eventTypes
2550
+
2551
+ // FIX for issue #623
2552
+ if (msg.hasOwnProperty("customEventFields")) {
2553
+ if (Array.isArray(msg.customEventFields)) {
2554
+ verbose_log("customEventFields: " + msg.customEventFields);
2555
+ fields = fields.concat(msg.customEventFields);
2556
+ } else {
2557
+ verbose_log("customEventFields is NOT Array!"); // Good to show
2558
+ }
2559
+ }
2560
+ else {
2561
+ verbose_log("msg object does not have customEventFields!");
2562
+ }
2563
+
2564
+ msg.eventFilter = opcua.constructEventFilter(fields, opcua.ofType(eventTypeId));
2565
+ msg.eventFields = fields;
2566
+ verbose_log("EventFields: " + msg.eventFields);
2567
+
2568
+ if (!subscription) {
2569
+ // first build and start subscription and subscribe on its started event by callback
2570
+ let timeMilliseconds = opcuaBasics.calc_milliseconds_by_time_and_unit(node.time, node.timeUnit);
2571
+ subscription = make_subscription(subscribe_monitoredEvent, msg, opcuaBasics.getEventSubscriptionParameters(timeMilliseconds));
2572
+ } else if (subscription.subscriptionId != "terminated") {
2573
+ // otherwise check if its terminated start to renew the subscription
2574
+ set_node_status_to("active subscribing");
2575
+ subscribe_monitoredEvent(subscription, msg);
2576
+ } else {
2577
+ subscription = null;
2578
+ monitoredItems.clear();
2579
+ set_node_status_to("terminated");
2580
+ }
2581
+ }
2582
+
2583
+ function subscribe_events_input(msg) {
2584
+ subscribe_events_async(msg);
2585
+ }
2586
+
2587
+ function reconnect(msg) {
2588
+ if (msg && msg.OpcUaEndpoint) {
2589
+ // Remove listeners if existing
2590
+ if (node.client) {
2591
+ verbose_log("Cleanup old listener events... before connecting to new client");
2592
+ verbose_log("All event names:" + node.client.eventNames());
2593
+ verbose_log("Connection_reestablished event count:" + node.client.listenerCount("connection_reestablished"));
2594
+ node.client.removeListener("connection_reestablished", reestablish);
2595
+ verbose_log("Backoff event count:" + node.client.listenerCount("backoff"));
2596
+ node.client.removeListener("backoff", backoff);
2597
+ verbose_log("Start reconnection event count:" + node.client.listenerCount("start_reconnection"));
2598
+ node.client.removeListener("start_reconnection", reconnection);
2599
+ }
2600
+ opcuaEndpoint = msg.OpcUaEndpoint; // Use global variable! Check all parameters!
2601
+ connectionOption.securityPolicy = opcua.SecurityPolicy[opcuaEndpoint?.securityPolicy]; // || opcua.SecurityPolicy.None;
2602
+ connectionOption.securityMode = opcua.MessageSecurityMode[opcuaEndpoint?.securityMode]; // || opcua.MessageSecurityMode.None;
2603
+ verbose_log("NEW connectionOption security parameters, policy: " + connectionOption.securityPolicy + " mode: " + connectionOption.securityMode);
2604
+ if (opcuaEndpoint.login === true) {
2605
+ let userIdentity = {
2606
+ userName: opcuaEndpoint?.user,
2607
+ password: opcuaEndpoint?.password,
2608
+ type: opcua.UserTokenType.UserName
2609
+ };
2610
+ verbose_log("NEW UserIdentity: " + JSON.stringify(userIdentity));
2611
+ }
2612
+ verbose_log("Using new endpoint:" + stringify(opcuaEndpoint));
2613
+ } else {
2614
+ verbose_log("Using endpoint:" + stringify(opcuaEndpoint));
2615
+ }
2616
+ // First close subscriptions etc.
2617
+ if (subscription && subscription.isActive) {
2618
+ subscription.terminate();
2619
+ }
2620
+ subscription = null;
2621
+ monitoredItems.clear();
2622
+
2623
+ set_node_status_to("reconnect");
2624
+
2625
+ // Sequential teardown: close session first, then disconnect client,
2626
+ // and only create the new client once the old TCP socket is fully closed.
2627
+ // This prevents stale connections from accumulating on the PLC.
2628
+ const doReconnect = () => {
2629
+ if (node.client) {
2630
+ verbose_log("Disconnecting old client before reconnect...");
2631
+ node.client.disconnect(function () {
2632
+ verbose_log("Old client disconnected, creating new client...");
2633
+ node.client = null;
2634
+ create_opcua_client(connect_opcua_client);
2635
+ });
2636
+ } else {
2637
+ create_opcua_client(connect_opcua_client);
2638
+ }
2639
+ };
2640
+
2641
+ if (node.session) {
2642
+ node.session.close(function (err) {
2643
+ if (err) {
2644
+ node_error("Session close error: " + err);
2645
+ } else {
2646
+ verbose_log("Session closed!");
2647
+ }
2648
+ node.session = null;
2649
+ doReconnect();
2650
+ });
2651
+ } else {
2652
+ verbose_warn("No session to close!");
2653
+ node.session = null;
2654
+ doReconnect();
2655
+ }
2656
+ }
2657
+
2658
+ node.on("close", async (done) => {
2659
+ if (subscription?.isActive) {
2660
+ subscription.terminate();
2661
+ // subscription becomes null by its terminated event
2662
+ }
2663
+
2664
+ if (node.session) {
2665
+ try {
2666
+ await node.session.close();
2667
+ verbose_log("Session closed");
2668
+ set_node_status_to("session closed");
2669
+ } catch (err) {
2670
+ node_error(node.name + " " + err);
2671
+ }
2672
+ }
2673
+
2674
+ node.session = null;
2675
+
2676
+ // Move done() inside the disconnect callback so Node-RED waits for the
2677
+ // TCP socket to fully close before considering this node shut down.
2678
+ // Without this, a redeploy starts a new connection while the old one is
2679
+ // still being torn down, causing the PLC to accumulate stale connections.
2680
+ if (node.client) {
2681
+ if(!node.disconnecting) return done();
2682
+ node.disconnecting = true;
2683
+ node.client.removeListener("connection_reestablished", reestablish);
2684
+ node.client.removeListener("backoff", backoff);
2685
+ node.client.removeListener("start_reconnection", reconnection);
2686
+ await node.client.disconnect(function () {
2687
+ verbose_log("Client disconnected on node close");
2688
+ node.disconnecting = false;
2689
+ node.client = null;
2690
+ done();
2691
+ });
2692
+ } else {
2693
+ done();
2694
+ }
2695
+ });
2696
+
2697
+ node.on("error", function () {
2698
+ if (subscription?.isActive) {
2699
+ subscription.terminate();
2700
+ // subscription becomes null by its terminated event
2701
+ }
2702
+
2703
+ if (node.session) {
2704
+ node.session.close(function (err) {
2705
+ verbose_log("Session closed on error emit");
2706
+ if (err) {
2707
+ node_error(node.name + " " + err);
2708
+ }
2709
+
2710
+ set_node_status_to("session closed");
2711
+ node.session = null;
2712
+ close_opcua_client("node error", err);
2713
+ });
2714
+
2715
+ } else {
2716
+ node.session = null;
2717
+ close_opcua_client("node error", 0);
2718
+ }
2719
+
2720
+
2721
+ verbose_log("Disconnecting...");
2722
+
2723
+ verbose_warn(`node.client == null ? ${node.client == null}`)
2724
+ if (node.client) {
2725
+ verbose_warn("Node CLient Error")
2726
+ node.client.removeListener("connection_reestablished", reestablish);
2727
+ verbose_log("Backoff event count:" + node.client.listenerCount("backoff"));
2728
+ node.client.removeListener("backoff", backoff);
2729
+ verbose_log("Start reconnection event count:" + node.client.listenerCount("start_reconnection"));
2730
+ node.client.removeListener("start_reconnection", reconnection);
2731
+ node.client.disconnect(function () {
2732
+ verbose_log("Client disconnected!");
2733
+ node.client = null;
2734
+ set_node_status_to("disconnected");
2735
+ });
2736
+ close_opcua_client("node error", 0);
2737
+ }
2738
+ });
2739
+ }
2740
+ RED.nodes.registerType("OpcUa-Client", OpcUaClientNode);
2741
+ }