@vitormnm/node-red-simple-opcua 1.6.3 → 1.8.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 (45) hide show
  1. package/README.md +104 -136
  2. package/client/lib/opcua-client-browser.js +254 -11
  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/lib/opcua-client-write-service.js +14 -4
  6. package/client/opcua-client-config.html +118 -1
  7. package/client/opcua-client-config.js +112 -9
  8. package/client/opcua-client-help.html +6 -0
  9. package/client/opcua-client-utils.js +158 -10
  10. package/client/opcua-client.html +8 -0
  11. package/client/opcua-client.js +97 -1
  12. package/client/view/opcua-client.js +106 -14
  13. package/examples/flows_simple_opc.json +1 -1
  14. package/package.json +2 -2
  15. package/server/lib/opcua-address-space-alarm.js +95 -32
  16. package/server/lib/opcua-address-space-builder.js +717 -59
  17. package/server/lib/opcua-config.js +110 -35
  18. package/server/lib/opcua-server-events-child.js +31 -5
  19. package/server/lib/opcua-server-runtime-child.js +424 -27
  20. package/server/lib/opcua-server-runtime.js +52 -5
  21. package/server/lib/opcua-server-status-child.js +46 -15
  22. package/server/nodered/simple_opcua/server/certificates/mutex +0 -0
  23. package/server/nodered/simple_opcua/server/certificates/own/certs/server_selfsigned_cert_2048.pem +25 -0
  24. package/server/nodered/simple_opcua/server/certificates/own/certs/server_selfsigned_cert_2048.pem.mutex +0 -0
  25. package/server/nodered/simple_opcua/server/certificates/own/openssl.cnf +72 -0
  26. package/server/nodered/simple_opcua/server/certificates/own/private/private_key.pem +28 -0
  27. package/server/nodered/simple_opcua/server/certificates/trusted/certs/NodeOPCUA-Client@tuf[c5a9e20a8b680cdff76aaf0165bb3c9318da37a5].pem +25 -0
  28. package/server/nodered/simple_opcua/server/myServer1/mutex +0 -0
  29. package/server/nodered/simple_opcua/server/myServer1/own/certs/server_selfsigned_cert_2048.pem +25 -0
  30. package/server/nodered/simple_opcua/server/myServer1/own/certs/server_selfsigned_cert_2048.pem.mutex +0 -0
  31. package/server/nodered/simple_opcua/server/myServer1/own/openssl.cnf +72 -0
  32. package/server/nodered/simple_opcua/server/myServer1/own/private/private_key.pem +28 -0
  33. package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[91e520c64ff891c67168f08a46dd194071e15dae].pem +25 -0
  34. package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[98ae95da627cea4c500753c319161a3554ee38d7].pem +25 -0
  35. package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[aef8d7a1cfba13d84189a0bcf1694208fc51a7f9].pem +25 -0
  36. package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[c5a9e20a8b680cdff76aaf0165bb3c9318da37a5].pem +25 -0
  37. package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[ebdf9acf1d02e347917a14108d3144799c638ea3].pem +25 -0
  38. package/server/opcua-server-io.html +93 -1
  39. package/server/opcua-server-io.js +153 -29
  40. package/server/opcua-server-registry.js +8 -2
  41. package/server/opcua-server.css +64 -0
  42. package/server/opcua-server.html +168 -44
  43. package/server/opcua-server.js +115 -5
  44. package/server/view/opcua-server.css +100 -6
  45. package/server/view/opcua-server.js +746 -48
@@ -26,14 +26,18 @@ class OpcUaServerConfigParser {
26
26
  serverName: config.serverName || DEFAULT_SERVER_NAME,
27
27
  port: normalizePort(config.port),
28
28
  maxConnections: this.normalizeMaxConnections(config.maxConnections),
29
+ minSessionTimeout: this.normalizeSessionTimeout(config.minSessionTimeout),
30
+ defaultSessionTimeout: this.normalizeSessionTimeout(config.defaultSessionTimeout),
31
+ maxSessionTimeout: this.normalizeSessionTimeout(config.maxSessionTimeout),
29
32
  namespaceUri: config.namespaceUri || DEFAULT_NAMESPACE_URI,
30
33
  resourcePath: config.resourcePath || DEFAULT_RESOURCE_PATH,
31
34
  treeConfig: this.parseTreeConfig(config.tree),
32
35
  allowAnonymous: this.normalizeAllowAnonymous(config.allowAnonymous),
36
+ automaticallyAcceptUnknownCertificate: this.normalizeAutomaticallyAcceptUnknownCertificate(config.automaticallyAcceptUnknownCertificate),
33
37
  groups: auth.groups,
34
38
  users: auth.users,
35
- securityPolicy: security.securityPolicy,
36
- securityMode: security.securityMode
39
+ securityPolicies: security.securityPolicies,
40
+ securityModes: security.securityModes
37
41
  };
