@vitormnm/node-red-simple-opcua 1.6.2 → 1.7.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 (42) hide show
  1. package/README.md +89 -136
  2. package/client/lib/opcua-client-browser.js +238 -10
  3. package/client/lib/opcua-client-method-service.js +1 -1
  4. package/client/lib/opcua-client-subscription-service.js +0 -2
  5. package/client/opcua-client-config.html +118 -1
  6. package/client/opcua-client-config.js +74 -8
  7. package/client/opcua-client-help.html +6 -0
  8. package/client/opcua-client-utils.js +34 -10
  9. package/client/opcua-client.html +7 -0
  10. package/client/opcua-client.js +97 -1
  11. package/examples/flows_simple_opc.json +1 -1
  12. package/package.json +1 -1
  13. package/server/lib/opcua-address-space-alarm.js +11 -5
  14. package/server/lib/opcua-address-space-builder.js +65 -15
  15. package/server/lib/opcua-config.js +81 -23
  16. package/server/lib/opcua-server-events-child.js +1 -1
  17. package/server/lib/opcua-server-runtime-child.js +429 -59
  18. package/server/lib/opcua-server-runtime.js +49 -5
  19. package/server/lib/opcua-server-status-child.js +14 -14
  20. package/server/nodered/simple_opcua/server/certificates/mutex +0 -0
  21. package/server/nodered/simple_opcua/server/certificates/own/certs/server_selfsigned_cert_2048.pem +25 -0
  22. package/server/nodered/simple_opcua/server/certificates/own/certs/server_selfsigned_cert_2048.pem.mutex +0 -0
  23. package/server/nodered/simple_opcua/server/certificates/own/openssl.cnf +72 -0
  24. package/server/nodered/simple_opcua/server/certificates/own/private/private_key.pem +28 -0
  25. package/server/nodered/simple_opcua/server/certificates/trusted/certs/NodeOPCUA-Client@tuf[c5a9e20a8b680cdff76aaf0165bb3c9318da37a5].pem +25 -0
  26. package/server/nodered/simple_opcua/server/myServer1/mutex +0 -0
  27. package/server/nodered/simple_opcua/server/myServer1/own/certs/server_selfsigned_cert_2048.pem +25 -0
  28. package/server/nodered/simple_opcua/server/myServer1/own/certs/server_selfsigned_cert_2048.pem.mutex +0 -0
  29. package/server/nodered/simple_opcua/server/myServer1/own/openssl.cnf +72 -0
  30. package/server/nodered/simple_opcua/server/myServer1/own/private/private_key.pem +28 -0
  31. package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[91e520c64ff891c67168f08a46dd194071e15dae].pem +25 -0
  32. package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[98ae95da627cea4c500753c319161a3554ee38d7].pem +25 -0
  33. package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[aef8d7a1cfba13d84189a0bcf1694208fc51a7f9].pem +25 -0
  34. package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[c5a9e20a8b680cdff76aaf0165bb3c9318da37a5].pem +25 -0
  35. package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[ebdf9acf1d02e347917a14108d3144799c638ea3].pem +25 -0
  36. package/server/opcua-server-io.html +76 -0
  37. package/server/opcua-server-io.js +135 -23
  38. package/server/opcua-server.css +52 -0
  39. package/server/opcua-server.html +166 -44
  40. package/server/opcua-server.js +142 -7
  41. package/server/view/opcua-server.css +89 -6
  42. package/server/view/opcua-server.js +523 -42
@@ -12,13 +12,136 @@
12
12
  var isSyncing = false;
13
13
  var DEFAULT_NAMESPACE_ID = 2;
14
14
 
15
+ var selectedCertFolder = "rejected";
16
+ var selectedCertName = "";
17
+ var certificatesData = { trusted: [], rejected: [] };
18
+
15
19
  function syncModalBodyClass() {
16
- $("body").toggleClass("opcua-tree-modal-open", $("#node-input-tree-modal").is(":visible") || $("#node-input-auth-modal").is(":visible"));
20
+ $("body").toggleClass("opcua-tree-modal-open",
21
+ $("#node-input-tree-modal").is(":visible") ||
22
+ $("#node-input-auth-modal").is(":visible") ||
23
+ $("#node-input-cert-modal").is(":visible") ||
24
+ $("#node-input-settings-modal").is(":visible")
25
+ );
17
26
  }
18
27
  function openTreeModal() { $("#node-input-tree-modal").show(); syncModalBodyClass(); }
19
28
  function closeTreeModal() { $("#node-input-tree-modal").hide(); syncModalBodyClass(); }
20
29
  function openAuthModal() { $("#node-input-auth-modal").show(); syncModalBodyClass(); renderAuthEditor(); }
21
30
  function closeAuthModal() { $("#node-input-auth-modal").hide(); syncModalBodyClass(); }
31
+ function openCertModal() { $("#node-input-cert-modal").show(); syncModalBodyClass(); initCertEditor(); }
32
+ function closeCertModal() { $("#node-input-cert-modal").hide(); syncModalBodyClass(); }
33
+ function openSettingsModal() { $("#node-input-settings-modal").show(); syncModalBodyClass(); }
34
+ function closeSettingsModal() { $("#node-input-settings-modal").hide(); syncModalBodyClass(); }
35
+
36
+ function initCertEditor() {
37
+ selectedCertFolder = "rejected";
38
+ selectedCertName = "";
39
+ $("#opcua-cert-details").hide();
40
+ $("#opcua-cert-folders .opcua-cert-item").removeClass("is-selected");
41
+ $('#opcua-cert-folders .opcua-cert-item[data-folder="rejected"]').addClass("is-selected");
42
+ fetchCertificates();
43
+ }
44
+
45
+ function fetchCertificates() {
46
+ var filesContainer = $("#opcua-cert-files");
47
+ filesContainer.empty().append('<div class="opcua-tree-empty"><i class="fa fa-spinner fa-spin"></i> Loading certificates...</div>');
48
+
49
+ var currentServerName = $("#node-input-serverName").val() || "";
50
+
51
+ $.ajax({
52
+ url: "opc-ua-server/certificates",
53
+ type: "GET",
54
+ data: {
55
+ serverName: currentServerName
56
+ },
57
+ dataType: "json",
58
+ success: function (data) {
59
+ certificatesData = data || { trusted: [], rejected: [] };
60
+ renderCertificatesList();
61
+ },
62
+ error: function (xhr, textStatus, errorThrown) {
63
+ filesContainer.empty().append('<div class="opcua-tree-empty" style="color: #d9534f;"><i class="fa fa-exclamation-triangle"></i> Failed to load certificates.</div>');
64
+ }
65
+ });
66
+ }
67
+
68
+ function renderCertificatesList() {
69
+ var filesContainer = $("#opcua-cert-files");
70
+ filesContainer.empty();
71
+
72
+ var list = certificatesData[selectedCertFolder] || [];
73
+ if (list.length === 0) {
74
+ filesContainer.append('<div class="opcua-tree-empty">No certificates found in this folder.</div>');
75
+ $("#opcua-cert-details").hide();
76
+ return;
77
+ }
78
+
79
+ list.forEach(function (filename) {
80
+ var item = $('<div class="opcua-cert-item"></div>');
81
+ item.attr("data-name", filename);
82
+ item.append('<i class="fa fa-certificate"></i> ' + escapeHtml(filename));
83
+ if (filename === selectedCertName) {
84
+ item.addClass("is-selected");
85
+ }
86
+ filesContainer.append(item);
87
+ });
88
+
89
+ // If previously selected cert is not in the list, hide details
90
+ if (selectedCertName && list.indexOf(selectedCertName) === -1) {
91
+ selectedCertName = "";
92
+ $("#opcua-cert-details").hide();
93
+ } else if (selectedCertName) {
94
+ showCertificateDetails();
95
+ }
96
+ }
97
+
98
+ function showCertificateDetails() {
99
+ $("#opcua-selected-cert-name").text(selectedCertName);
100
+ var targetSelect = $("#opcua-cert-target-folder");
101
+ targetSelect.empty();
102
+
103
+ if (selectedCertFolder === "rejected") {
104
+ targetSelect.append('<option value="trusted">Trusted Certificates</option>');
105
+ } else {
106
+ targetSelect.append('<option value="rejected">Rejected Certificates</option>');
107
+ }
108
+
109
+ $("#opcua-cert-details").show();
110
+ }
111
+
112
+ function moveCertificate() {
113
+ if (!selectedCertName) return;
114
+ var targetFolder = $("#opcua-cert-target-folder").val();
115
+ if (!targetFolder) return;
116
+
117
+ var currentServerName = $("#node-input-serverName").val() || "";
118
+ var moveBtn = $("#opcua-cert-move-btn");
119
+ moveBtn.addClass("disabled").append(' <i class="fa fa-spinner fa-spin"></i>');
120
+
121
+ $.ajax({
122
+ url: "opc-ua-server/certificates/move",
123
+ type: "POST",
124
+ contentType: "application/json",
125
+ data: JSON.stringify({
126
+ serverName: currentServerName,
127
+ filename: selectedCertName,
128
+ fromFolder: selectedCertFolder,
129
+ toFolder: targetFolder
130
+ }),
131
+ success: function (res) {
132
+ moveBtn.removeClass("disabled").find("i.fa-spin").remove();
133
+ selectedCertName = "";
134
+ $("#opcua-cert-details").hide();
135
+ RED.notify("Certificate moved successfully.", "success");
136
+ fetchCertificates();
137
+ },
138
+ error: function (xhr, textStatus, errorThrown) {
139
+ moveBtn.removeClass("disabled").find("i.fa-spin").remove();
140
+ var errMsg = xhr.responseJSON && xhr.responseJSON.error ? xhr.responseJSON.error : "Failed to move certificate.";
141
+ RED.notify(errMsg, "error");
142
+ }
143
+ });
144
+ }
22
145
 
