@vitormnm/node-red-simple-opcua 1.7.0 → 1.8.1

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.
@@ -657,7 +657,7 @@
657
657
  }).fail(function (xhr) {
658
658
  var message = xhr && xhr.responseJSON && xhr.responseJSON.error
659
659
  ? xhr.responseJSON.error
660
- : "Falha ao navegar no servidor OPC UA.";
660
+ : "Failed to browse the OPC UA server.";
661
661
  browseState = null;
662
662
  container.html('<div class="opcua-tree-empty">' + escapeHtml(message) + '</div>');
663
663
  RED.notify(message, "error");
@@ -675,6 +675,7 @@
675
675
  contextMenuPath = path || "";
676
676
  $("#node-input-browse-context-refresh").toggle(!!item && !isVariable(item));
677
677
  $("#node-input-browse-context-copy-nodeid").toggle(!!nodeIdOf(item));
678
+ $("#node-input-browse-context-read-value").toggle(!!item && isVariable(item) && !!nodeIdOf(item));
678
679
  menu.css({ left: x + "px", top: y + "px" }).show();
679
680
  }
680
681
 
@@ -682,15 +683,15 @@
682
683
  var item = getItemAtPath(path);
683
684
  var nodeId = nodeIdOf(item);
684
685
  if (!nodeId) {
685
- RED.notify("NodeID nao encontrado para o item selecionado.", "warning");
686
+ RED.notify("NodeID not found for the selected item.", "warning");
686
687
  return;
687
688
  }
688
689
 
689
690
  if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
690
691
  navigator.clipboard.writeText(nodeId).then(function () {
691
- RED.notify("NodeID copiado.", "success");
692
+ RED.notify("NodeID copied.", "success");
692
693
  }).catch(function () {
693
- RED.notify("Falha ao copiar NodeID.", "error");
694
+ RED.notify("Failed to copy NodeID.", "error");
694
695
  });
695
696
  return;
696
697
  }
@@ -704,13 +705,48 @@
704
705
  input[0].select();
705
706
  try {
706
707
  document.execCommand("copy");
707
- RED.notify("NodeID copiado.", "success");
708
+ RED.notify("NodeID copied.", "success");
708
709
  } catch (error) {
709
- RED.notify("Falha ao copiar NodeID.", "error");
710
+ RED.notify("Failed to copy NodeID.", "error");
710
711
  }
711
712
  input.remove();
712
713
  }
713
714
 
