fix(opcuaclient): native alarms translate RawPath <-> upstream source/condition ids (#619)
The host subscribes alarms with RawPaths and routes transitions on (driver, RawPath), but the driver filtered upstream SourceNode ids against the RawPaths and reported the upstream condition NodeId as ConditionId, so no opc-plc (or any upstream) alarm could ever reach a condition node. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using Opc.Ua;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
@@ -71,6 +72,66 @@ public sealed class OpcUaClientAlarmTests
|
||||
await drv.AcknowledgeAsync([], TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
// ---- #619: v3 RawPath <-> upstream-source translation on the native-alarm path ----
|
||||
|
||||
private static VariantCollection EventFields(string sourceNode, string conditionNode) =>
|
||||
[
|
||||
new Variant(new byte[] { 1 }), // EventId
|
||||
new Variant(new NodeId("TripAlarmType", 3)), // EventType
|
||||
new Variant(NodeId.Parse(sourceNode)), // SourceNode
|
||||
new Variant(new LocalizedText("tripped")), // Message
|
||||
new Variant((ushort)700), // Severity
|
||||
new Variant(DateTime.UtcNow), // Time
|
||||
new Variant(NodeId.Parse(conditionNode)), // ConditionId
|
||||
];
|
||||
|
||||
/// <summary>The host hands SubscribeAlarmsAsync RawPaths; the index must be keyed by the UPSTREAM source id.</summary>
|
||||
[Fact]
|
||||
public void BuildAlarmSourceIndex_keys_the_upstream_source_id_to_the_RawPath()
|
||||
{
|
||||
var index = OpcUaClientDriver.BuildAlarmSourceIndex(
|
||||
["Plant/opc/plc/Trip1"],
|
||||
raw => raw == "Plant/opc/plc/Trip1" ? "ns=3;s=Alarms.Trip1" : null);
|
||||
|
||||
index["ns=3;s=Alarms.Trip1"].ShouldBe("Plant/opc/plc/Trip1");
|
||||
}
|
||||
|
||||
/// <summary>A ref the authored table cannot resolve is kept verbatim (direct upstream-id use stays valid).</summary>
|
||||
[Fact]
|
||||
public void BuildAlarmSourceIndex_keeps_an_unresolvable_ref_as_its_own_key()
|
||||
{
|
||||
var index = OpcUaClientDriver.BuildAlarmSourceIndex(["ns=3;s=Direct"], _ => null);
|
||||
index["ns=3;s=Direct"].ShouldBe("ns=3;s=Direct");
|
||||
}
|
||||
|
||||
/// <summary>The emitted ConditionId must be the RawPath - DriverHostActor routes on (driver, RawPath).</summary>
|
||||
[Fact]
|
||||
public void ProjectEvent_reports_the_RawPath_as_ConditionId_and_remembers_the_condition_node()
|
||||
{
|
||||
var index = new Dictionary<string, string> { ["ns=3;s=Alarms.Trip1"] = "Plant/opc/plc/Trip1" };
|
||||
string? rememberedRaw = null, rememberedCond = null;
|
||||
|
||||
var e = OpcUaClientDriver.ProjectEvent(
|
||||
EventFields("ns=3;s=Alarms.Trip1", "ns=3;s=Alarms.Trip1.Cond"), index, new FakeAlarmHandle(),
|
||||
(raw, cond) => { rememberedRaw = raw; rememberedCond = cond; });
|
||||
|
||||
e.ShouldNotBeNull();
|
||||
e.ConditionId.ShouldBe("Plant/opc/plc/Trip1");
|
||||
e.SourceNodeId.ShouldBe("Plant/opc/plc/Trip1");
|
||||
rememberedRaw.ShouldBe("Plant/opc/plc/Trip1");
|
||||
rememberedCond.ShouldBe("ns=3;s=Alarms.Trip1.Cond");
|
||||
}
|
||||
|
||||
/// <summary>An event from a source nobody authored is filtered out.</summary>
|
||||
[Fact]
|
||||
public void ProjectEvent_drops_an_unauthored_source()
|
||||
{
|
||||
var index = new Dictionary<string, string> { ["ns=3;s=Alarms.Trip1"] = "Plant/opc/plc/Trip1" };
|
||||
OpcUaClientDriver.ProjectEvent(
|
||||
EventFields("ns=3;s=Other", "ns=3;s=Other.Cond"), index, new FakeAlarmHandle(), (_, _) => { })
|
||||
.ShouldBeNull();
|
||||
}
|
||||
|
||||
private sealed class FakeAlarmHandle : IAlarmSubscriptionHandle
|
||||
{
|
||||
/// <summary>Gets the diagnostic identifier for this alarm handle.</summary>
|
||||
|
||||
Reference in New Issue
Block a user