23
146
  function parseTree(rawValue, strict) {
24
147
  if (!rawValue) return { objects: [], folders: [], objectsTypes: [], enumerations: [], nameSpaces: [] };
@@ -174,19 +297,20 @@
174
297
  variableNodeId: alarm.variableNodeId ? String(alarm.variableNodeId) : "",
175
298
  type: type,
176
299
  enabled: alarm.enabled !== undefined ? !!alarm.enabled : true,
300
+ sendValue: alarm.sendValue !== undefined ? !!alarm.sendValue : true,
177
301
  severity: alarm.severity !== undefined ? alarm.severity : 500,
178
302
  description: alarm.description ? String(alarm.description) : "",
179
303
  displayName: alarm.displayName ? String(alarm.displayName) : "",
180
304
  nodeId: alarm.nodeId ? String(alarm.nodeId) : "",
181
305
  namespaceId: normalizeNamespaceId(alarm.namespaceId),
182
306
  accessPermission: normalizeAccessPermissionValues(alarm.accessPermission || alarm.accessPermissions),
183
- highHighLimit: alarm.highHighLimit !== undefined ? alarm.highHighLimit : 100,
307
+ highHighLimit: alarm.highHighLimit !== undefined ? alarm.highHighLimit : 90,
184
308
  highHighMessage: alarm.highHighMessage ? String(alarm.highHighMessage) : "High High alarm",
185
309
  highLimit: alarm.highLimit !== undefined ? alarm.highLimit : 80,
186
310
  highMessage: alarm.highMessage ? String(alarm.highMessage) : "High alarm",
187
311
  lowLimit: alarm.lowLimit !== undefined ? alarm.lowLimit : 20,
188
312
  lowMessage: alarm.lowMessage ? String(alarm.lowMessage) : "Low alarm",
189
- lowLowLimit: alarm.lowLowLimit !== undefined ? alarm.lowLowLimit : 0,
313
+ lowLowLimit: alarm.lowLowLimit !== undefined ? alarm.lowLowLimit : 10,
190
314
  lowLowMessage: alarm.lowLowMessage ? String(alarm.lowLowMessage) : "Low Low alarm",
191
315
  normalStateValue: alarm.normalStateValue !== undefined ? alarm.normalStateValue : 0,
192
316
  digitalMessage: alarm.digitalMessage ? String(alarm.digitalMessage) : "Digital alarm"
@@ -320,6 +444,7 @@
320
444
  function normalizeSearchTerm(value) { return String(value || "").trim().toLowerCase(); }
321
445
  function isExpanded(path, defaultValue) { if (expansionState[path] === undefined) expansionState[path] = !!defaultValue; return expansionState[path]; }
322
446
  function nodeClassFromPath(path) {
447
+ if (path && path.indexOf("virtual:") === 0) return "VisualFolder";
323
448
  var tokens = pathToTokens(path);
324
449
  if (!tokens.length) return "Object";
325
450
 
@@ -333,7 +458,18 @@
333
458
  if (collectionToken === "folders") return "Folder";
334
459
  return "Object";
335
460
  }
336
- function getNodeDisplayName(path) { var item = getAtPath(editorState, path); return item ? (item.name || "(unnamed)") : ""; }
461
+ function getVirtualNodeName(path) {
462
+ if (path === "virtual:Objects") return "Objects";
463
+ if (path === "virtual:Types") return "Types";
464
+ if (path === "virtual:Types.ObjectTypes") return "ObjectTypes";
465
+ if (path === "virtual:Types.DataTypes") return "DataTypes";
466
+ return "";
467
+ }
468
+ function getNodeDisplayName(path) {
469
+ if (path && path.indexOf("virtual:") === 0) return getVirtualNodeName(path);
470
+ var item = getAtPath(editorState, path);
471
+ return item ? (item.name || "(unnamed)") : "";
472
+ }
337
473
  function getNamespaceOptions() {
338
474
  return Array.isArray(editorState.nameSpaces) ? editorState.nameSpaces.slice().sort(function (left, right) { return left.id - right.id; }) : [];
339
475
  }
@@ -444,7 +580,7 @@
444
580
  function nodeIdSuffixFromValue(nodeId, defaultSuffix) {
445
581
  var raw = String(nodeId || "").trim();
446
582
  if (!raw) return defaultSuffix;
447
- var match = /^ns=\d+;s=(.*)$/.exec(raw);
583
+ var match = /^ns=\d+;[si]=(.*)$/.exec(raw);
448
584
  if (match) return match[1];
449
585
  return raw;
450
586
  }
@@ -485,8 +621,69 @@
485
621
  var item = getAtPath(editorState, path);
486
622
  if (isObjectTypeModelPath(path)) return buildGeneratedNodeIdForPath(path);
487
623
  var customNodeId = item && item.nodeId ? String(item.nodeId).trim() : "";
488
- var suffix = nodeIdSuffixFromValue(customNodeId, buildDefaultNodeIdSuffixFromEditorPath(path));
489
- return getNodeIdPrefix(getNodeNamespaceId(path)) + suffix;
624
+ if (customNodeId) {
625
+ if (/^ns=\d+;[si]=/.test(customNodeId)) {
626
+ return customNodeId;
627
+ }
628
+ return getNodeIdPrefix(getNodeNamespaceId(path)) + customNodeId;
629
+ }
630
+ return buildGeneratedNodeIdForPath(path);
631
+ }
632
+ function parseNodeId(nodeId, defaultSuffix) {
633
+ var raw = String(nodeId || "").trim();
634
+ if (!raw) {
635
+ return { type: "s", value: defaultSuffix };
636
+ }
637
+ var match = /^ns=\d+;([si])=(.*)$/.exec(raw);
638
+ if (match) {
639
+ return { type: match[1], value: match[2] };
640
+ }
641
+ return { type: "s", value: raw };
642
+ }
643
+ function updateNodeIdValueInputState(mode, type) {
644
+ var inputId = "#opcua-" + mode + "-nodeid-value";
645
+ var labelId = "#opcua-" + mode + "-nodeid-value-label";
646
+ var input = $(inputId);
647
+ var label = $(labelId);
648
+
649
+ if (type === "i") {
650
+ if (label.length) label.text("nodeId Value (Numeric)");
651
+ input.attr("type", "number");
652
+ input.attr("step", "1");
653
+ input.attr("placeholder", "Enter a number");
654
+ } else {
655
+ if (label.length) label.text("nodeId Value (String)");
656
+ input.attr("type", "text");
657
+ input.removeAttr("step");
658
+ if (mode === "create") {
659
+ input.attr("placeholder", "Leave blank for default (s)");
660
+ } else {
661
+ input.attr("placeholder", "Enter a string");
662
+ }
663
+ }
664
+ }
665
+ function saveDetailNodeId(path) {
666
+ if (!path) return;
667
+ var type = $("#opcua-detail-nodeid-type").val();
668
+ var rawVal = $("#opcua-detail-nodeid-value").val();
669
+ var nsId = getNodeNamespaceId(path);
670
+
671
+ var customNodeId = "";
672
+ if (rawVal) {
673
+ if (type === "i") {
674
+ var numVal = parseInt(rawVal, 10);
675
+ if (!isNaN(numVal)) {
676
+ customNodeId = "ns=" + nsId + ";i=" + numVal;
677
+ }
678
+ } else {
679
+ var defaultSuffix = buildDefaultNodeIdSuffixFromEditorPath(path);
680
+ var nextSuffix = String(rawVal).trim();
681
+ if (nextSuffix && nextSuffix !== defaultSuffix) {
682
+ customNodeId = "ns=" + nsId + ";s=" + nextSuffix;
683
+ }
684
+ }
685
+ }
686
+ updateNode(path, { nodeId: customNodeId });
490
687
  }