715
+ function readValueFromPath(path) {
716
+ var item = getItemAtPath(path);
717
+ var nodeId = nodeIdOf(item);
718
+ if (!nodeId) {
719
+ RED.notify("NodeID not found for the selected item.", "warning");
720
+ return;
721
+ }
722
+
723
+ var connectionId = $("#node-input-connection").val();
724
+ if (!connectionId) {
725
+ RED.notify("Select an OPC UA connection before reading.", "warning");
726
+ return;
727
+ }
728
+
729
+ $.getJSON("opcua-client-config/" + encodeURIComponent(connectionId) + "/read", {
730
+ nodeId: nodeId
731
+ }).done(function (payload) {
732
+ if (payload && payload.error) {
733
+ RED.notify("Read failed: " + payload.error, "error");
734
+ } else if (payload) {
735
+ var valueText = (payload.valueEnumeration !== undefined && payload.valueEnumeration !== null)
736
+ ? payload.valueEnumeration + " (" + payload.value + ")"
737
+ : (payload.value !== undefined ? String(payload.value) : "undefined");
738
+ RED.notify("Variable value: " + valueText, "success");
739
+ } else {
740
+ RED.notify("No value returned from the server.", "warning");
741
+ }
742
+ }).fail(function (xhr) {
743
+ var message = xhr && xhr.responseJSON && xhr.responseJSON.error
744
+ ? xhr.responseJSON.error
745
+ : "Failed to read variable value.";
746
+ RED.notify(message, "error");
747
+ });
748
+ }
749
+
714
750
  function setBrowseSelectedPath(path) {
715
751
  browseSelectedPath = path || "";
716
752
  $(".opcua-tree-row").removeClass("is-selected");
@@ -759,20 +795,51 @@
759
795
  renderBrowseTree();
760
796
  var browseNodeId = item.nodeID || item.nodeId;
761
797
  loadBrowse(browseNodeId).done(function (payload) {
762
-
763
-
764
-
765
- item.browse = Array.isArray(payload.browse) ? payload.browse : [];
766
- saveBrowseSession();
767
- renderBrowseTree();
798
+ try {
799
+ if (!payload) {
800
+ console.error("Browse returned empty payload for node " + browseNodeId);
801
+ RED.notify("Browse returned empty payload.", "error");
802
+ item.browse = [];
803
+ } else if (payload.error) {
804
+ console.error("Browse returned error for node " + browseNodeId + ":", payload.error);
805
+ RED.notify(payload.error, "error");
806
+ item.browse = [];
807
+ } else {
808
+ item.browse = Array.isArray(payload.browse) ? payload.browse : [];
809
+ }
810
+ saveBrowseSession();
811
+ renderBrowseTree();
812
+ triggerChildrenExpansion(item.browse, path);
813
+ } catch (err) {
814
+ console.error("Error handling browse payload for node " + browseNodeId + ":", err);
815
+ RED.notify("Error handling browse response: " + err.message, "error");
816
+ expansionState[path] = false;
817
+ saveBrowseSession();
818
+ renderBrowseTree();
819
+ }
768
820
  }).fail(function (xhr) {
769
821
  expansionState[path] = false;
770
822
  saveBrowseSession();
771
823
  renderBrowseTree();
772
824
  var message = xhr && xhr.responseJSON && xhr.responseJSON.error
773
825
  ? xhr.responseJSON.error
774
- : "Falha ao expandir o node.";
826
+ : "Failed to expand the node.";
775
827
  RED.notify(message, "error");
828
+ console.error("Browse request failed for node " + browseNodeId + ":", xhr);
829
+ });
830
+ }
831
+
832
+ function triggerChildrenExpansion(children, parentPath) {
833
+ if (!Array.isArray(children)) return;
834
+ children.forEach(function (child, index) {
835
+ var childPath = parentPath + ".browse." + index;
836
+ if (isExpanded(childPath, false)) {
837
+ if (!Array.isArray(child.browse)) {
838
+ expandNode(childPath);
839
+ } else {
840
+ triggerChildrenExpansion(child.browse, childPath);
841
+ }
842
+ }
776
843
  });
777
844
  }
778
845
 
@@ -1057,7 +1124,24 @@
1057
1124
  if ($(event.target).closest(".opcua-client-toggle-tree, .opcua-client-toggle-tag, .opcua-tree-actions, #node-input-browse-context-menu").length) {
1058
1125
  return;
1059
1126
  }
1060
- setBrowseSelectedPath($(this).attr("data-path"));
1127
+ var path = $(this).attr("data-path");
1128
+ setBrowseSelectedPath(path);
1129
+
1130
+ if (event.ctrlKey || event.metaKey) {
1131
+ event.preventDefault();
1132
+ if ($("#node-input-mode").val() === "method") {
1133
+ var item = getItemAtPath(path);
1134
+ if (item && item.nodeClass === "Method") {
1135
+ var parentPath = path.split(".");
1136
+ parentPath.splice(parentPath.length - 2, 2);
1137
+ parentPath = parentPath.join(".");
1138
+ var parentItem = getItemAtPath(parentPath);
1139
+ addMethodFromTree(item, parentItem);
1140
+ return;
1141
+ }
1142
+ }
1143
+ toggleSelectedNode(path);
1144
+ }
1061
1145
  });
1062
1146
 
1063
1147
  $(document).on("contextmenu", ".opcua-tree-row", function (event) {
@@ -1091,6 +1175,14 @@
1091
1175
  copyNodeIdFromPath(path);
1092
1176
  });
1093
1177
 
