dd4f1c2aa3
AlarmConditionState.Severity mixed two conventions: OPC UA rows carried Part 9 Severity (higher = more severe) while MxGateway native alarms and computed alarms carried a raw AVEVA-style priority (1 = most urgent). Every consumer ranks higher as worse, so Worst Severity, the severity filter/sort and the site event-log Error threshold picked the LEAST urgent MxGateway/computed alarms. - AlarmPriorityBands: the OtOpcUa GalaxyAlarmPriority table (lmxopcua#887, b61f3e05), value for value: 1-250 -> 1000, 251-500 -> 750, 501-750 -> 500, 751-999 -> 250; <= 0 clamps to Critical, > 999 to Low. - MxGatewayAlarmMapper bands the gateway severity (= AVEVA priority); AlarmConditionStateFactory.ForComputed bands PriorityLevel. OPC UA unchanged. - AlarmConditionState gains trailing Priority (int?): the raw source number, exactly the pre-#129 Severity. AlarmStateChanged.Priority and the wire AlarmStateUpdate.priority keep that meaning (no proto change); central derives Severity by kind (SeverityFromPriority), so an older site decodes correctly. - ScriptAlarm gains trailing Priority. The native MES 900-999 band and the MES API Severity/MinSeverity/MaxSeverity key off Priority; rig seed scripts moved from a.Severity to a.Priority, so MES output is unchanged. - AlarmActor raise logs Error when the banded severity >= 700 (same threshold as NativeAlarmActor): priority 1-500 Error, 501+ Warning (was raw >= 700). - NativeAlarmActor upgrades pre-#129 native_alarm_state rows on rehydrate. - Debug snapshot decode restores Condition.Priority from the row's priority. - Docs: Commons, DCL, SiteRuntime, Communication, CentralUI, InboundAPI, HighLevelReqs, MES alarm-status plan, CLAUDE.md. Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
65 lines
2.9 KiB
C#
65 lines
2.9 KiB
C#
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Messages;
|
|
|
|
public class AlarmStateChangedEnrichmentTests
|
|
{
|
|
/// <summary>Verifies that a default alarm state change is of the computed kind with auto-acknowledge.</summary>
|
|
[Fact]
|
|
public void Defaults_AreComputedKind_WithAutoAck()
|
|
{
|
|
var m = new AlarmStateChanged("inst", "HiAlarm", AlarmState.Active, 700, DateTimeOffset.UnixEpoch);
|
|
Assert.Equal(AlarmKind.Computed, m.Kind);
|
|
Assert.True(m.Condition.Acknowledged); // computed = auto-acked
|
|
Assert.Equal(500, m.Condition.Severity); // priority 700 banded: Medium → 500 (#129)
|
|
Assert.Equal(700, m.Condition.Priority); // raw priority kept verbatim
|
|
Assert.True(m.Condition.Active); // derived from State
|
|
Assert.Equal("", m.SourceReference);
|
|
}
|
|
|
|
/// <summary>Verifies that the computed factory maps priority and state onto the message.</summary>
|
|
[Fact]
|
|
public void Factory_ForComputed_MapsPriorityAndState()
|
|
{
|
|
var c = AlarmConditionStateFactory.ForComputed(AlarmState.Normal, priority: 250);
|
|
Assert.False(c.Active);
|
|
Assert.True(c.Acknowledged);
|
|
Assert.Equal(1000, c.Severity); // priority 250 is AVEVA Critical (#129)
|
|
Assert.Equal(250, c.Priority);
|
|
}
|
|
|
|
// ── MES alarm-status API §6.4: additive AckTime ──
|
|
|
|
/// <summary>Verifies that the acknowledge time defaults to null when using the positional constructor.</summary>
|
|
[Fact]
|
|
public void AckTime_DefaultsToNull_OnThePositionalConstructor()
|
|
{
|
|
// Additive-only evolution: every existing positional construction stays valid and
|
|
// reports no ack time. A computed alarm is auto-acked but has no operator ack
|
|
// event, so null (not the timestamp) is the honest value.
|
|
var m = new AlarmStateChanged("inst", "HiAlarm", AlarmState.Active, 700, DateTimeOffset.UnixEpoch);
|
|
|
|
Assert.True(m.Condition.Acknowledged); // computed = auto-acked…
|
|
Assert.Null(m.AckTime); // …yet still no ack instant
|
|
}
|
|
|
|
/// <summary>Verifies that the acknowledge time round-trips through its init property.</summary>
|
|
[Fact]
|
|
public void AckTime_RoundTripsThroughTheInitProperty()
|
|
{
|
|
var ackedAt = new DateTimeOffset(2026, 8, 1, 12, 0, 0, TimeSpan.Zero);
|
|
|
|
var m = new AlarmStateChanged("inst", "Tank01.Level.HiHi", AlarmState.Active, 900, DateTimeOffset.UnixEpoch)
|
|
{
|
|
Kind = AlarmKind.NativeOpcUa,
|
|
AckTime = ackedAt
|
|
};
|
|
|
|
Assert.Equal(ackedAt, m.AckTime);
|
|
// `with` (used by the mirror to synthesise a return-to-normal) preserves it.
|
|
Assert.Equal(ackedAt, (m with { State = AlarmState.Normal }).AckTime);
|
|
}
|
|
}
|