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;
///