feat(dcl): populate obtainable NativeAlarmTransition fields from OPC UA and MxGateway (#27, M2.13)

OPC UA (RealOpcUaClient):
- Append 5 new SelectClauses at indices 13–17 (never renumber 0–12):
  - 13: AlarmConditionType/ActiveState/TransitionTime → OriginalRaiseTime
  - 14–17: LimitAlarmType HighHighLimit/HighLimit/LowLimit/LowLowLimit → LimitValue
- New OpcUaAlarmMapper.PickLimitValue helper: first non-null in HiHi→Hi→Lo→LoLo
  priority order, InvariantCulture-formatted; empty string for non-limit alarm types.
- HandleAlarmEvent reads new indices with fields.Count > N guards; hard minimum (6)
  unchanged so base ConditionType events still process without the limit fields.
- Document unavailable-by-protocol fields (Category, Description, OperatorUser,
  CurrentValue) inline in BuildAlarmEventFilter and HandleAlarmEvent.

MxGateway (MxGatewayAlarmMapper):
- MapTransition: CurrentValue and LimitValue now populated via MxValueToString
  (uses MxValueExtensions.ToClrValue + InvariantCulture) from OnAlarmTransitionEvent
  proto fields current_value/limit_value.
- MapSnapshot: same — populated from ActiveAlarmSnapshot.current_value/limit_value.
- MxValueToString helper (internal): null-safe MxValue → string conversion.

Tests (17 new, 40 total pass):
- OpcUaAlarmMapperTests: PickLimitValue priority, InvariantCulture, all-null case.
- MxGatewayAlarmMapperTests: CurrentValue/LimitValue populate from double/string
  MxValue; absent fields yield empty strings.
- RealOpcUaClientAlarmFilterTests: index alignment assertions (count=18, per-index
  TypeDefinitionId+BrowsePath), regression guard on existing indices 0–12.
This commit is contained in:
Joseph Doherty
2026-06-16 06:37:19 -04:00
parent e2b31a9fd2
commit 722b8663c1
8 changed files with 338 additions and 9 deletions
@@ -55,4 +55,54 @@ public class OpcUaAlarmMapperTests
{
Assert.Equal(expected, OpcUaAlarmMapper.MapShelve(name));
}
// ── PickLimitValue (M2.13 / #27) ─────────────────────────────────────────
[Fact]
public void PickLimitValue_AllNull_ReturnsEmpty()
{
// All four limit fields absent (non-limit alarm type) → empty string.
Assert.Equal("", OpcUaAlarmMapper.PickLimitValue(null, null, null, null));
}
[Fact]
public void PickLimitValue_HighHighLimitPresent_ReturnsIt()
{
// HighHighLimit takes top priority; other fields are null (absent).
var result = OpcUaAlarmMapper.PickLimitValue(100.5, null, null, null);
Assert.Equal("100.5", result);
}
[Fact]
public void PickLimitValue_OnlyHighLimit_ReturnsHighLimit()
{
// Only HighLimit present (HighHighLimit absent on this alarm type).
var result = OpcUaAlarmMapper.PickLimitValue(null, 80.0, null, null);
Assert.Equal("80", result);
}
[Fact]
public void PickLimitValue_PriorityOrder_HighHighWinsOverHigh()
{
// When multiple limits are present, HighHighLimit takes precedence.
var result = OpcUaAlarmMapper.PickLimitValue(95.0, 80.0, 20.0, 5.0);
Assert.Equal("95", result);
}
[Fact]
public void PickLimitValue_OnlyLowLow_ReturnsLowLow()
{
// LowLowLimit only — last in priority, but should still be returned.
var result = OpcUaAlarmMapper.PickLimitValue(null, null, null, -10.5);
Assert.Equal("-10.5", result);
}
[Fact]
public void PickLimitValue_UsesInvariantCulture()
{
// Decimal separator must always be '.' regardless of thread culture.
var result = OpcUaAlarmMapper.PickLimitValue(1.5, null, null, null);
Assert.Contains('.', result); // invariant culture: '.' not ','
Assert.Equal("1.5", result);
}
}