38
42
  }
39
43
 
@@ -509,17 +513,18 @@ class OpcUaServerConfigParser {
509
513
  nodeId: this.normalizeOptionalNodeId(alarmConfig.nodeId),
510
514
  namespaceId: this.normalizeNamespaceId(alarmConfig.namespaceId),
511
515
  accessPermission: this.normalizeAccessPermissions(alarmConfig.accessPermission || alarmConfig.accessPermissions),
512
- enabled: typeof alarmConfig.enabled === "boolean" ? alarmConfig.enabled : true
516
+ enabled: typeof alarmConfig.enabled === "boolean" ? alarmConfig.enabled : true,
517
+ sendValue: typeof alarmConfig.sendValue === "boolean" ? alarmConfig.sendValue : true
513
518
  };
514
519
 
515
520
  if (type === "levelAlarm") {
516
- base.highHighLimit = Number.isFinite(Number(alarmConfig.highHighLimit)) ? Number(alarmConfig.highHighLimit) : 100;
521
+ base.highHighLimit = Number.isFinite(Number(alarmConfig.highHighLimit)) ? Number(alarmConfig.highHighLimit) : 90;
517
522
  base.highHighMessage = typeof alarmConfig.highHighMessage === "string" ? alarmConfig.highHighMessage : "High High alarm";
518
523
  base.highLimit = Number.isFinite(Number(alarmConfig.highLimit)) ? Number(alarmConfig.highLimit) : 80;
519
524
  base.highMessage = typeof alarmConfig.highMessage === "string" ? alarmConfig.highMessage : "High alarm";
520
525
  base.lowLimit = Number.isFinite(Number(alarmConfig.lowLimit)) ? Number(alarmConfig.lowLimit) : 20;
521
526
  base.lowMessage = typeof alarmConfig.lowMessage === "string" ? alarmConfig.lowMessage : "Low alarm";
522
- base.lowLowLimit = Number.isFinite(Number(alarmConfig.lowLowLimit)) ? Number(alarmConfig.lowLowLimit) : 0;
527
+ base.lowLowLimit = Number.isFinite(Number(alarmConfig.lowLowLimit)) ? Number(alarmConfig.lowLowLimit) : 10;
523
528
  base.lowLowMessage = typeof alarmConfig.lowLowMessage === "string" ? alarmConfig.lowLowMessage : "Low Low alarm";
524
529
  } else if (type === "digitalAlarm") {
525
530
  base.normalStateValue = Number.isFinite(Number(alarmConfig.normalStateValue)) ? Number(alarmConfig.normalStateValue) : 0;
@@ -643,6 +648,17 @@ class OpcUaServerConfigParser {
643
648
  return value !== false;
644
649
  }
645
650
 
651
+ normalizeAutomaticallyAcceptUnknownCertificate(value) {
652
+ if (value === undefined) {
653
+ return true;
654
+ }
655
+ if (typeof value === "string") {
656
+ return value !== "false";
657
+ }
658
+
659
+ return value !== false;
660
+ }
661
+
646
662
  normalizeMaxConnections(value) {
647
663
  const parsed = Number(value);
648
664
  if (!Number.isInteger(parsed) || parsed <= 0) {
@@ -651,50 +667,109 @@ class OpcUaServerConfigParser {
651
667
  return parsed;
652
668
  }
653
669
 
654
- applySecuritySettings(policy, mode) {
655
- const rawPolicy = typeof policy === "string" ? policy.trim() : "None";
656
- const rawMode = typeof mode === "string" ? mode.trim() : "None";
670
+ normalizeSessionTimeout(value) {
671
+ if (value === undefined || value === null || value === "") {
672
+ return undefined;
673
+ }
674
+ const parsed = Number(value);
675
+ if (!Number.isInteger(parsed) || parsed < 0) {
676
+ return undefined;
677
+ }
678
+ return parsed;
679
+ }
657
680
 
658
- let securityPolicy = Object.prototype.hasOwnProperty.call(SECURITY_POLICY_MAP, rawPolicy)
659
- ? SECURITY_POLICY_MAP[rawPolicy]
660
- : SECURITY_POLICY_MAP.None;
661
- let securityMode = Object.prototype.hasOwnProperty.call(SECURITY_MODE_MAP, rawMode)
662
- ? SECURITY_MODE_MAP[rawMode]
663
- : SECURITY_MODE_MAP.None;
681
+ applySecuritySettings(policies, modes) {
682
+ // policies and modes can be comma-separated strings, e.g. "None,Basic256Sha256"
683
+ const rawPolicies = typeof policies === "string" ? policies.split(",").map(p => p.trim()).filter(Boolean) : ["None"];
684
+ const rawModes = typeof modes === "string" ? modes.split(",").map(m => m.trim()).filter(Boolean) : ["None"];
664
685
 
665
- if (securityMode === MessageSecurityMode.None) {
666
- securityPolicy = SecurityPolicy.None;
667
- if (rawPolicy !== "None") {
668
- this.node.warn("Security policy adjusted to None because security mode is None");
686
+ let securityPolicies = [];
687
+ rawPolicies.forEach(policy => {
688
+ if (Object.prototype.hasOwnProperty.call(SECURITY_POLICY_MAP, policy)) {
689
+ securityPolicies.push(SECURITY_POLICY_MAP[policy]);
690
+ }
691
+ });
692
+ if (securityPolicies.length === 0) {
693
+ securityPolicies.push(SECURITY_POLICY_MAP.None);
694
+ }
695
+
696
+ let securityModes = [];
697
+ rawModes.forEach(mode => {
698
+ if (Object.prototype.hasOwnProperty.call(SECURITY_MODE_MAP, mode)) {
699
+ securityModes.push(SECURITY_MODE_MAP[mode]);
700
+ }
701
+ });
702
+ if (securityModes.length === 0) {
703
+ securityModes.push(SECURITY_MODE_MAP.None);
704
+ }
705
+
706
+ const hasNoneMode = securityModes.includes(MessageSecurityMode.None);
707
+ const hasSignedMode = securityModes.some(m => m !== MessageSecurityMode.None);
708
+
709
+ if (hasNoneMode && !hasSignedMode) {
710
+ // ONLY None mode is selected, so policy MUST be None
711
+ securityPolicies = [SecurityPolicy.None];
712
+ } else if (!hasNoneMode && hasSignedMode) {
713
+ // ONLY signed modes are selected, so policy cannot be None
714
+ securityPolicies = securityPolicies.filter(p => p !== SecurityPolicy.None);
715
+ if (securityPolicies.length === 0) {
716
+ securityPolicies.push(SecurityPolicy.Basic256Sha256);
717
+ this.node.warn("Security policy adjusted to Basic256Sha256 because signed modes require a policy");
718
+ }
719
+ } else {
720
+ // Both None and signed modes are selected. Policies can be a mix.
721
+ // If the user selected a signed mode but only selected "None" policy, we must add a default signed policy.
722
+ const hasSignedPolicy = securityPolicies.some(p => p !== SecurityPolicy.None);
723
+ if (!hasSignedPolicy) {
724
+ securityPolicies.push(SecurityPolicy.Basic256Sha256);
725
+ this.node.warn("Added default Security Policy Basic256Sha256 because signed modes require a policy");
726
+ }
727
+ // Ensure None policy is present for the None mode
728
+ if (!securityPolicies.includes(SecurityPolicy.None)) {
729
+ securityPolicies.push(SecurityPolicy.None);
669
730
  }
670
- } else if (securityPolicy === SecurityPolicy.None) {
671
- securityPolicy = SecurityPolicy.Basic256Sha256;
672
- this.node.warn("Security policy adjusted to Basic256Sha256 because signed modes require a policy");
673
731
  }
674
732
 
675
733
  return {
676
- securityPolicy,
677
- securityMode
734
+ securityPolicies,
735
+ securityModes
678
736
  };
679
737
  }
680
738
 
681
739
  coerceValue(value, type) {
682
- if (Array.isArray(value)) {
683
- return value.map((item) => this.coerceScalarValue(item, type));
684
- }
740
+ const recursiveMap = (val, fn) => {
741
+ if (Array.isArray(val)) {
742
+ if ((type === "Int64" || type === "UInt64") && val.length === 2 && typeof val[0] === "number" && typeof val[1] === "number") {
743
+ return fn(val);
744
+ }
745
+ return val.map(item => recursiveMap(item, fn));
746
+ }
747
+ return fn(val);
748
+ };
685
749
 
686
- if (typeof value === "string") {
687
- const trimmed = value.trim();
688
- if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
689
- try {
690
- const parsed = JSON.parse(trimmed);
691
- if (Array.isArray(parsed)) {
692
- return parsed.map((item) => this.coerceScalarValue(item, type));
750
+ const extractArray = (val) => {
751
+ if (Array.isArray(val)) {
752
+ return val;
753
+ }
754
+ if (typeof val === "string") {
755
+ const trimmed = val.trim();
756
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
757
+ try {
758
+ const parsed = JSON.parse(trimmed);
759
+ if (Array.isArray(parsed)) {
760
+ return parsed;
761
+ }
762
+ } catch (error) {
763
+ throw new Error("Invalid array value for type " + type + ": " + error.message);
693
764
  }
694
- } catch (error) {
695
- throw new Error("Invalid array value for type " + type + ": " + error.message);
696
765
  }
697
766
  }
767
+ return null;
768
+ };
769
+
770
+ const items = extractArray(value);
771
+ if (items) {
772
+ return recursiveMap(items, (item) => this.coerceScalarValue(item, type));
698
773
  }
699
774
 
700
775
  return this.coerceScalarValue(value, type);
@@ -9,7 +9,7 @@ const { resolveRegisteredServer } = require("./server-node-utils");
9
9
 
10
10
  function eventsServer(node, rootNode, nodeId) {
11
11
 
12
-
12
+ node.intervalMs = rootNode.intervalMs;
13
13
 
14
14
  // 🔥 ALTERADO: usar Map para garantir unicidade por nodeID
15
15
  node.queue = {
@@ -110,10 +110,36 @@ function flushQueue(node, nodeId) {
110
110
 
111
111
 
112
112
  function upsertEvent(map, event) {
113
- const key = String(event.nodeID || "").trim();
114
- if (!key) return;
115
-
116
- map.set(key, event); // sobrescreve automaticamente
113
+ const nodeKey = String(event.nodeID || "").trim();
114
+ if (!nodeKey) return;
115
+
116
+ // Key by nodeID only so the same variable is merged into one entry
117
+ const key = nodeKey;
118
+ const existing = map.get(key);
119
+
120
+ if (!existing) {
121
+ // First access for this variable in this interval — store a copy
122
+ map.set(key, Object.assign({}, event, { users: Array.isArray(event.users) ? [...event.users] : [] }));
123
+ } else {
124
+ // Variable already seen — update value and merge any new users
125
+ existing.value = event.value;
126
+ if (event.message !== undefined) existing.message = event.message;
127
+ if (event.severity !== undefined) existing.severity = event.severity;
128
+ if (event.retain !== undefined) existing.retain = event.retain;
129
+ if (event.activeState !== undefined) existing.activeState = event.activeState;
130
+ if (event.sourceName !== undefined) existing.sourceName = event.sourceName;
131
+ if (event.conditionName !== undefined) existing.conditionName = event.conditionName;
132
+ if (event.ConfirmedState !== undefined) existing.ConfirmedState = event.ConfirmedState;
133
+ if (event.ackedState !== undefined) existing.ackedState = event.ackedState;
134
+ if (event.alarmNode !== undefined) existing.alarmNode = event.alarmNode;
135
+ const existingNames = new Set((existing.users || []).map(u => u.name));
136
+ for (const user of (event.users || [])) {
137
+ if (!existingNames.has(user.name)) {
138
+ existing.users.push(user);
139
+ existingNames.add(user.name);
140
+ }
141
+ }
142
+ }
117
143
  }
118
144
 
119
145
  function matchesServer(serverRef, event) {