diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/AlarmDispatcherTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/AlarmDispatcherTests.cs
index 18af656..0aca018 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/AlarmDispatcherTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/AlarmDispatcherTests.cs
@@ -64,6 +64,41 @@ public sealed class AlarmDispatcherTests
Assert.Equal(ts, body.TransitionTimestamp.ToDateTime());
}
+ ///
+ /// Verifies that a transition enqueued via the subtag fallback
+ /// (degraded: true) is marked
+ /// with , while the default path
+ /// stays on the alarmmgr parity contract.
+ ///
+ [Fact]
+ public void EnqueueTransition_WhenDegraded_MarksDegradedAndSubtagProvider()
+ {
+ MxAccessEventQueue queue = new MxAccessEventQueue();
+ MxAccessAlarmEventSink sink = new MxAccessAlarmEventSink(queue, new MxAccessEventMapper());
+ sink.Attach(new object(), SessionId);
+
+ DateTime ts = new DateTime(2026, 5, 1, 17, 26, 14, 709, DateTimeKind.Utc);
+ sink.EnqueueTransition(
+ alarmFullReference: "Galaxy!TestArea.TestMachine_001.TestAlarm001",
+ sourceObjectReference: "TestMachine_001.TestAlarm001",
+ alarmTypeName: "DSC",
+ transitionKind: AlarmTransitionKind.Raise,
+ severity: 500,
+ originalRaiseTimestampUtc: null,
+ transitionTimestampUtc: ts,
+ operatorUser: string.Empty,
+ operatorComment: string.Empty,
+ category: "TestArea",
+ description: string.Empty,
+ degraded: true);
+
+ Assert.Equal(1, queue.Count);
+ Assert.True(queue.TryDequeue(out WorkerEvent? workerEvent));
+ OnAlarmTransitionEvent body = workerEvent!.Event.OnAlarmTransition;
+ Assert.True(body.Degraded);
+ Assert.Equal(AlarmProviderMode.Subtag, body.SourceProvider);
+ }
+
/// Verifies that unchanged alarm states do not emit transitions.
[Fact]
public void OnTransition_WithConsecutiveUnchangedState_DoesNotEmitTransition()
diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/SyntheticAlarmGuidTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/SyntheticAlarmGuidTests.cs
new file mode 100644
index 0000000..6877bf3
--- /dev/null
+++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/SyntheticAlarmGuidTests.cs
@@ -0,0 +1,27 @@
+using System;
+using ZB.MOM.WW.MxGateway.Worker.MxAccess;
+
+namespace ZB.MOM.WW.MxGateway.Worker.Tests.MxAccess;
+
+///
+/// Unit tests for : the subtag-fallback
+/// path derives a deterministic GUID from the alarm reference, so identical
+/// references must collide and distinct references must not.
+///
+public sealed class SyntheticAlarmGuidTests
+{
+ /// Verifies the same reference yields the same GUID.
+ [Fact]
+ public void SameReference_SameGuid() =>
+ Assert.Equal(SyntheticAlarmGuid.ForReference("A.B.C"), SyntheticAlarmGuid.ForReference("A.B.C"));
+
+ /// Verifies distinct references yield distinct GUIDs.
+ [Fact]
+ public void DifferentReference_DifferentGuid() =>
+ Assert.NotEqual(SyntheticAlarmGuid.ForReference("A.B.C"), SyntheticAlarmGuid.ForReference("A.B.D"));
+
+ /// Verifies a reference produces a non-empty GUID.
+ [Fact]
+ public void Reference_ProducesNonEmptyGuid() =>
+ Assert.NotEqual(Guid.Empty, SyntheticAlarmGuid.ForReference("A.B.C"));
+}
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessAlarmEventSink.cs b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessAlarmEventSink.cs
index 8948396..f732952 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessAlarmEventSink.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessAlarmEventSink.cs
@@ -86,6 +86,12 @@ public sealed class MxAccessAlarmEventSink : IMxAccessEventSink
/// The operator's comment, if any.
/// The alarm category.
/// The alarm description.
+ ///
+ /// when the transition was synthesized by the
+ /// subtag-provider fallback rather than the native alarmmgr path.
+ /// Defaults to so existing alarmmgr callers
+ /// compile unchanged and stay on the parity (alarmmgr) path.
+ ///
internal void EnqueueTransition(
string alarmFullReference,
string sourceObjectReference,
@@ -97,10 +103,16 @@ public sealed class MxAccessAlarmEventSink : IMxAccessEventSink
string operatorUser,
string operatorComment,
string category,
- string description)
+ string description,
+ bool degraded = false)
{
try
{
+ // Degraded transitions come from the subtag fallback; the native
+ // alarmmgr (wnwrap) path stays degraded=false / ALARMMGR for parity.
+ AlarmProviderMode sourceProvider = degraded
+ ? AlarmProviderMode.Subtag
+ : AlarmProviderMode.Alarmmgr;
MxEvent mxEvent = eventMapper.CreateOnAlarmTransition(
sessionId,
alarmFullReference,
@@ -114,7 +126,9 @@ public sealed class MxAccessAlarmEventSink : IMxAccessEventSink
operatorComment,
category,
description,
- statuses: null);
+ statuses: null,
+ degraded: degraded,
+ sourceProvider: sourceProvider);
eventQueue.Enqueue(mxEvent);
}
catch (Exception exception)
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessEventMapper.cs b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessEventMapper.cs
index 3f3f40b..08f844f 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessEventMapper.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessEventMapper.cs
@@ -124,6 +124,15 @@ public sealed class MxAccessEventMapper
/// Alarm taxonomy bucket from the Galaxy template.
/// Human-readable alarm description.
/// Array of MxStatusProxy values from MXAccess.
+ ///
+ /// when this transition was synthesized by the
+ /// subtag-provider fallback rather than the native alarmmgr path.
+ /// Defaults to to preserve alarmmgr parity.
+ ///
+ ///
+ /// The alarm provider that sourced this transition. Defaults to
+ /// for the native path.
+ ///
public MxEvent CreateOnAlarmTransition(
string sessionId,
string alarmFullReference,
@@ -137,7 +146,9 @@ public sealed class MxAccessEventMapper
string operatorComment,
string category,
string description,
- Array? statuses)
+ Array? statuses,
+ bool degraded = false,
+ AlarmProviderMode sourceProvider = AlarmProviderMode.Alarmmgr)
{
MxEvent mxEvent = CreateBaseEvent(
MxEventFamily.OnAlarmTransition,
@@ -159,6 +170,8 @@ public sealed class MxAccessEventMapper
OperatorComment = operatorComment ?? string.Empty,
Category = category ?? string.Empty,
Description = description ?? string.Empty,
+ Degraded = degraded,
+ SourceProvider = sourceProvider,
};
if (originalRaiseTimestampUtc is { } orts)
{
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAlarmSnapshot.cs b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAlarmSnapshot.cs
index 529f089..9bcee70 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAlarmSnapshot.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAlarmSnapshot.cs
@@ -37,4 +37,12 @@ public sealed class MxAlarmSnapshotRecord
public string OperatorName { get; set; } = string.Empty;
/// Gets or sets the alarm comment.
public string AlarmComment { get; set; } = string.Empty;
+ ///
+ /// Gets or sets a value indicating whether this record was synthesized
+ /// by the subtag-provider fallback rather than emitted by the native
+ /// alarmmgr (wnwrap) path. Default preserves
+ /// parity for the alarmmgr path; the subtag fallback sets it to
+ /// .
+ ///
+ public bool Degraded { get; set; }
}
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/SyntheticAlarmGuid.cs b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/SyntheticAlarmGuid.cs
new file mode 100644
index 0000000..729428d
--- /dev/null
+++ b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/SyntheticAlarmGuid.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Security.Cryptography;
+using System.Text;
+
+namespace ZB.MOM.WW.MxGateway.Worker.MxAccess;
+
+///
+/// Derives a deterministic synthetic from an alarm
+/// reference for the subtag-provider fallback path, which has no native
+/// MxAccess alarm GUID. Hashing the reference yields a stable identity so
+/// repeated transitions for the same alarm reference correlate downstream
+/// (acknowledge, snapshot, OPC UA mapping) without an alarmmgr-supplied GUID.
+///
+public static class SyntheticAlarmGuid
+{
+ ///
+ /// Produces a stable for the given alarm reference.
+ /// The same reference always maps to the same GUID; distinct references
+ /// map to distinct GUIDs with overwhelming probability.
+ ///
+ ///
+ /// The fully-qualified alarm reference (for example
+ /// "Galaxy!Area.Tag.HiHi"). Treated as UTF-8 bytes.
+ ///
+ /// A deterministic, non-empty GUID derived from the reference.
+ ///
+ /// Thrown when is .
+ ///
+ public static Guid ForReference(string reference)
+ {
+ if (reference is null) throw new ArgumentNullException(nameof(reference));
+
+ byte[] bytes = Encoding.UTF8.GetBytes(reference);
+
+ // MD5 is used purely for a stable, non-cryptographic identity mapping
+ // (reference -> 16-byte GUID), never for security. Its 128-bit output
+ // fits a GUID exactly, which is why it is preferred here.
+ using MD5 md5 = MD5.Create();
+ byte[] hash = md5.ComputeHash(bytes);
+ return new Guid(hash);
+ }
+}