491
688
  function normalizeCustomNodeIdFromSuffix(path, suffix) {
492
689
  if (isObjectTypeModelPath(path)) return "";
@@ -527,6 +724,25 @@
527
724
  }
528
725
 
529
726
  function getChildrenByPath(path) {
727
+ if (path === "virtual:Objects") {
728
+ var children = [];
729
+ (editorState.folders || []).forEach(function (_, i) { children.push("folders." + i); });
730
+ (editorState.objects || []).forEach(function (_, i) { children.push("objects." + i); });
731
+ return children;
732
+ }
733
+ if (path === "virtual:Types") {
734
+ return ["virtual:Types.ObjectTypes", "virtual:Types.DataTypes"];
735
+ }
736
+ if (path === "virtual:Types.ObjectTypes") {
737
+ var children = [];
738
+ (editorState.objectsTypes || []).forEach(function (_, i) { children.push("objectsTypes." + i); });
739
+ return children;
740
+ }
741
+ if (path === "virtual:Types.DataTypes") {
742
+ var children = [];
743
+ (editorState.enumerations || []).forEach(function (_, i) { children.push("enumerations." + i); });
744
+ return children;
745
+ }
530
746
  var item = getAtPath(editorState, path);
531
747
  if (!item) return [];
532
748
  var children = [];
@@ -540,11 +756,7 @@
540
756
  }
541
757
 
542
758
  function getTopLevelPaths() {
543
- var paths = [];
544
- (editorState.folders || []).forEach(function (_, i) { paths.push("folders." + i); });
545
- (editorState.objects || []).forEach(function (_, i) { paths.push("objects." + i); });
546
- (editorState.objectsTypes || []).forEach(function (_, i) { paths.push("objectsTypes." + i); });
547
- (editorState.enumerations || []).forEach(function (_, i) { paths.push("enumerations." + i); });
759
+ var paths = ["virtual:Objects", "virtual:Types"];
548
760
  (editorState.nameSpaces || []).forEach(function (_, i) { paths.push("nameSpaces." + i); });
549
761
  return paths;
550
762
  }
@@ -556,6 +768,9 @@
556
768
 
557
769
  function nodeMatchesSearch(path) {
558
770
  if (!treeSearchTerm) return true;
771
+ if (path && path.indexOf("virtual:") === 0) {
772
+ return getVirtualNodeName(path).toLowerCase().indexOf(treeSearchTerm) !== -1;
773
+ }
559
774
  var item = getAtPath(editorState, path);
560
775
  if (!item) return false;
561
776
  var values = [path, item.name, item.displayName, item.description, nodeClassFromPath(path), item.type, item.value, item.id, item.namespaceId];
@@ -568,7 +783,7 @@
568
783
  }
569
784
 
570
785
  function iconForNodeClass(nodeClass) {
571
- if (nodeClass === "Folder") return "fa-folder";
786
+ if (nodeClass === "Folder" || nodeClass === "VisualFolder") return "fa-folder";
572
787
  if (nodeClass === "Object") return "fa-cube";
573
788
  if (nodeClass === "Variable") return "fa-tag";
574
789
  if (nodeClass === "ObjectType") return "fa-cubes";
@@ -594,7 +809,7 @@
594
809
  }
595
810
 
