From 1fbb814daaf8e60bf61e18252274ab8191078adb Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 29 May 2026 16:13:02 -0400 Subject: [PATCH] =?UTF-8?q?feat(dcl):=20OPC=20UA=20A&C=20field=20mapper=20?= =?UTF-8?q?(Task=2011=20part=201=20=E2=80=94=20pure,=20unit-tested)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Adapters/OpcUaAlarmMapper.cs | 52 +++++++++++++++++ .../OpcUaAlarmMapperTests.cs | 58 +++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/OpcUaAlarmMapper.cs create mode 100644 tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/OpcUaAlarmMapperTests.cs 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)); + } +}