diff --git a/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/OpcUaAlarmMapper.cs b/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/OpcUaAlarmMapper.cs
new file mode 100644
index 00000000..12bfc238
--- /dev/null
+++ b/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/OpcUaAlarmMapper.cs
@@ -0,0 +1,52 @@
+using ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms;
+using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
+
+namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Adapters;
+
+///
+/// Pure mapping helpers turning OPC UA Alarms & Conditions event fields into the
+/// protocol-neutral / transition shape. Kept
+/// free of any OPC UA SDK types so it is unit-testable without a live server;
+/// the SDK field extraction lives in RealOpcUaClient and is exercised by
+/// the live smoke test (Task 28).
+///
+public static class OpcUaAlarmMapper
+{
+ /// Clamps an OPC UA severity (1–1000, sometimes out of range) to the unified 0–1000 scale.
+ public static int NormalizeSeverity(int severity) => Math.Clamp(severity, 0, 1000);
+
+ /// Builds an from the orthogonal A&C sub-states.
+ public static AlarmConditionState BuildCondition(
+ bool active, bool acked, bool? confirmed, AlarmShelveState shelve, bool suppressed, int severity) =>
+ new(Active: active, Acknowledged: acked, Confirmed: confirmed,
+ Shelve: shelve, Suppressed: suppressed, Severity: NormalizeSeverity(severity));
+
+ ///
+ /// Derives the transition kind from the change in active/acked sub-states.
+ /// Acknowledgement takes precedence over an active/inactive edge when both
+ /// change in the same event; an unchanged event is reported as a StateChange.
+ ///
+ public static AlarmTransitionKind DeriveKind(bool prevAcked, bool nowAcked, bool prevActive, bool nowActive)
+ {
+ if (!prevAcked && nowAcked)
+ return AlarmTransitionKind.Acknowledge;
+ if (!prevActive && nowActive)
+ return AlarmTransitionKind.Raise;
+ if (prevActive && !nowActive)
+ return AlarmTransitionKind.Clear;
+ if (prevActive && nowActive)
+ return AlarmTransitionKind.Retrigger;
+ return AlarmTransitionKind.StateChange;
+ }
+
+ /// Maps the OPC UA ShelvingState current-state node name to the shelve enum.
+ public static AlarmShelveState MapShelve(string? shelvingStateName) => shelvingStateName switch
+ {
+ "OneShotShelved" => AlarmShelveState.OneShotShelved,
+ "TimedShelved" => AlarmShelveState.TimedShelved,
+ // OPC UA does not expose a distinct "permanent" shelve; treat any other
+ // shelved name as one-shot and "Unshelved"/null as unshelved.
+ null or "Unshelved" => AlarmShelveState.Unshelved,
+ _ => AlarmShelveState.OneShotShelved
+ };
+}
diff --git a/tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/OpcUaAlarmMapperTests.cs b/tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/OpcUaAlarmMapperTests.cs
new file mode 100644
index 00000000..df034bce
--- /dev/null
+++ b/tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/OpcUaAlarmMapperTests.cs
@@ -0,0 +1,58 @@
+using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
+using ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Adapters;
+
+namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests;
+
+/// Task-11: pure OPC UA A&C field → AlarmConditionState/transition mapping.
+public class OpcUaAlarmMapperTests
+{
+ [Fact]
+ public void NormalizeSeverity_ClampsTo0_1000()
+ {
+ Assert.Equal(1000, OpcUaAlarmMapper.NormalizeSeverity(5000));
+ Assert.Equal(0, OpcUaAlarmMapper.NormalizeSeverity(-1));
+ Assert.Equal(500, OpcUaAlarmMapper.NormalizeSeverity(500));
+ }
+
+ [Fact]
+ public void BuildCondition_ActiveUnacked()
+ {
+ var c = OpcUaAlarmMapper.BuildCondition(active: true, acked: false, confirmed: null,
+ shelve: AlarmShelveState.Unshelved, suppressed: false, severity: 700);
+ Assert.True(c.Active);
+ Assert.False(c.Acknowledged);
+ Assert.Equal(700, c.Severity);
+ Assert.Equal(AlarmShelveState.Unshelved, c.Shelve);
+ }
+
+ [Fact]
+ public void DeriveKind_AckEdge_YieldsAcknowledge()
+ {
+ Assert.Equal(AlarmTransitionKind.Acknowledge,
+ OpcUaAlarmMapper.DeriveKind(prevAcked: false, nowAcked: true, prevActive: true, nowActive: true));
+ }
+
+ [Fact]
+ public void DeriveKind_ActiveEdge_YieldsRaise()
+ {
+ Assert.Equal(AlarmTransitionKind.Raise,
+ OpcUaAlarmMapper.DeriveKind(prevAcked: true, nowAcked: true, prevActive: false, nowActive: true));
+ }
+
+ [Fact]
+ public void DeriveKind_InactiveEdge_YieldsClear()
+ {
+ Assert.Equal(AlarmTransitionKind.Clear,
+ OpcUaAlarmMapper.DeriveKind(prevAcked: true, nowAcked: true, prevActive: true, nowActive: false));
+ }
+
+ [Theory]
+ [InlineData("OneShotShelved", AlarmShelveState.OneShotShelved)]
+ [InlineData("TimedShelved", AlarmShelveState.TimedShelved)]
+ [InlineData("Unshelved", AlarmShelveState.Unshelved)]
+ [InlineData(null, AlarmShelveState.Unshelved)]
+ public void MapShelve_MapsCurrentStateName(string? name, AlarmShelveState expected)
+ {
+ Assert.Equal(expected, OpcUaAlarmMapper.MapShelve(name));
+ }
+}