596
811
  function appendNodeToFrag(frag, path, depth, ancestorMatched) {
597
- var item = getAtPath(editorState, path);
812
+ var item = (path && path.indexOf("virtual:") === 0) ? {} : getAtPath(editorState, path);
598
813
  if (!item) return;
599
814
  var nodeClass = nodeClassFromPath(path);
600
815
  var hasChildren = nodeClass !== "Variable" && nodeClass !== "Alarm" && nodeClass !== "Namespace" && getChildrenByPath(path).length > 0;
@@ -607,11 +822,13 @@
607
822
  var row = document.createElement("div");
608
823
  row.className = "opcua-tree-row" + (path === selectedPath ? " is-selected" : "");
609
824
  row.setAttribute("data-path", path);
825
+ var label = (path && path.indexOf("virtual:") === 0) ? getVirtualNodeName(path) : (item.name || "(unnamed)");
826
+ var displayClass = (path && path.indexOf("virtual:") === 0) ? "Folder" : nodeClass;
610
827
  row.innerHTML = indents
611
828
  + '<span class="opcua-tree-twisty">' + (hasChildren ? '<i class="fa ' + (expanded ? "fa-caret-down" : "fa-caret-right") + '"></i>' : "") + "</span>"
612
829
  + '<span class="opcua-tree-icon"><i class="fa ' + iconForNodeClass(nodeClass) + '"></i></span>'
613
- + '<span class="opcua-tree-label">' + escapeHtml(item.name || "(unnamed)") + "</span>"
614
- + '<span class="opcua-tree-type">' + escapeHtml(nodeClass) + "</span>";
830
+ + '<span class="opcua-tree-label">' + escapeHtml(label) + "</span>"
831
+ + '<span class="opcua-tree-type">' + escapeHtml(displayClass) + "</span>";
615
832
  frag.appendChild(row);
616
833
 
617
834
  if (hasChildren && expanded) {
@@ -650,9 +867,28 @@
650
867
  function renderBreadcrumbs() {
651
868
  var el = $("#opcua-tree-breadcrumbs");
652
869
  if (!selectedPath) { el.text("No selection"); return; }
870
+ if (selectedPath.indexOf("virtual:") === 0) {
871
+ var parts = [];
872
+ if (selectedPath === "virtual:Objects") parts = ["Objects"];
873
+ else if (selectedPath === "virtual:Types") parts = ["Types"];
874
+ else if (selectedPath === "virtual:Types.ObjectTypes") parts = ["Types", "ObjectTypes"];
875
+ else if (selectedPath === "virtual:Types.DataTypes") parts = ["Types", "DataTypes"];
876
+ el.text(parts.join("."));
877
+ return;
878
+ }
653
879
  var tokens = pathToTokens(selectedPath);
654
880
  var cursor = [];
655
881
  var parts = [];
882
+ var rootToken = tokens[0];
883
+ if (rootToken === "folders" || rootToken === "objects") {
884
+ parts.push("Objects");
885
+ } else if (rootToken === "objectsTypes") {
886
+ parts.push("Types");
887
+ parts.push("ObjectTypes");
888
+ } else if (rootToken === "enumerations") {
889
+ parts.push("Types");
890
+ parts.push("DataTypes");
891
+ }
656
892
  tokens.forEach(function (token) {
657
893
  cursor.push(token);
658
894
  if (/^\d+$/.test(token)) parts.push(getNodeDisplayName(cursor.join(".")) || ("#" + token));
@@ -672,14 +908,38 @@
672
908
 
673
909
  function openCreateForm(path, kind) {
674
910
  if (!path) return;
675
- if (nodeClassFromPath(path) === "Variable" || nodeClassFromPath(path) === "Namespace") {
911
+ if (path.indexOf("virtual:") === 0) {
912
+ if (path === "virtual:Objects") {
913
+ if (kind !== "folder" && kind !== "object") {
914
+ RED.notify("Only Folders and Objects can be added directly under Objects", "warning");
915
+ return;
916
+ }
917
+ } else if (path === "virtual:Types.ObjectTypes") {
918
+ if (kind !== "objecttype") {
919
+ RED.notify("Only ObjectTypes can be added under ObjectTypes", "warning");
920
+ return;
921
+ }
922
+ } else if (path === "virtual:Types.DataTypes") {
923
+ if (kind !== "enumeration") {
924
+ RED.notify("Only Enumerations can be added under DataTypes", "warning");
925
+ return;
926
+ }
927
+ } else {
928
+ RED.notify("Cannot add children to this visual folder", "warning");
929
+ return;
930
+ }
931
+ } else if (nodeClassFromPath(path) === "Variable" || nodeClassFromPath(path) === "Namespace") {
676
932
  RED.notify("Selected item cannot have children", "warning");
677
933
  return;
678
934
  }
935
+ var parentSuffix = path.indexOf("virtual:") === 0 ? "" : buildDefaultNodeIdSuffixFromEditorPath(path);
936
+ var defaultName = kind === "variable" ? "newVariable" : kind === "enum-variable" ? "newEnumVariable" : "newObject";
937
+ var defaultSuffix = parentSuffix ? parentSuffix + "." + defaultName : defaultName;
938
+
679
939
  pendingCreate = {
680
940
  parentPath: path,
681
941
  kind: kind,
682
- name: kind === "variable" ? "newVariable" : kind === "enum-variable" ? "newEnumVariable" : kind === "folder" ? "newFolder" : kind === "objecttype" ? "newObjectType" : kind === "alarm" ? "newAlarm" : kind === "method" ? "newMethod" : "newObject",
942
+ name: defaultName,
683
943
  displayName: "",
684
944
  dataType: kind === "enum-variable" ? (getDefinedEnumerationNames()[0] || "") : "Int32",
685
945
  value: "",
@@ -689,16 +949,19 @@
689
949
  alarmType: "levelAlarm",
690
950
  variableNodeId: "",
691
951
  severity: 500,
692
- highHighLimit: 100,
952
+ sendValue: true,
953
+ highHighLimit: 90,
693
954
  highHighMessage: "High High alarm",
694
955
  highLimit: 80,
695
956
  highMessage: "High alarm",
696
957
  lowLimit: 20,
697
958
  lowMessage: "Low alarm",
698
- lowLowLimit: 0,
959
+ lowLowLimit: 10,
699
960
  lowLowMessage: "Low Low alarm",
700
961
  normalStateValue: 0,
701
- digitalMessage: "Digital alarm"
962
+ digitalMessage: "Digital alarm",
963
+ nodeIdType: "s",
964
+ nodeIdValue: (kind === "variable" || kind === "enum-variable") ? defaultSuffix : ""
702
965
  };
703
966
  renderDetails();
704
967
  }
@@ -707,27 +970,51 @@
707
970
  if (!pendingCreate) return;
708
971
  var parentPath = pendingCreate.parentPath;
709
972
  var kind = pendingCreate.kind;
710
- var branchTargetPath = (kind === "variable" || kind === "enum-variable")
711
- ? (parentPath + ".variables")
712
- : kind === "folder"
713
- ? (parentPath + ".folders")
714
- : kind === "objecttype"
715
- ? (parentPath + ".objectsTypes")
716
- : kind === "alarm"
717
- ? (parentPath + ".alarms")
718
- : kind === "method"
719
- ? (parentPath + ".methods")
720
- : (parentPath + ".objects");
973
+ var branchTargetPath;
974
+ if (parentPath.indexOf("virtual:") === 0) {
975
+ if (parentPath === "virtual:Objects") {
976
+ branchTargetPath = kind === "folder" ? "folders" : "objects";
977
+ } else if (parentPath === "virtual:Types.ObjectTypes") {
978
+ branchTargetPath = "objectsTypes";
979
+ } else {
980
+ return;
981
+ }
982
+ } else {
983
+ branchTargetPath = (kind === "variable" || kind === "enum-variable")
984
+ ? (parentPath + ".variables")
985
+ : kind === "folder"
986
+ ? (parentPath + ".folders")
987
+ : kind === "objecttype"
988
+ ? (parentPath + ".objectsTypes")
989
+ : kind === "alarm"
990
+ ? (parentPath + ".alarms")
991
+ : kind === "method"
992
+ ? (parentPath + ".methods")
993
+ : (parentPath + ".objects");
994
+ }
721
995
  var target = getAtPath(editorState, branchTargetPath);
722
996
  if (!Array.isArray(target)) return;
723
997
  if (kind === "variable" || kind === "enum-variable") {
998
+ var customNodeId = "";
999
+ var nsId = getNodeNamespaceId(parentPath);
1000
+ if (pendingCreate.nodeIdValue) {
1001
+ if (pendingCreate.nodeIdType === "i") {
1002
+ var numVal = parseInt(pendingCreate.nodeIdValue, 10);
1003
+ if (!isNaN(numVal)) {
1004
+ customNodeId = "ns=" + nsId + ";i=" + numVal;
1005
+ }
1006
+ } else {
1007
+ customNodeId = "ns=" + nsId + ";s=" + pendingCreate.nodeIdValue.trim();
1008
+ }
1009
+ }
724
1010
  target.push(normalizeVariable({
725
1011
  name: pendingCreate.name,
726
1012
  displayName: pendingCreate.displayName || "",
727
1013
  type: pendingCreate.dataType,
728
1014
  value: pendingCreate.value,
729
1015
  access: pendingCreate.access || "readwrite",
730
- accessPermission: pendingCreate.accessPermission
1016
+ accessPermission: pendingCreate.accessPermission,
1017
+ nodeId: customNodeId
731
1018
  }));
732
1019
  } else if (kind === "folder") {
733
1020
  target.push(normalizeBranch({ name: pendingCreate.name, displayName: pendingCreate.displayName || "", accessPermission: pendingCreate.accessPermission }));
@@ -742,6 +1029,7 @@
742
1029
  type: pendingCreate.alarmType,
743
1030
  variableNodeId: pendingCreate.variableNodeId,
744
1031
  severity: Number(pendingCreate.severity || 500),
1032
+ sendValue: pendingCreate.sendValue,
745
1033
  highHighLimit: pendingCreate.highHighLimit,
746
1034
  highHighMessage: pendingCreate.highHighMessage,
747
1035
  highLimit: pendingCreate.highLimit,
@@ -784,6 +1072,8 @@
784
1072
  panel.append('<div class="form-row"><label>dataType</label>' + typeHtml + '</div>');
785
1073
  panel.append('<div class="form-row"><label>Value</label><input type="text" id="opcua-create-value"></div>');
786
1074
  panel.append('<div class="form-row"><label>Access</label><select id="opcua-create-access"><option value="readwrite">readwrite</option><option value="readonly">readonly</option></select></div>');
1075
+ panel.append('<div class="form-row"><label>nodeId Type</label><select id="opcua-create-nodeid-type"><option value="s">s (String)</option><option value="i">i (Numeric)</option></select></div>');
1076
+ panel.append('<div class="form-row" id="opcua-create-nodeid-value-row"><label id="opcua-create-nodeid-value-label">nodeId Value</label><input type="text" id="opcua-create-nodeid-value" placeholder="Leave blank for default (s)"></div>');
787
1077
  }
788
1078
  if (pendingCreate.kind === "objecttype") {
789
1079
  panel.append('<div class="form-row"><label>objectsType</label>' + buildObjectTypeSelect("opcua-create-objectstype", pendingCreate.objectsType || "") + '</div>');
@@ -792,6 +1082,7 @@
792
1082
  panel.append('<div class="form-row"><label>alarmType</label><select id="opcua-create-alarm-type"><option value="levelAlarm">levelAlarm</option><option value="digitalAlarm">digitalAlarm</option></select></div>');
793
1083
  panel.append('<div class="form-row"><label>variablePath</label><input type="text" id="opcua-create-variable-nodeid"></div>');
794
1084
  panel.append('<div class="form-row"><label>severity</label><input type="number" id="opcua-create-severity"></div>');
1085
+ panel.append('<div class="form-row"><label for="opcua-create-sendvalue">Send alarm value</label><input type="checkbox" id="opcua-create-sendvalue" style="width: auto; flex: 0 0 auto; min-width: 0;"></div>');
795
1086
  if (pendingCreate.alarmType === "levelAlarm") {
796
1087
  panel.append('<div class="form-row"><label>highHighLimit</label><input type="number" id="opcua-create-highhighlimit"></div>');
797
1088
  panel.append('<div class="form-row"><label>highHighMessage</label><input type="text" id="opcua-create-highhighmessage"></div>');
@@ -812,6 +1103,11 @@
812
1103
  $("#opcua-create-name").val(pendingCreate.name);
813
1104
  $("#opcua-create-displayname").val(pendingCreate.displayName || "");
814
1105
  $("#opcua-create-accesspermission").val(normalizeAccessPermissionValues(pendingCreate.accessPermission));
1106
+ if (pendingCreate.kind === "variable" || pendingCreate.kind === "enum-variable") {
1107
+ $("#opcua-create-nodeid-type").val(pendingCreate.nodeIdType || "s");
1108
+ $("#opcua-create-nodeid-value").val(pendingCreate.nodeIdValue || "");
1109
+ updateNodeIdValueInputState("create", pendingCreate.nodeIdType || "s");
1110
+ }
815
1111
  $("#opcua-create-type").val(pendingCreate.dataType);
816
1112
  $("#opcua-create-value").val(pendingCreate.value);
817
1113
  $("#opcua-create-objectstype").val(pendingCreate.objectsType);
@@ -819,6 +1115,7 @@
819
1115
  $("#opcua-create-alarm-type").val(pendingCreate.alarmType);
820
1116
  $("#opcua-create-variable-nodeid").val(pendingCreate.variableNodeId);
821
1117
  $("#opcua-create-severity").val(pendingCreate.severity);
1118
+ $("#opcua-create-sendvalue").prop("checked", pendingCreate.sendValue !== false);
822
1119
  $("#opcua-create-highhighlimit").val(pendingCreate.highHighLimit);
823
1120
  $("#opcua-create-highhighmessage").val(pendingCreate.highHighMessage);
824
1121
  $("#opcua-create-highlimit").val(pendingCreate.highLimit);
@@ -832,6 +1129,11 @@
832
1129
  return;
833
1130
  }
834
1131
  if (!selectedPath) { panel.append('<div class="opcua-tree-empty">Select a node to edit browseName, namespace, nodeId, and description.</div>'); return; }
1132
+ if (selectedPath.indexOf("virtual:") === 0) {
1133
+ var visualName = getVirtualNodeName(selectedPath);
1134
+ panel.append('<div class="opcua-tree-empty">Visual folder: <strong>' + escapeHtml(visualName) + '</strong><br><span style="font-size: 11px; color: #666;">This folder is used strictly for visual organization within the modal.</span></div>');
1135
+ return;
1136
+ }
835
1137
  var item = getAtPath(editorState, selectedPath);
836
1138
  if (!item) { panel.append('<div class="opcua-tree-empty">Selected node not found.</div>'); return; }
837
1139
  var nodeClass = nodeClassFromPath(selectedPath);
@@ -883,7 +1185,18 @@
883
1185
  panel.append('<div class="form-row"><label>browseName</label><input type="text" id="opcua-detail-name"></div>');
884
1186
  panel.append('<div class="form-row"><label>nodeClass</label><input type="text" id="opcua-detail-class" readonly></div>');
885
1187
  panel.append('<div class="form-row"><label>namespace</label><select id="opcua-detail-namespace"></select></div>');
886
- panel.append('<div class="form-row"><label>nodeId</label><div class="opcua-nodeid-field"><span class="opcua-nodeid-prefix">' + getNodeIdPrefix(namespaceId) + '</span><input type="text" id="opcua-detail-nodeid"' + (nodeIdLocked ? ' readonly title="Generated automatically for object type models."' : '') + '><a href="#" id="opcua-detail-copy-nodeid" class="editor-button editor-button-small"><i class="fa fa-copy"></i> Copy</a></div></div>');
1188
+ if (nodeClass === "Variable" && !nodeIdLocked) {
1189
+ panel.append('<div class="form-row"><label>nodeId</label>' +
1190
+ '<div class="opcua-nodeid-field">' +
1191
+ '<span class="opcua-nodeid-prefix">ns=' + namespaceId + ';</span>' +
1192
+ '<select id="opcua-detail-nodeid-type" style="width: 70px; flex: 0 0 auto;"><option value="s">s</option><option value="i">i</option></select>' +
1193
+ '<span style="padding: 0 4px; flex: 0 0 auto;">=</span>' +
1194
+ '<input type="text" id="opcua-detail-nodeid-value" style="flex: 1 1 auto; font-family: monospace;">' +
1195
+ '<a href="#" id="opcua-detail-copy-nodeid" class="editor-button editor-button-small"><i class="fa fa-copy"></i> Copy</a>' +
1196
+ '</div></div>');
1197
+ } else {
1198
+ panel.append('<div class="form-row"><label>nodeId</label><div class="opcua-nodeid-field"><span class="opcua-nodeid-prefix">' + getNodeIdPrefix(namespaceId) + '</span><input type="text" id="opcua-detail-nodeid"' + (nodeIdLocked ? ' readonly title="Generated automatically for object type models."' : '') + '><a href="#" id="opcua-detail-copy-nodeid" class="editor-button editor-button-small"><i class="fa fa-copy"></i> Copy</a></div></div>');
1199
+ }
887
1200
  panel.append('<div class="form-row"><label>Description</label><input type="text" id="opcua-detail-description"></div>');
888
1201
  panel.append('<div class="form-row"><label>displayName</label><input type="text" id="opcua-detail-displayname" placeholder="Leave blank to use browseName"></div>');
889
1202
  panel.append('<div class="form-row"><label>accessPermission</label>' + buildAccessPermissionSelect("opcua-detail-accesspermission", item.accessPermission || ["public"]) + '</div>');
@@ -942,6 +1255,7 @@
942
1255
  panel.append('<div class="form-row"><label>alarmType</label><select id="opcua-detail-alarm-type"><option value="levelAlarm">levelAlarm</option><option value="digitalAlarm">digitalAlarm</option></select></div>');
943
1256
  panel.append('<div class="form-row"><label>variablePath</label><input type="text" id="opcua-detail-variable-nodeid"></div>');
944
1257
  panel.append('<div class="form-row"><label>severity</label><input type="number" id="opcua-detail-severity"></div>');
1258
+ panel.append('<div class="form-row"><label for="opcua-detail-sendvalue">Send alarm value</label><input type="checkbox" id="opcua-detail-sendvalue" style="width: auto; flex: 0 0 auto; min-width: 0;"></div>');
945
1259
  if ((item.type || "levelAlarm") === "levelAlarm") {
946
1260
  panel.append('<div class="form-row"><label>highHighLimit</label><input type="number" id="opcua-detail-highhighlimit"></div>');
947
1261
  panel.append('<div class="form-row"><label>highHighMessage</label><input type="text" id="opcua-detail-highhighmessage"></div>');
@@ -959,7 +1273,14 @@
959
1273
  panel.append('<div class="form-row"><label style="width:90px;">Actions</label><div><a href="#" id="opcua-detail-edit" class="editor-button editor-button-small"><i class="fa fa-pencil"></i> Edit</a> <a href="#" id="opcua-detail-remove" class="editor-button editor-button-small"><i class="fa fa-trash"></i> Remove</a></div></div>');
960
1274
  $("#opcua-detail-name").val(item.name || "");
961
1275
  $("#opcua-detail-class").val(nodeClass);
962
- $("#opcua-detail-nodeid").val(nodeIdLocked ? buildDefaultNodeIdSuffixFromEditorPath(selectedPath) : nodeIdSuffix);
1276
+ if (nodeClass === "Variable" && !nodeIdLocked) {
1277
+ var parsed = parseNodeId(item.nodeId, buildDefaultNodeIdSuffixFromEditorPath(selectedPath));
1278
+ $("#opcua-detail-nodeid-type").val(parsed.type);
1279
+ $("#opcua-detail-nodeid-value").val(parsed.value);
1280
+ updateNodeIdValueInputState("detail", parsed.type);
1281
+ } else {
1282
+ $("#opcua-detail-nodeid").val(nodeIdLocked ? buildDefaultNodeIdSuffixFromEditorPath(selectedPath) : nodeIdSuffix);
1283
+ }
963
1284
  $("#opcua-detail-description").val(item.description || "");
964
1285
  namespaceOptions.forEach(function (option) {
965
1286
  $("#opcua-detail-namespace").append($("<option></option>").val(option.id).text(getNamespaceLabel(option.id)));
@@ -973,13 +1294,14 @@
973
1294
  $("#opcua-detail-alarm-type").val(item.type || "levelAlarm");
974
1295
  $("#opcua-detail-variable-nodeid").val(item.variableNodeId || "");
975
1296
  $("#opcua-detail-severity").val(item.severity !== undefined ? item.severity : 500);
976
- $("#opcua-detail-highhighlimit").val(item.highHighLimit !== undefined ? item.highHighLimit : 100);
1297
+ $("#opcua-detail-sendvalue").prop("checked", item.sendValue !== false);
1298
+ $("#opcua-detail-highhighlimit").val(item.highHighLimit !== undefined ? item.highHighLimit : 90);
977
1299
  $("#opcua-detail-highhighmessage").val(item.highHighMessage || "High High alarm");
978
1300
  $("#opcua-detail-highlimit").val(item.highLimit !== undefined ? item.highLimit : 80);
979
1301
  $("#opcua-detail-highmessage").val(item.highMessage || "High alarm");
980
1302
  $("#opcua-detail-lowlimit").val(item.lowLimit !== undefined ? item.lowLimit : 20);
981
1303
  $("#opcua-detail-lowmessage").val(item.lowMessage || "Low alarm");
982
- $("#opcua-detail-lowlowlimit").val(item.lowLowLimit !== undefined ? item.lowLowLimit : 0);
1304
+ $("#opcua-detail-lowlowlimit").val(item.lowLowLimit !== undefined ? item.lowLowLimit : 10);
983
1305
  $("#opcua-detail-lowlowmessage").val(item.lowLowMessage || "Low Low alarm");
984
1306
  $("#opcua-detail-normalstatevalue").val(item.normalStateValue !== undefined ? item.normalStateValue : 0);
985
1307
  $("#opcua-detail-digitalmessage").val(item.digitalMessage || "Digital alarm");
@@ -993,6 +1315,10 @@
993
1315
 
994
1316
  function removeNode(path) {
995
1317
  if (!path) return;
1318
+ if (path.indexOf("virtual:") === 0) {
1319
+ RED.notify("Visual folders cannot be removed.", "warning");
1320
+ return;
1321
+ }
996
1322
  if (nodeClassFromPath(path) === "Namespace") {
997
1323
  var namespaceItem = getAtPath(editorState, path);
998
1324
  if (normalizeNamespaceId(namespaceItem && namespaceItem.id) === DEFAULT_NAMESPACE_ID) {
@@ -1246,8 +1572,48 @@
1246
1572
 
1247
1573
  $(document).on("contextmenu", ".opcua-tree-row", function (event) {
1248
1574
  event.preventDefault();
1249
- selectNode($(this).attr("data-path"));
1250
- $("#opcua-tree-context-menu").css({ left: event.clientX + "px", top: event.clientY + "px" }).show();
1575
+ var path = $(this).attr("data-path");
1576
+ selectNode(path);
1577
+
1578
+ var contextMenu = $("#opcua-tree-context-menu");
1579
+ contextMenu.find("a").hide();
1580
+
1581
+ if (path.indexOf("virtual:") === 0) {
1582
+ if (path === "virtual:Objects") {
1583
+ contextMenu.find('[data-action="add-folder"]').show();
1584
+ contextMenu.find('[data-action="add-object"]').show();
1585
+ } else if (path === "virtual:Types.ObjectTypes") {
1586
+ contextMenu.find('[data-action="add-objecttype"]').show();
1587
+ } else if (path === "virtual:Types.DataTypes") {
1588
+ contextMenu.find('[data-action="add-enumeration"]').show();
1589
+ }
1590
+ } else {
1591
+ var nodeClass = nodeClassFromPath(path);
1592
+ if (nodeClass === "Folder" || nodeClass === "Object" || nodeClass === "ObjectType") {
1593
+ contextMenu.find('[data-action="add-folder"]').show();
1594
+ contextMenu.find('[data-action="add-object"]').show();
1595
+ contextMenu.find('[data-action="add-variable"]').show();
1596
+ contextMenu.find('[data-action="add-enum-variable"]').show();
1597
+ contextMenu.find('[data-action="add-alarm"]').show();
1598
+ contextMenu.find('[data-action="add-method"]').show();
1599
+ contextMenu.find('[data-action="edit"]').show();
1600
+ contextMenu.find('[data-action="remove"]').show();
1601
+ } else if (nodeClass === "Enumeration") {
1602
+ contextMenu.find('[data-action="edit"]').show();
1603
+ contextMenu.find('[data-action="remove"]').show();
1604
+ } else if (nodeClass === "Namespace") {
1605
+ contextMenu.find('[data-action="edit"]').show();
1606
+ var item = getAtPath(editorState, path);
1607
+ if (item && normalizeNamespaceId(item.id) !== DEFAULT_NAMESPACE_ID) {
1608
+ contextMenu.find('[data-action="remove"]').show();
1609
+ }
1610
+ } else {
1611
+ contextMenu.find('[data-action="edit"]').show();
1612
+ contextMenu.find('[data-action="remove"]').show();
1613
+ }
1614
+ }
1615
+
1616
+ contextMenu.css({ left: event.clientX + "px", top: event.clientY + "px" }).show();
1251
1617
  });
1252
1618
 
1253
1619
  $(document).on("click", function () { $("#opcua-tree-context-menu").hide(); });
@@ -1344,13 +1710,25 @@
1344
1710
  var item = getAtPath(editorState, selectedPath);
1345
1711
  if (!item) return;
1346
1712
  item.namespaceId = nextNamespaceId;
1347
- item.nodeId = normalizeCustomNodeIdFromSuffix(selectedPath, $("#opcua-detail-nodeid").val());
1713
+ if (nodeClassFromPath(selectedPath) === "Variable" && !isObjectTypeModelPath(selectedPath)) {
1714
+ saveDetailNodeId(selectedPath);
1715
+ } else {
1716
+ item.nodeId = normalizeCustomNodeIdFromSuffix(selectedPath, $("#opcua-detail-nodeid").val());
1717
+ }
1348
1718
  syncStateToJson(false);
1349
1719
  renderTree();
1350
1720
  renderBreadcrumbs();
1351
1721
  renderDetails();
1352
1722
  });
1353
1723
  $(document).on("input", "#opcua-detail-nodeid", function () { updateNode(selectedPath, { nodeId: normalizeCustomNodeIdFromSuffix(selectedPath, $(this).val()) }); });
1724
+ $(document).on("change", "#opcua-detail-nodeid-type", function () {
1725
+ var type = $(this).val();
1726
+ updateNodeIdValueInputState("detail", type);
1727
+ saveDetailNodeId(selectedPath);
1728
+ });
1729
+ $(document).on("input", "#opcua-detail-nodeid-value", function () {
1730
+ saveDetailNodeId(selectedPath);
1731
+ });
1354
1732
  $(document).on("click", "#opcua-detail-copy-nodeid", function (event) {
1355
1733
  event.preventDefault();
1356
1734
  copyNodeIdValue(buildDisplayNodeIdFromEditorPath(selectedPath));
@@ -1389,6 +1767,7 @@
1389
1767
  });
1390
1768
  $(document).on("input", "#opcua-detail-variable-nodeid", function () { updateNode(selectedPath, { variableNodeId: $(this).val() }); });
1391
1769
  $(document).on("input", "#opcua-detail-severity", function () { updateNode(selectedPath, { severity: Number($(this).val() || 0) }); });
1770
+ $(document).on("change", "#opcua-detail-sendvalue", function () { updateNode(selectedPath, { sendValue: $(this).is(":checked") }); });
1392
1771
  $(document).on("input", "#opcua-detail-highhighlimit", function () { updateNode(selectedPath, { highHighLimit: Number($(this).val() || 0) }); });
1393
1772
  $(document).on("input", "#opcua-detail-highhighmessage", function () { updateNode(selectedPath, { highHighMessage: $(this).val() }); });
1394
1773
  $(document).on("input", "#opcua-detail-highlimit", function () { updateNode(selectedPath, { highLimit: Number($(this).val() || 0) }); });
@@ -1399,8 +1778,35 @@
1399
1778
  $(document).on("input", "#opcua-detail-lowlowmessage", function () { updateNode(selectedPath, { lowLowMessage: $(this).val() }); });
1400
1779
  $(document).on("input", "#opcua-detail-normalstatevalue", function () { updateNode(selectedPath, { normalStateValue: Number($(this).val() || 0) }); });
1401
1780
  $(document).on("input", "#opcua-detail-digitalmessage", function () { updateNode(selectedPath, { digitalMessage: $(this).val() }); });
1402
- $(document).on("input", "#opcua-create-name", function () { if (pendingCreate) pendingCreate.name = $(this).val(); });
1781
+ $(document).on("input", "#opcua-create-name", function () {
1782
+ if (pendingCreate) {
1783
+ var oldName = pendingCreate.name;
1784
+ var nextName = $(this).val();
1785
+ pendingCreate.name = nextName;
1786
+
1787
+ if (pendingCreate.kind === "variable" || pendingCreate.kind === "enum-variable") {
1788
+ if (pendingCreate.nodeIdType === "s") {
1789
+ var parentSuffix = buildDefaultNodeIdSuffixFromEditorPath(pendingCreate.parentPath);
1790
+ var oldAutoSuffix = parentSuffix ? parentSuffix + "." + oldName : oldName;
1791
+ var nextAutoSuffix = parentSuffix ? parentSuffix + "." + nextName : nextName;
1792
+
1793
+ if (!pendingCreate.nodeIdValue || pendingCreate.nodeIdValue === oldAutoSuffix) {
1794
+ pendingCreate.nodeIdValue = nextAutoSuffix;
1795
+ $("#opcua-create-nodeid-value").val(nextAutoSuffix);
1796
+ }
1797
+ }
1798
+ }
1799
+ }
1800
+ });
1403
1801
  $(document).on("input", "#opcua-create-displayname", function () { if (pendingCreate) pendingCreate.displayName = $(this).val(); });
1802
+ $(document).on("change", "#opcua-create-nodeid-type", function () {
1803
+ var type = $(this).val();
1804
+ if (pendingCreate) pendingCreate.nodeIdType = type;
1805
+ updateNodeIdValueInputState("create", type);
1806
+ });
1807
+ $(document).on("input", "#opcua-create-nodeid-value", function () {
1808
+ if (pendingCreate) pendingCreate.nodeIdValue = $(this).val();
1809
+ });
1404
1810
  $(document).on("change", "#opcua-create-accesspermission", function () { if (pendingCreate) pendingCreate.accessPermission = normalizeAccessPermissionValues($(this).val()); });
1405
1811
  $(document).on("change", "#opcua-create-type", function () { if (pendingCreate) pendingCreate.dataType = $(this).val(); });
1406
1812
  $(document).on("input", "#opcua-create-value", function () { if (pendingCreate) pendingCreate.value = $(this).val(); });
@@ -1414,6 +1820,7 @@
1414
1820
  });
1415
1821
  $(document).on("input", "#opcua-create-variable-nodeid", function () { if (pendingCreate) pendingCreate.variableNodeId = $(this).val(); });
1416
1822
  $(document).on("input", "#opcua-create-severity", function () { if (pendingCreate) pendingCreate.severity = Number($(this).val() || 0); });
1823
+ $(document).on("change", "#opcua-create-sendvalue", function () { if (pendingCreate) pendingCreate.sendValue = $(this).is(":checked"); });
1417
1824
  $(document).on("input", "#opcua-create-highhighlimit", function () { if (pendingCreate) pendingCreate.highHighLimit = Number($(this).val() || 0); });
1418
1825
  $(document).on("input", "#opcua-create-highhighmessage", function () { if (pendingCreate) pendingCreate.highHighMessage = $(this).val(); });
1419
1826
  $(document).on("input", "#opcua-create-highlimit", function () { if (pendingCreate) pendingCreate.highLimit = Number($(this).val() || 0); });
@@ -1475,9 +1882,12 @@
1475
1882
  color: "#d9edf7",
1476
1883
  credentials: { username: { type: "text" }, password: { type: "password" }, users: { type: "text" }, groups: { type: "text" } },
1477
1884
  defaults: {
1478
- name: { value: "" }, resourcePath: { value: "/" }, serverName: { value: "Node-RED OPC UA Server", required: true }, allowAnonymous: { value: true },
1885
+ name: { value: "" }, resourcePath: { value: "/" }, serverName: { value: "Node-RED OPC UA Server", required: true }, allowAnonymous: { value: true }, automaticallyAcceptUnknownCertificate: { value: true },
1479
1886
  port: { value: 4840, required: true, validate: function (value) { var port = Number(value); return Number.isInteger(port) && port > 0 && port < 65536; } },
1480
1887
  maxConnections: { value: 10, required: true, validate: function (value) { var n = Number(value); return Number.isInteger(n) && n > 0; } },
1888
+ minSessionTimeout: { value: 100, required: true, validate: function (value) { var n = Number(value); return Number.isInteger(n) && n >= 0; } },
1889
+ defaultSessionTimeout: { value: 30000, required: true, validate: function (value) { var n = Number(value); return Number.isInteger(n) && n >= 0; } },
1890
+ maxSessionTimeout: { value: 3000000, required: true, validate: function (value) { var n = Number(value); return Number.isInteger(n) && n >= 0; } },
1481
1891
  securityPolicy: { value: "None", required: true }, securityMode: { value: "None", required: true }, namespaceUri: { value: "urn:node-red:opc-ua-server", required: true },
1482
1892
  tree: {
1483
1893
  value: "{\n \"folders\": [],\n \"objects\": [],\n \"objectsTypes\": [],\n \"nameSpaces\": [\n {\n \"id\": 2,\n \"name\": \"urn:node-red:opc-ua-server\"\n }\n ]\n}",
@@ -1493,6 +1903,15 @@
1493
1903
  label: function () { return this.name || this.serverName || "opc-ua-server"; },
1494
1904
  oneditprepare: function () {
1495
1905
  var node = this;
1906
+ if (!$("#node-input-minSessionTimeout").val()) {
1907
+ $("#node-input-minSessionTimeout").val(100);
1908
+ }
1909
+ if (!$("#node-input-defaultSessionTimeout").val()) {
1910
+ $("#node-input-defaultSessionTimeout").val(30000);
1911
+ }
1912
+ if (!$("#node-input-maxSessionTimeout").val()) {
1913
+ $("#node-input-maxSessionTimeout").val(3000000);
1914
+ }
1496
1915
  editorState = cloneTree(normalizeTree(parseTree(node.tree)));
1497
1916
  authGroups = normalizeAuthGroups($("#node-input-groups").val());
1498
1917
  authUsers = normalizeAuthUsers($("#node-input-users").val());
@@ -1501,6 +1920,35 @@
1501
1920
  $("#node-input-namespaceUri").val(defaultNamespaceEntry.name);
1502
1921
  }
1503
1922
  syncAuthCredentialFields();
1923
+ $("#node-input-securityPolicy").typedInput({
1924
+ types: [
1925
+ {
1926
+ value: "securityPolicy",
1927
+ multiple: "true",
1928
+ options: [
1929
+ { value: "None", label: "None" },
1930
+ { value: "Basic128Rsa15", label: "Basic128Rsa15" },
1931
+ { value: "Basic256", label: "Basic256" },
1932
+ { value: "Basic256Sha256", label: "Basic256Sha256" },
1933
+ { value: "Aes128_Sha256_RsaOaep", label: "Aes128_Sha256_RsaOaep" },
1934
+ { value: "Aes256_Sha256_RsaPss", label: "Aes256_Sha256_RsaPss" }
1935
+ ]
1936
+ }
1937
+ ]
1938
+ });
1939
+ $("#node-input-securityMode").typedInput({
1940
+ types: [
1941
+ {
1942
+ value: "securityMode",
1943
+ multiple: "true",
1944
+ options: [
1945
+ { value: "None", label: "None" },
1946
+ { value: "Sign", label: "Sign" },
1947
+ { value: "SignAndEncrypt", label: "SignAndEncrypt" }
1948
+ ]
1949
+ }
1950
+ ]
1951
+ });
1504
1952
  updateTreeField(prettyTree(editorState), false);
1505
1953
  $("#node-input-tree-editor").typedInput({ type: "json", types: ["json"] });
1506
1954
  $("#node-input-tree-editor").typedInput("value", prettyTree(editorState));
@@ -1524,6 +1972,35 @@
1524
1972
  $("#node-input-add-auth-group").off("click").on("click", function (event) { event.preventDefault(); addAuthGroup(); });
1525
1973
  $("#node-input-add-auth-user").off("click").on("click", function (event) { event.preventDefault(); addAuthUser(); });
1526
1974
 
1975
+ $("#node-input-open-cert-modal").off("click").on("click", function (event) { event.preventDefault(); openCertModal(); });
1976
+ $("#node-input-close-cert-modal").off("click").on("click", function (event) { event.preventDefault(); closeCertModal(); });
1977
+ $("#node-input-cert-modal").off("click").on("click", function (event) { if (event.target === this) closeCertModal(); });
1978
+
1979
+ $("#node-input-open-settings-modal").off("click").on("click", function (event) { event.preventDefault(); openSettingsModal(); });
1980
+ $("#node-input-close-settings-modal").off("click").on("click", function (event) { event.preventDefault(); closeSettingsModal(); });
1981
+ $("#node-input-settings-modal").off("click").on("click", function (event) { if (event.target === this) closeSettingsModal(); });
1982
+
1983
+ $("#opcua-cert-folders").off("click", ".opcua-cert-item").on("click", ".opcua-cert-item", function () {
1984
+ $("#opcua-cert-folders .opcua-cert-item").removeClass("is-selected");
1985
+ $(this).addClass("is-selected");
1986
+ selectedCertFolder = $(this).attr("data-folder");
1987
+ selectedCertName = "";
1988
+ $("#opcua-cert-details").hide();
1989
+ renderCertificatesList();
1990
+ });
1991
+
1992
+ $("#opcua-cert-files").off("click", ".opcua-cert-item").on("click", ".opcua-cert-item", function () {
1993
+ $("#opcua-cert-files .opcua-cert-item").removeClass("is-selected");
1994
+ $(this).addClass("is-selected");
1995
+ selectedCertName = $(this).attr("data-name");
1996
+ showCertificateDetails();
1997
+ });
1998
+
1999
+ $("#opcua-cert-move-btn").off("click").on("click", function (event) {
2000
+ event.preventDefault();
2001
+ moveCertificate();
2002
+ });
2003
+
1527
2004
  $("#node-input-tree-search").off("input").on("input", debounce(function () {
1528
2005
  treeSearchValue = $(this).val(); treeSearchTerm = normalizeSearchTerm(treeSearchValue);
1529
2006
  $("#node-input-tree-search-clear").toggle(!!treeSearchTerm); renderTree();
@@ -1546,6 +2023,10 @@
1546
2023
  closeAuthModal();
1547
2024
  return;
1548
2025
  }
2026
+ if ($("#node-input-cert-modal").is(":visible")) {
2027
+ closeCertModal();
2028
+ return;
2029
+ }
1549
2030
  closeTreeModal();
1550
2031
  });
1551
2032