diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs index da5abc6a..bbf77430 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs @@ -111,7 +111,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers private readonly Dictionary _driverRefByNodeId = new(StringComparer.Ordinal); - /// (DriverInstanceId, FullName = alarm SourceNodeId) → folder-scoped condition NodeId(s). + /// (DriverInstanceId, FullName = alarm ConditionId / AlarmFullReference) → folder-scoped condition NodeId(s). /// Built from EquipmentTags whose plan carries Alarm, alongside the value maps; resolves a native /// alarm transition to the materialised Part 9 condition node(s). Alarm tags are conditions, not /// value variables, so they are kept OUT of the value maps + value-subscription set. @@ -497,10 +497,11 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers /// /// Routes a native alarm transition (published by a driver child as /// ) to its materialised Part 9 condition - /// node(s). The alarm path analogue of : the driver fires keyed by the - /// alarm source's SourceNodeId (the equipment tag's wire-ref FullName), which the - /// map — built each apply from the alarm-bearing EquipmentTags — - /// resolves to the folder-scoped condition NodeId(s) the materialiser placed the condition(s) at. + /// node(s). The alarm path analogue of : the transition's ConditionId + /// (the dotted alarm full-reference, which equals the authored equipment-tag FullName — NOT the bare + /// SourceNodeId owning object) is resolved by the map — + /// built each apply from the alarm-bearing EquipmentTags — to the folder-scoped condition NodeId(s) + /// the materialiser placed the condition(s) at. /// For each node the projects the transition delta into a full /// AlarmConditionSnapshot, then this Tells /// — the SAME message scripted alarms use, so it routes through WriteAlarmCondition. An @@ -518,10 +519,13 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers private void ForwardNativeAlarm(DriverInstanceActor.AttributeAlarmPublished msg) { if (_opcUaPublishActor is null) return; - if (!_alarmNodeIdByDriverRef.TryGetValue((msg.DriverInstanceId, msg.Args.SourceNodeId), out var nodeIds)) + // Resolve on ConditionId, NOT SourceNodeId: for Galaxy the dotted alarm full-reference — which + // equals the authored equipment-tag FullName the map is keyed by — is carried in ConditionId + // (AlarmFullReference), while SourceNodeId is the bare owning object (SourceObjectReference). + if (!_alarmNodeIdByDriverRef.TryGetValue((msg.DriverInstanceId, msg.Args.ConditionId), out var nodeIds)) { _log.Debug("DriverHost {Node}: no alarm condition for ({Driver},{Ref}) — transition dropped", - _localNode, msg.DriverInstanceId, msg.Args.SourceNodeId); + _localNode, msg.DriverInstanceId, msg.Args.ConditionId); return; } foreach (var nodeId in nodeIds) @@ -862,7 +866,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers // reflected. Each NodeId maps to exactly one driver ref (a variable is backed by a single driver // attribute), so last-writer-wins on the rare duplicate is harmless. _driverRefByNodeId.Clear(); - // Alarm condition routing map: (DriverInstanceId, FullName = alarm SourceNodeId) → folder-scoped + // Alarm condition routing map: (DriverInstanceId, FullName = alarm ConditionId/AlarmFullReference) → folder-scoped // condition NodeId(s). Built from the SAME EquipmentTags pass (alarm-bearing tags only) so // ForwardNativeAlarm can land a native transition on the right condition node. Clear-and-rebuild // every apply; the projector is Clear()'d too so stale per-condition state never leaks across diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverHostActorNativeAlarmTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverHostActorNativeAlarmTests.cs index 425426b2..32553578 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverHostActorNativeAlarmTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverHostActorNativeAlarmTests.cs @@ -49,9 +49,11 @@ public sealed class DriverHostActorNativeAlarmTests : RuntimeActorTestBase private static readonly RevisionHash RevA = RevisionHash.Parse(new string('a', 64)); private static readonly DateTime Ts = new(2026, 6, 14, 10, 0, 0, DateTimeKind.Utc); - /// A native alarm RAISE published by SourceNodeId (the alarm tag's FullName) lands on the - /// condition's folder-scoped NodeId (here eq-1/temp_hi) as an - /// with State.Active == true. + /// A native alarm RAISE whose ConditionId equals the alarm tag's FullName lands on + /// the condition's folder-scoped NodeId (here eq-1/temp_hi) as an + /// with State.Active == true. The event carries a + /// production-shaped SourceNodeId (the bare owning object, distinct from ConditionId) so the + /// lookup is proven to key on ConditionId, not SourceNodeId. [Fact] public void Native_alarm_raise_routes_to_folder_scoped_condition_NodeId_active() { @@ -64,8 +66,8 @@ public sealed class DriverHostActorNativeAlarmTests : RuntimeActorTestBase actor.Tell(new DriverInstanceActor.AttributeAlarmPublished("drv-1", new AlarmEventArgs( new StubAlarmHandle(), - SourceNodeId: "Temp.HiHi", - ConditionId: "cond-1", + SourceNodeId: "Temp", // bare owning object (SourceObjectReference) — NOT the lookup key + ConditionId: "Temp.HiHi", // dotted alarm full-reference = the authored FullName (the lookup key) AlarmType: "OffNormalAlarm", Message: "temperature high", Severity: AlarmSeverity.High, @@ -79,8 +81,8 @@ public sealed class DriverHostActorNativeAlarmTests : RuntimeActorTestBase update.TimestampUtc.ShouldBe(Ts); } - /// An for a SourceNodeId not in the - /// alarm map produces NO (unknown-ref drop). + /// An whose ConditionId is not in + /// the alarm map produces NO (unknown-ref drop). [Fact] public void Unknown_alarm_ref_produces_no_AlarmStateUpdate() { @@ -92,8 +94,8 @@ public sealed class DriverHostActorNativeAlarmTests : RuntimeActorTestBase actor.Tell(new DriverInstanceActor.AttributeAlarmPublished("drv-1", new AlarmEventArgs( new StubAlarmHandle(), - SourceNodeId: "NoSuch.Alarm", - ConditionId: "cond-x", + SourceNodeId: "Temp", // owning object exists, but the condition ref below is unmapped + ConditionId: "NoSuch.HiHi", // dotted ref not in the alarm map ⇒ drop AlarmType: "OffNormalAlarm", Message: "nope", Severity: AlarmSeverity.Low, @@ -122,8 +124,8 @@ public sealed class DriverHostActorNativeAlarmTests : RuntimeActorTestBase actor.Tell(new DriverInstanceActor.AttributeAlarmPublished("drv-1", new AlarmEventArgs( new StubAlarmHandle(), - SourceNodeId: "Temp.HiHi", - ConditionId: "cond-1", + SourceNodeId: "Temp", // bare owning object (SourceObjectReference) — NOT the lookup key + ConditionId: "Temp.HiHi", // dotted alarm full-reference = the authored FullName (the lookup key) AlarmType: "OffNormalAlarm", Message: "temperature high", Severity: AlarmSeverity.High, @@ -169,8 +171,8 @@ public sealed class DriverHostActorNativeAlarmTests : RuntimeActorTestBase actor.Tell(new DriverInstanceActor.AttributeAlarmPublished("drv-1", new AlarmEventArgs( new StubAlarmHandle(), - SourceNodeId: "Temp.HiHi", - ConditionId: "cond-1", + SourceNodeId: "Temp", // bare owning object (SourceObjectReference) — NOT the lookup key + ConditionId: "Temp.HiHi", // dotted alarm full-reference = the authored FullName (the lookup key) AlarmType: "OffNormalAlarm", Message: "temperature high", Severity: AlarmSeverity.High,