diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs index 9e700438..71a11f0c 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs @@ -832,7 +832,10 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 /// (or root when null). Initial value=null, quality=Bad, /// timestamp=epoch — fills these in once driver data flows. /// Idempotent. Materialises equipment-namespace tags so they're browseable before drivers - /// issue SubscribeBulk. + /// issue SubscribeBulk. Note: because of the early _variables.ContainsKey return, a + /// re-apply of an EXISTING node with a changed historize intent (e.g. non-historized → + /// historized) is silently ignored — a historize-intent change only takes effect after a + /// (which the planner triggers on an equipment-tag delta). /// /// The node identifier of the variable. /// The node identifier of the parent folder; null to use the namespace root. diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DeploymentArtifact.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DeploymentArtifact.cs index 115e631a..a99f5c22 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DeploymentArtifact.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DeploymentArtifact.cs @@ -680,7 +680,7 @@ public static class DeploymentArtifact /// false) and the optional historianTagname string override (absent / not a string / /// whitespace-or-empty ⇒ null, meaning the historian tagname defaults to the tag's FullName, /// resolved later). The raw string value is used — not trimmed — matching ExtractTagFullName / - /// ExtractTagAlarm. Never throws. The live-edit side + /// ExtractTagAlarm. Never throws. The live-edit composer side /// (Phase7Composer.ExtractTagHistorize) MUST parse identically (byte-parity). private static (bool IsHistorized, string? HistorianTagname) ExtractTagHistorize(string? tagConfig) { diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/DeferredAddressSpaceSinkTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/DeferredAddressSpaceSinkTests.cs index 76d03ab2..cd6793ab 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/DeferredAddressSpaceSinkTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/DeferredAddressSpaceSinkTests.cs @@ -67,6 +67,22 @@ public sealed class DeferredAddressSpaceSinkTests second.Calls.Single().ShouldBe("WV:b"); } + /// Verifies that forwards the + /// historianTagname argument to the inner sink unchanged. + [Fact] + public void EnsureVariable_forwards_historianTagname_to_inner_sink() + { + var deferred = new DeferredAddressSpaceSink(); + var inner = new RecordingSink(); + deferred.SetSink(inner); + + deferred.EnsureVariable("v-1", null, "MyVar", "Float", writable: false, historianTagname: "MyTag.PV"); + + var call = inner.HistorianCalls.ShouldHaveSingleItem(); + call.NodeId.ShouldBe("v-1"); + call.HistorianTagname.ShouldBe("MyTag.PV"); + } + /// Builds a minimal for the forwarding tests (the /// inner sink only records the node id, so the exact state values don't matter here). private static AlarmConditionSnapshot Snapshot(bool active = false) => @@ -80,6 +96,12 @@ public sealed class DeferredAddressSpaceSinkTests /// Gets the list of recorded calls. public List Calls => CallQueue.ToList(); + /// Gets the queue of (NodeId, HistorianTagname) pairs captured per + /// EnsureVariable call, in call order. + public ConcurrentQueue<(string NodeId, string? HistorianTagname)> HistorianQueue { get; } = new(); + /// Gets the list of (NodeId, HistorianTagname) pairs captured per EnsureVariable call. + public List<(string NodeId, string? HistorianTagname)> HistorianCalls => HistorianQueue.ToList(); + /// public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc) => CallQueue.Enqueue($"WV:{nodeId}"); @@ -94,7 +116,10 @@ public sealed class DeferredAddressSpaceSinkTests => CallQueue.Enqueue($"EF:{folderNodeId}"); /// public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null) - => CallQueue.Enqueue($"EV:{variableNodeId}"); + { + CallQueue.Enqueue($"EV:{variableNodeId}"); + HistorianQueue.Enqueue((variableNodeId, historianTagname)); + } /// public void RebuildAddressSpace() => CallQueue.Enqueue("RB"); }