@vitormnm/node-red-simple-opcua 1.0.2 → 1.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.
@@ -0,0 +1,380 @@
1
+ "use strict";
2
+
3
+ const { coerceNodeId } = require("node-opcua");
4
+
5
+ const {
6
+ buildVariantFromItem,
7
+ callResultToItemResult,
8
+ ensureArrayPayload,
9
+ getMethodArgumentDefinition,
10
+ normalizeTypeName,
11
+ resolveName,
12
+ resolveMethodObjectId,
13
+ resolveNodeId
14
+ } = require("./opcua-client-utils");
15
+
16
+ const {
17
+ browseNode: browseNodeWithSession,
18
+ ROOT_NODE_ID
19
+ } = require("./lib/opcua-client-browser");
20
+
21
+ const { OpcUaClientReadService } = require("./lib/opcua-client-read-service");
22
+ const { OpcUaClientWriteService } = require("./lib/opcua-client-write-service");
23
+ const { OpcUaClientSubscriptionService } = require("./lib/opcua-client-subscription-service");
24
+ const { OpcUaClientSubscriptionIdService } = require("./lib/opcua-client-subscription-id-service");
25
+
26
+ module.exports = function (RED) {
27
+ function OpcUaClientNode(config) {
28
+ RED.nodes.createNode(this, config);
29
+ const node = this;
30
+
31
+ node.name = (config.name || "").trim();
32
+ node.connection = RED.nodes.getNode(config.connection);
33
+ node.mode = config.mode || "read";
34
+ node.selectedItems = parseConfiguredItems(config.selectedItems);
35
+ node.valueProperty = (config.valueProperty || "payload").trim();
36
+ node.valuePropertyType = config.valuePropertyType || "msg";
37
+ node.samplingInterval = Math.max(Number(config.samplingInterval) || 250, 50);
38
+ node.publishingInterval = Math.max(Number(config.publishingInterval) || 250, 50);
39
+ node.subscription = null;
40
+ node.monitoredItems = [];
41
+
42
+ const itemsResolver = createItemsResolver(RED);
43
+ const readService = new OpcUaClientReadService();
44
+ const writeService = new OpcUaClientWriteService();
45
+ const subscriptionService = new OpcUaClientSubscriptionService();
46
+ const subscriptionIdService = new OpcUaClientSubscriptionIdService();
47
+
48
+ node.on("input", async function (msg, send, done) {
49
+ send = send || function () {
50
+ node.send.apply(node, arguments);
51
+ };
52
+
53
+ try {
54
+ if (!node.connection) {
55
+ throw new Error("No OPC UA client configuration selected");
56
+ }
57
+
58
+ if (node.mode === "subscription") {
59
+ const subscriptionItems = await subscriptionService.startDataSubscription(node, msg, itemsResolver);
60
+ node.status({ fill: "green", shape: "dot", text: "subscribed " + subscriptionItems.length + " nodes" });
61
+ done();
62
+ return;
63
+ }
64
+
65
+ if (node.mode === "events") {
66
+ const eventsItems = await subscriptionService.startEventSubscription(node, msg, itemsResolver);
67
+ node.status({ fill: "green", shape: "dot", text: "events " + eventsItems.length + " nodes" });
68
+ done();
69
+ return;
70
+ }
71
+
72
+ const session = await node.connection.getSession();
73
+ let payload;
74
+
75
+ if (node.mode === "read") {
76
+ payload = await readService.execute(node, msg, session, itemsResolver);
77
+ node.status({ fill: "green", shape: "dot", text: "read " + payload.length + " nodes" });
78
+ } else if (node.mode === "write") {
79
+ payload = await writeService.execute(node, msg, session, itemsResolver);
80
+ node.status({ fill: "green", shape: "dot", text: "write " + payload.length + " nodes" });
81
+ } else if (node.mode === "browse") {
82
+ payload = await executeBrowse(node, msg, session);
83
+ node.status({ fill: "green", shape: "dot", text: "browsed " + payload.length + " nodes" });
84
+ } else if (node.mode === "method") {
85
+ payload = await executeMethod(node, msg, session);
86
+ node.status({ fill: "green", shape: "dot", text: "called " + payload.length + " methods" });
87
+ } else if (node.mode === "getSubscriptionId") {
88
+ payload = await subscriptionIdService.execute(node);
89
+ node.status({ fill: "green", shape: "dot", text: "called getSubscriptionId" });
90
+ } else {
91
+ throw new Error("Unsupported OPC UA client mode: " + node.mode);
92
+ }
93
+
94
+ msg.payload = payload;
95
+ send(msg);
96
+ done();
97
+ } catch (error) {
98
+ node.status({ fill: "red", shape: "ring", text: node.mode + " failed" });
99
+ done(error);
100
+ }
101
+ });
102
+
103
+ node.on("close", async function (done) {
104
+ try {
105
+ await subscriptionService.stop(node);
106
+ done();
107
+ } catch (error) {
108
+ done(error);
109
+ }
110
+ });
111
+ }
112
+
113
+ async function executeBrowse(node, msg, session) {
114
+ const roots = normalizeBrowseRoots(node, msg ? msg.payload : undefined);
115
+ const payload = [];
116
+
117
+ for (const root of roots) {
118
+ payload.push(await browseNodeWithSession(session, root));
119
+ }
120
+
121
+ return payload;
122
+ }
123
+
124
+ async function executeMethod(node, msg, session) {
125
+ const items = ensureArrayPayload(msg, "OPC UA method call");
126
+ const payload = [];
127
+
128
+ for (const item of items) {
129
+ const methodNodeId = resolveMethodId(item);
130
+
131
+ try {
132
+ const objectId = resolveMethodObjectIdFromItem(item) || await resolveMethodObjectId(
133
+ session,
134
+ methodNodeId,
135
+ node.connection.methodObjectIdCache
136
+ );
137
+ const argumentDefinition = await safeGetMethodArgumentDefinition(
138
+ session,
139
+ methodNodeId,
140
+ node.connection.methodDefinitionCache
141
+ );
142
+ const callRequest = {
143
+ objectId,
144
+ methodId: methodNodeId
145
+ };
146
+
147
+ if (Array.isArray(item.inputs) && item.inputs.length > 0) {
148
+ callRequest.inputArguments = item.inputs.map((input) => buildVariantFromItem(input, input.type));
149
+ }
150
+
151
+ const callResult = await session.call(callRequest);
152
+ payload.push(callResultToItemResult(item, callResult, argumentDefinition));
153
+ } catch (itemError) {
154
+ payload.push({
155
+ name: item.name || methodNodeId,
156
+ nodeID: methodNodeId,
157
+ status: itemError.message,
158
+ outputs: []
159
+ });
160
+ }
161
+ }
162
+
163
+ return payload;
164
+ }
165
+
166
+ async function safeGetMethodArgumentDefinition(session, methodNodeId, cache) {
167
+ try {
168
+ return await getMethodArgumentDefinition(session, methodNodeId, cache);
169
+ } catch (error) {
170
+ return {
171
+ inputArguments: [],
172
+ outputArguments: []
173
+ };
174
+ }
175
+ }
176
+
177
+ function createItemsResolver(REDRuntime) {
178
+ return {
179
+ ensureClientItems,
180
+ ensureWriteItems
181
+ };
182
+
183
+ function ensureClientItems(node, msg, contextName) {
184
+ const payload = msg ? msg.payload : undefined;
185
+
186
+ if (Array.isArray(payload) && payload.length > 0) {
187
+ return payload;
188
+ }
189
+
190
+ if (node.selectedItems.length > 0) {
191
+ return node.selectedItems;
192
+ }
193
+
194
+ return ensureArrayPayload(msg, contextName);
195
+ }
196
+
197
+ function ensureWriteItems(node, msg) {
198
+ const payload = msg ? msg.payload : undefined;
199
+
200
+ if (Array.isArray(payload) && payload.length > 0) {
201
+ return payload;
202
+ }
203
+
204
+ if (node.selectedItems.length > 0) {
205
+ const configuredValue = resolveConfiguredWriteValue(node, msg, REDRuntime);
206
+ return node.selectedItems.map((item, index) => ({
207
+ name: item.name,
208
+ nodeID: item.nodeID,
209
+ type: item.type,
210
+ value: resolveWriteValueForItem(node, msg, item, index, configuredValue, REDRuntime)
211
+ }));
212
+ }
213
+
214
+ return ensureArrayPayload(msg, "OPC UA write");
215
+ }
216
+ }
217
+
218
+ function normalizeBrowseRoots(node, payload) {
219
+ if (payload === undefined || payload === null) {
220
+ if (node.selectedItems.length > 0) {
221
+ return node.selectedItems.map((item) => ({
222
+ name: item.name,
223
+ nodeID: normalizeBrowseNodeId(item.nodeID)
224
+ }));
225
+ }
226
+ return [{ name: "RootFolder", nodeID: ROOT_NODE_ID }];
227
+ }
228
+
229
+ if (!Array.isArray(payload)) {
230
+ return node.selectedItems;
231
+ }
232
+
233
+ if (!payload.length) {
234
+ return [{ name: "RootFolder", nodeID: ROOT_NODE_ID }];
235
+ }
236
+
237
+ return payload.map((item) => {
238
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
239
+ throw new Error("Each browse item must be an object");
240
+ }
241
+
242
+ return {
243
+ name: typeof item.name === "string" && item.name.trim() ? item.name.trim() : "",
244
+ nodeID: normalizeBrowseNodeId(item.nodeID || item.nodeId)
245
+ };
246
+ });
247
+ }
248
+
249
+ function normalizeBrowseNodeId(nodeId) {
250
+ return coerceNodeId(nodeId).toString();
251
+ }
252
+
253
+ function resolveMethodObjectIdFromItem(item) {
254
+ const objectId = item && (item.objectID || item.objectId);
255
+ if (!objectId || !String(objectId).trim()) {
256
+ return "";
257
+ }
258
+
259
+ return String(objectId).trim();
260
+ }
261
+
262
+ function resolveMethodId(item) {
263
+ const methodId = item && (item.methodID || item.methodId || item.nodeID || item.nodeId);
264
+ if (!methodId || !String(methodId).trim()) {
265
+ throw new Error("Each method item must contain methodId or nodeID");
266
+ }
267
+
268
+ return String(methodId).trim();
269
+ }
270
+
271
+ function parseConfiguredItems(rawValue) {
272
+ if (!rawValue || typeof rawValue !== "string") {
273
+ return [];
274
+ }
275
+
276
+ try {
277
+ const parsed = JSON.parse(rawValue);
278
+ if (!Array.isArray(parsed)) {
279
+ return [];
280
+ }
281
+
282
+ return parsed
283
+ .filter((item) => item && typeof item === "object" && !Array.isArray(item))
284
+ .map((item) => ({
285
+ name: typeof item.name === "string" && item.name.trim()
286
+ ? item.name.trim()
287
+ : resolveName(item, item.nodeID || item.nodeId || ""),
288
+ nodeID: resolveNodeId(item),
289
+ type: normalizeTypeName(item.type) || normalizeTypeName(item.dataType) || undefined,
290
+ valueProperty: typeof item.valueProperty === "string" && item.valueProperty.trim()
291
+ ? item.valueProperty.trim()
292
+ : "payload",
293
+ valuePropertyType: item.valuePropertyType === "flow" || item.valuePropertyType === "global"
294
+ ? item.valuePropertyType
295
+ : "msg"
296
+ }));
297
+ } catch (error) {
298
+ return [];
299
+ }
300
+ }
301
+
302
+ function resolveWriteValueForItem(node, msg, item, index, fallbackConfiguredValue, REDRuntime) {
303
+ const property = item && typeof item.valueProperty === "string" && item.valueProperty.trim()
304
+ ? item.valueProperty.trim()
305
+ : (node.valueProperty || "payload");
306
+ const type = item && (item.valuePropertyType === "msg" || item.valuePropertyType === "flow" || item.valuePropertyType === "global")
307
+ ? item.valuePropertyType
308
+ : (node.valuePropertyType || "msg");
309
+
310
+ let value;
311
+ if (type === "msg") {
312
+ value = REDRuntime.util.getMessageProperty(msg, property);
313
+ } else if (type === "flow") {
314
+ value = node.context().flow.get(property);
315
+ } else if (type === "global") {
316
+ value = node.context().global.get(property);
317
+ } else {
318
+ value = REDRuntime.util.getMessageProperty(msg, property);
319
+ }
320
+
321
+ if (typeof value !== "undefined") {
322
+ return value;
323
+ }
324
+
325
+ return selectWriteValueForItem(fallbackConfiguredValue, item, index);
326
+ }
327
+
328
+ function resolveConfiguredWriteValue(node, msg, REDRuntime) {
329
+ const property = node.valueProperty || "payload";
330
+ const type = node.valuePropertyType || "msg";
331
+
332
+ if (type === "msg") {
333
+ return REDRuntime.util.getMessageProperty(msg, property);
334
+ }
335
+
336
+ if (type === "flow") {
337
+ return node.context().flow.get(property);
338
+ }
339
+
340
+ if (type === "global") {
341
+ return node.context().global.get(property);
342
+ }
343
+
344
+ return REDRuntime.util.getMessageProperty(msg, property);
345
+ }
346
+
347
+ function selectWriteValueForItem(configuredValue, item, index) {
348
+ if (Array.isArray(configuredValue)) {
349
+ const byName = item && item.name ? configuredValue.find((entry) => entry && entry.name === item.name) : undefined;
350
+ if (byName && Object.prototype.hasOwnProperty.call(byName, "value")) {
351
+ return byName.value;
352
+ }
353
+
354
+ const byNodeId = item && item.nodeID ? configuredValue.find((entry) => entry && (entry.nodeID === item.nodeID || entry.nodeId === item.nodeID)) : undefined;
355
+ if (byNodeId && Object.prototype.hasOwnProperty.call(byNodeId, "value")) {
356
+ return byNodeId.value;
357
+ }
358
+
359
+ return configuredValue[index];
360
+ }
361
+
362
+ if (configuredValue && typeof configuredValue === "object") {
363
+ if (Object.prototype.hasOwnProperty.call(configuredValue, "value")) {
364
+ return configuredValue.value;
365
+ }
366
+
367
+ if (item && item.nodeID && Object.prototype.hasOwnProperty.call(configuredValue, item.nodeID)) {
368
+ return configuredValue[item.nodeID];
369
+ }
370
+
371
+ if (item && item.name && Object.prototype.hasOwnProperty.call(configuredValue, item.name)) {
372
+ return configuredValue[item.name];
373
+ }
374
+ }
375
+
376
+ return configuredValue;
377
+ }
378
+
379
+ RED.nodes.registerType("opcua-client", OpcUaClientNode);
380
+ };
package/object.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "objects": [],
3
+ "folders": [
4
+ {
5
+ "name": "site",
6
+ "folders": [
7
+ {
8
+ "name": "line1",
9
+ "folders": [],
10
+ "objects": [
11
+ {
12
+ "name": "motor",
13
+ "folders": [],
14
+ "objects": [],
15
+ "variables": [
16
+ {
17
+ "name": "speed",
18
+ "type": "Float",
19
+ "value": "",
20
+ "access": "readonly",
21
+ "description": "",
22
+ "displayName": ""
23
+ }
24
+ ],
25
+ "method": [
26
+ {
27
+ "name": "cmd_on",
28
+ "description": "",
29
+ "displayName": "",
30
+ "inputs": [
31
+ {
32
+ "name": "sp_speed",
33
+ "type": "Float",
34
+ "displayName": ""
35
+ },
36
+ {
37
+ "name": "cmd_on",
38
+ "type": "Boolean",
39
+ "displayName": ""
40
+ }
41
+ ],
42
+ "outputs": [
43
+ {
44
+ "name": "sts_spped",
45
+ "type": "Float",
46
+ "displayName": ""
47
+ },
48
+ {
49
+ "name": "sts_motor",
50
+ "type": "Boolean",
51
+ "displayName": ""
52
+ }
53
+ ]
54
+ }
55
+ ]
56
+ }
57
+ ],
58
+ "variables": []
59
+ }
60
+ ],
61
+ "objects": [],
62
+ "variables": []
63
+ }
64
+ ]
65
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitormnm/node-red-simple-opcua",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -15,13 +15,12 @@
15
15
  "opcua-client": "client/opcua-client.js"
16
16
  }
17
17
  },
18
- "files": [
19
- "resources/*",
20
- "examples/*"
21
- ],
22
18
  "scripts": {
23
19
  "test": "echo \"Error: no test specified\" && exit 1"
24
20
  },
21
+ "engines": {
22
+ "node": ">=20.0.0"
23
+ },
25
24
  "author": {
26
25
  "name": "Vitor Mião",
27
26
  "url": "https://github.com/vitormnm"