diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IAlarmSource.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IAlarmSource.cs index fa66e5a4..09521fc6 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IAlarmSource.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IAlarmSource.cs @@ -71,6 +71,12 @@ public sealed record AlarmAcknowledgeRequest( /// Diagnostics). Maps to OPC UA ConditionClassName downstream when /// a class mapping is configured. Null when the upstream path doesn't carry it. /// +/// +/// The alarm transition kind (raise / acknowledge / clear / retrigger). Lets a +/// consumer derive OPC UA Part 9 active/ack state without inferring it from +/// other fields. when the upstream +/// path doesn't surface a distinct kind. +/// public sealed record AlarmEventArgs( IAlarmSubscriptionHandle SubscriptionHandle, string SourceNodeId, @@ -81,7 +87,15 @@ public sealed record AlarmEventArgs( DateTime SourceTimestampUtc, string? OperatorComment = null, DateTime? OriginalRaiseTimestampUtc = null, - string? AlarmCategory = null); + string? AlarmCategory = null, + AlarmTransitionKind Kind = AlarmTransitionKind.Unspecified); /// Mirrors the NodePermissions alarm-severity enum in docs/v2/acl-design.md. public enum AlarmSeverity { Low, Medium, High, Critical } + +/// +/// Kind of alarm state change carried by , letting a +/// consumer derive OPC UA Part 9 active/ack state. Mirrors the driver-side +/// transition-kind enums (e.g. Galaxy's GalaxyAlarmTransitionKind). +/// +public enum AlarmTransitionKind { Unspecified = 0, Raise, Acknowledge, Clear, Retrigger } diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriver.cs index 1bc2cedd..2cc822b6 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriver.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriver.cs @@ -1153,7 +1153,17 @@ public sealed class GalaxyDriver SourceTimestampUtc: transition.TransitionTimestampUtc, OperatorComment: string.IsNullOrEmpty(transition.OperatorComment) ? null : transition.OperatorComment, OriginalRaiseTimestampUtc: transition.OriginalRaiseTimestampUtc, - AlarmCategory: string.IsNullOrEmpty(transition.Category) ? null : transition.Category); + AlarmCategory: string.IsNullOrEmpty(transition.Category) ? null : transition.Category, + // Fully-qualify the Core.Abstractions enum: this file also imports + // MxGateway.Contracts.Proto, which defines a same-named AlarmTransitionKind. + Kind: transition.TransitionKind switch + { + GalaxyAlarmTransitionKind.Raise => Core.Abstractions.AlarmTransitionKind.Raise, + GalaxyAlarmTransitionKind.Acknowledge => Core.Abstractions.AlarmTransitionKind.Acknowledge, + GalaxyAlarmTransitionKind.Clear => Core.Abstractions.AlarmTransitionKind.Clear, + GalaxyAlarmTransitionKind.Retrigger => Core.Abstractions.AlarmTransitionKind.Retrigger, + _ => Core.Abstractions.AlarmTransitionKind.Unspecified, + }); try { OnAlarmEvent?.Invoke(this, args); diff --git a/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests/AlarmEventArgsTests.cs b/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests/AlarmEventArgsTests.cs new file mode 100644 index 00000000..3815c210 --- /dev/null +++ b/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests/AlarmEventArgsTests.cs @@ -0,0 +1,23 @@ +using Shouldly; +using ZB.MOM.WW.OtOpcUa.Core.Abstractions; +using Xunit; + +namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests; + +public class AlarmEventArgsTests +{ + private static AlarmEventArgs Make(AlarmTransitionKind? kind = null) => + kind is null + ? new AlarmEventArgs(new FakeHandle(), "Tank1.Hi", "c1", "LimitAlarm.Hi", "msg", AlarmSeverity.High, DateTime.UnixEpoch) + : new AlarmEventArgs(new FakeHandle(), "Tank1.Hi", "c1", "LimitAlarm.Hi", "msg", AlarmSeverity.High, DateTime.UnixEpoch, Kind: kind.Value); + + [Fact] + public void Kind_defaults_to_Unspecified_so_existing_callers_compile() + => Make().Kind.ShouldBe(AlarmTransitionKind.Unspecified); + + [Fact] + public void Kind_round_trips_when_supplied() + => Make(AlarmTransitionKind.Raise).Kind.ShouldBe(AlarmTransitionKind.Raise); + + private sealed class FakeHandle : IAlarmSubscriptionHandle { public string DiagnosticId => "t"; } +} diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/GalaxyDriverAlarmSourceTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/GalaxyDriverAlarmSourceTests.cs index af5a0560..ebc90a7a 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/GalaxyDriverAlarmSourceTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/GalaxyDriverAlarmSourceTests.cs @@ -42,6 +42,39 @@ public sealed class GalaxyDriverAlarmSourceTests observed[0].SubscriptionHandle.ShouldBe(handle); } + /// + /// Verifies that the driver maps each Galaxy transition kind onto the matching + /// on the surfaced , + /// so a Part 9 consumer can derive active/ack state. The Galaxy kind is passed by + /// name because GalaxyAlarmTransitionKind is internal to the driver and so + /// cannot appear in a public test method signature. + /// + /// The GalaxyAlarmTransitionKind member name fed into the alarm feed. + /// The expected on the surfaced event. + [Theory] + [InlineData(nameof(GalaxyAlarmTransitionKind.Raise), AlarmTransitionKind.Raise)] + [InlineData(nameof(GalaxyAlarmTransitionKind.Acknowledge), AlarmTransitionKind.Acknowledge)] + [InlineData(nameof(GalaxyAlarmTransitionKind.Clear), AlarmTransitionKind.Clear)] + [InlineData(nameof(GalaxyAlarmTransitionKind.Retrigger), AlarmTransitionKind.Retrigger)] + [InlineData(nameof(GalaxyAlarmTransitionKind.Unspecified), AlarmTransitionKind.Unspecified)] + public async Task Transition_kind_maps_onto_AlarmEventArgs_Kind( + string galaxyKindName, AlarmTransitionKind expectedKind) + { + var galaxyKind = Enum.Parse(galaxyKindName); + var feed = new FakeAlarmFeed(); + var ack = new RecordingAcknowledger(); + using var driver = NewDriver(feed, ack); + + var handle = await driver.SubscribeAlarmsAsync(["Tank01"], CancellationToken.None); + var observed = new List(); + driver.OnAlarmEvent += (_, args) => observed.Add(args); + + feed.Emit(NewTransition("Tank01.Level.HiHi", "Tank01", galaxyKind, AlarmSeverity.High)); + + observed.ShouldHaveSingleItem(); + observed[0].Kind.ShouldBe(expectedKind); + } + /// Verifies that OnAlarmEvent does not fire before any alarm subscription. [Fact] public void OnAlarmEvent_does_not_fire_before_any_alarm_subscription() diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/Runtime/GatewayGalaxyAlarmFeedTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/Runtime/GatewayGalaxyAlarmFeedTests.cs index 077a9efd..e0482d60 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/Runtime/GatewayGalaxyAlarmFeedTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/Runtime/GatewayGalaxyAlarmFeedTests.cs @@ -6,6 +6,11 @@ using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime; +// Core.Abstractions now also defines an AlarmTransitionKind (the AlarmEventArgs surface +// kind). This file drives the gateway proto, so pin the bare token to the proto enum to +// preserve the existing references unchanged. +using AlarmTransitionKind = ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmTransitionKind; + namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime; ///