1178
+ $(document).on("click", "#node-input-browse-context-read-value", function (event) {
1179
+ event.preventDefault();
1180
+ var highlightedPath = $(".opcua-tree-row.is-selected").first().attr("data-path") || "";
1181
+ var path = contextMenuPath || browseSelectedPath || highlightedPath || "";
1182
+ hideTreeContextMenu();
1183
+ readValueFromPath(path);
1184
+ });
1185
+
1094
1186
  $(document).on("click", function (event) {
1095
1187
  if (!$(event.target).closest("#node-input-browse-context-menu").length) {
1096
1188
  hideTreeContextMenu();
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@vitormnm/node-red-simple-opcua",
3
- "version": "1.7.0",
3
+ "version": "1.8.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
- "description": "Custom Node-RED OPC UA server and client node powered by node-opcua",
7
+ "description": "OPC UA server and client nodes with a graphical interface, powered by node-opcua",
8
8
  "main": "server/opcua-server.js",
9
9
  "node-red": {
10
10
  "version": ">=3.0.0",
@@ -15,18 +15,14 @@ class OpcUaAddressSpaceAlarm {
15
15
  }
16
16
 
17
17
  emitTagAccess(operation, details) {
18
- this.registry.emitTagAccess({
18
+ this.registry.emitTagAccess(Object.assign({
19
19
  operation,
20
20
  serverId: this.node.id,
21
21
  serverNodeName: this.node.name || "",
22
22
  serverName: this.serverName,
23
23
  timestamp: new Date().toISOString(),
24
- path: details.path,
25
- nodeID: details.nodeID,
26
- browseName: details.browseName,
27
- dataType: details.dataType,
28
- value: details.value
29
- });
24
+ users: []
25
+ }, details));
30
26
  }
31
27
 
32
28
  createAlarm(namespace, browseName, parentNode, inputNode, conditionName, nodeId, sourceName, alarmConfig) {
@@ -102,21 +98,21 @@ class OpcUaAddressSpaceAlarm {
102
98
  }
103
99
  }
104
100
 
105
- checkAlarm(alarm, variableValue) {
101
+ checkAlarm(alarm, variableValue, context = null) {
106
102
  if (alarm) {
107
103
  const alarmConfig = alarm.alarmConfig;
108
104
  const type = alarmConfig.type;
109
105
 
110
106
  if (type === "levelAlarm") {
111
- this.levelAlarm(alarm, variableValue);
107
+ this.levelAlarm(alarm, variableValue, context);
112
108
  }
113
109
  if (type === "digitalAlarm") {
114
- this.digitalAlarm(alarm, variableValue);
110
+ this.digitalAlarm(alarm, variableValue, context);
115
111
  }
116
112
  }
117
113
  }
118
114
 
119
- levelAlarm(alarm, variableValue) {
115
+ levelAlarm(alarm, variableValue, context = null) {
120
116
  const alarmNode = alarm.node;
121
117
  const alarmConfig = alarm.alarmConfig;
122
118
 
@@ -133,36 +129,37 @@ class OpcUaAddressSpaceAlarm {
133
129
 
134
130
  if (variableValue >= highHighSp && enabled) {
135
131
  message = sendValue ? (alarmConfig.highHighMessage + ": " + variableValue) : alarmConfig.highHighMessage;
136
- this.raiseAlarm(alarmNode, message, alarmConfig.severity);
132
+ this.raiseAlarm(alarmNode, message, alarmConfig.severity, true, context);
137
133
  lastMessage = message;
138
134
  } else if (variableValue >= highSp && enabled) {
139
135
  message = sendValue ? (alarmConfig.highMessage + ": " + variableValue) : alarmConfig.highMessage;
140
- this.raiseAlarm(alarmNode, message, alarmConfig.severity);
136
+ this.raiseAlarm(alarmNode, message, alarmConfig.severity, true, context);
141
137
  lastMessage = message;
142
138
  } else if (variableValue <= lowLowSp && enabled) {
143
139
  message = sendValue ? (alarmConfig.lowLowMessage + ": " + variableValue) : alarmConfig.lowLowMessage;
144
- this.raiseAlarm(alarmNode, message, alarmConfig.severity);
140
+ this.raiseAlarm(alarmNode, message, alarmConfig.severity, true, context);
145
141
  lastMessage = message;
146
142
  } else if (variableValue <= lowSp && enabled) {
147
143
  message = sendValue ? (alarmConfig.lowMessage + ": " + variableValue) : alarmConfig.lowMessage;
148
- this.raiseAlarm(alarmNode, message, alarmConfig.severity);
144
+ this.raiseAlarm(alarmNode, message, alarmConfig.severity, true, context);
149
145
  lastMessage = message;
150
146
  } else if (isActive) {
151
- this.clearAlarm(alarmNode, lastMessage, alarmConfig.severity);
147
+ message = sendValue ? (alarmConfig.normalMessage + ": " + variableValue) : alarmConfig.normalMessage;
148
+ this.clearAlarm(alarmNode, lastMessage || message, alarmConfig.severity, context);
152
149
  }
153
150
 
154
151
  this.alarmMethods(alarmNode);
155
152
  }
156
153
 
157
- digitalAlarm(alarm, variableValue) {
154
+ digitalAlarm(alarm, variableValue, context = null) {
158
155
  const alarmNode = alarm.node;
159
156
  const alarmConfig = alarm.alarmConfig;
160
157
  const enabled = alarmNode.getPropertyByName("enabled").readValue().value.value;
161
158
 
162
159
  if (variableValue && enabled) {
163
- this.raiseAlarm(alarmNode, alarmConfig.digitalMessage, alarmConfig.severity);
160
+ this.raiseAlarm(alarmNode, alarmConfig.digitalMessage, alarmConfig.severity, true, context);
164
161
  } else {
165
- this.clearAlarm(alarmNode, alarmConfig.digitalMessage, alarmConfig.severity);
162
+ this.clearAlarm(alarmNode, alarmConfig.digitalMessage, alarmConfig.severity, context);
166
163
  }
167
164
 
168
165
  this.alarmMethods(alarmNode);
@@ -184,9 +181,9 @@ class OpcUaAddressSpaceAlarm {
184
181
  retain: false
185
182
  });
186
183
 
187
- this.raiseNewConditionAlarm(alarm, message, severity, false);
184
+ this.raiseNewConditionAlarm(alarm, message, severity, false, context);
188
185
  } else {
189
- this.raiseNewConditionAlarm(alarm, message, severity, true);
186
+ this.raiseNewConditionAlarm(alarm, message, severity, true, context);
190
187
  }
191
188
 
192
189
  callback(null, {
@@ -202,7 +199,7 @@ class OpcUaAddressSpaceAlarm {
202
199
  alarm.ackedState.setValue(true);
203
200
  alarm.confirmedState.setValue(false);
204
201
 
205
- this.raiseNewConditionAlarm(alarm, message, severity, true);
202
+ this.raiseNewConditionAlarm(alarm, message, severity, true, context);
206
203
 
207
204
  callback(null, {
208
205
  statusCode: StatusCodes.Good,
@@ -225,16 +222,16 @@ class OpcUaAddressSpaceAlarm {
225
222
  });
226
223
  }
227
224
 
228
- clearAlarm(alarmNode, message, severity) {
225
+ clearAlarm(alarmNode, message, severity, context = null) {
229
226
  const isAcked = !alarmNode.ackedState.id.readValue().value.value;
230
227
 
231
228
  alarmNode.activeState.setValue(false);
232
229
  alarmNode.raiseNewCondition({ message, severity: severity, isAcked });
233
230
 
234
- this.raiseNewConditionAlarm(alarmNode, message, severity, isAcked);
231
+ this.raiseNewConditionAlarm(alarmNode, message, severity, isAcked, context);
235
232
  }
236
233
 
237
- raiseAlarm(alarmNode, message, severity, retain = true) {
234
+ raiseAlarm(alarmNode, message, severity, retain = true, context = null) {
238
235
  const isActive = alarmNode.activeState.id.readValue().value.value;
239
236
  const isAcked = alarmNode.ackedState.id.readValue().value.value;
240
237
  if (isActive && isAcked) {
@@ -247,10 +244,27 @@ class OpcUaAddressSpaceAlarm {
247
244
  }
248
245
 
249
246
  alarmNode.activeState.setValue(true);
250
- this.raiseNewConditionAlarm(alarmNode, message, severity, retain);
247
+ this.raiseNewConditionAlarm(alarmNode, message, severity, retain, context);
251
248
  }
252
249
 
253
- raiseNewConditionAlarm(alarmNode, message, severity, retain) {
250
+ getUserGroups(username) {
251
+ const normalized = String(username || "").trim();
252
+ if (!normalized || normalized.toLowerCase() === "anonymous") {
253
+ return [];
254
+ }
255
+ const users = (this.node && this.node.runtime && this.node.runtime.users) || [];
256
+ const user = users.find(u => u && u.username === normalized);
257
+ if (!user) {
258
+ return [];
259
+ }
260
+ return typeof user.group === "string"
261
+ ? user.group.split(",").map(g => g.trim()).filter(Boolean)
262
+ : Array.isArray(user.group)
263
+ ? user.group
264
+ : [];
265
+ }
266
+
267
+ raiseNewConditionAlarm(alarmNode, message, severity, retain, context = null) {
254
268
  alarmNode.raiseNewCondition({ message, severity, retain });
255
269
 
256
270
  this.registry.registerActiveAlarms(alarmNode, message, severity, retain, this.node);
@@ -258,12 +272,55 @@ class OpcUaAddressSpaceAlarm {
258
272
  const alarmConfig = alarmNode.alarmConfig || {};
259
273
  const sendValue = alarmConfig.sendValue !== false;
260
274
 
275
+ const ConditionName = alarmNode.getPropertyByName("ConditionName").readValue().value.value;
276
+ const SourceName = alarmNode.getPropertyByName("SourceName").readValue().value.value;
277
+ const isActive = alarmNode.activeState.id.readValue().value.value;
278
+ const isAcked = alarmNode.ackedState.id.readValue().value.value;
279
+ const ConfirmedState = alarmNode.confirmedState.id.readValue().value.value;
280
+
281
+ const activeContext = context || this.registry.activeWriteContext;
282
+
283
+ const users = [];
284
+ if (activeContext && activeContext.session) {
285
+ const session = activeContext.session;
286
+ const username = (session.userIdentityToken && session.userIdentityToken.userName)
287
+ ? session.userIdentityToken.userName
288
+ : "anonymous";
289
+ const groups = this.getUserGroups(username);
290
+ users.push({
291
+ name: username,
292
+ groups: groups
293
+ });
294
+ } else {
295
+ users.push({
296
+ name: "anonymous",
297
+ groups: []
298
+ });
299
+ }
300
+
261
301
  this.emitTagAccess("alarm", {
302
+ path: alarmNode.path || alarmNode.browseName.name,
303
+ nodeID: alarmNode.nodeId.toString(),
304
+ browseName: alarmNode.browseName.name,
262
305
  message: message,
263
306
  severity: severity,
264
307
  retain: retain,
265
308
  dataType: "alarm",
266
- value: sendValue ? "highHighSp" : null
309
+ value: sendValue ? "highHighSp" : null,
310
+ activeState: isActive,
311
+ sourceName: SourceName,
312
+ conditionName: ConditionName,
313
+ ConfirmedState: ConfirmedState,
314
+ ackedState: isAcked,
315
+ users: users,
316
+ alarmNode: {
317
+ nodeId: alarmNode.nodeId,
318
+ browseName: alarmNode.browseName,
319
+ displayName: alarmNode.displayName,
320
+ description: alarmNode.description,
321
+ nodeClass: alarmNode.nodeClass,
322
+ typeDefinition: alarmNode.typeDefinition
323
+ }
267
324
  });
268
325
  }
269
326