feat(dcl): OPC UA A&C field mapper (Task 11 part 1 — pure, unit-tested)
This commit is contained in:
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pure mapping helpers turning OPC UA Alarms & Conditions event fields into the
|
||||||
|
/// protocol-neutral <see cref="AlarmConditionState"/> / 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 <c>RealOpcUaClient</c> and is exercised by
|
||||||
|
/// the live smoke test (Task 28).
|
||||||
|
/// </summary>
|
||||||
|
public static class OpcUaAlarmMapper
|
||||||
|
{
|
||||||
|
/// <summary>Clamps an OPC UA severity (1–1000, sometimes out of range) to the unified 0–1000 scale.</summary>
|
||||||
|
public static int NormalizeSeverity(int severity) => Math.Clamp(severity, 0, 1000);
|
||||||
|
|
||||||
|
/// <summary>Builds an <see cref="AlarmConditionState"/> from the orthogonal A&C sub-states.</summary>
|
||||||
|
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));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Maps the OPC UA ShelvingState current-state node name to the shelve enum.</summary>
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>Task-11: pure OPC UA A&C field → AlarmConditionState/transition mapping.</summary>
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user