fix(alarms): Severity runs one direction, higher = more urgent; raw priority kept (#129)
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
This commit is contained in:
@@ -96,7 +96,8 @@ public class SiteStreamGrpcClientTests
|
||||
Assert.False(alarm.Condition.Acknowledged);
|
||||
Assert.Equal(AlarmShelveState.TimedShelved, alarm.Condition.Shelve);
|
||||
Assert.True(alarm.Condition.Suppressed);
|
||||
Assert.Equal(850, alarm.Condition.Severity);
|
||||
Assert.Equal(850, alarm.Condition.Severity); // OPC UA: wire value is Part 9 Severity already
|
||||
Assert.Equal(850, alarm.Condition.Priority);
|
||||
Assert.Equal("T01.Hi", alarm.SourceReference);
|
||||
Assert.Equal("AnalogLimit.Hi", alarm.AlarmTypeName);
|
||||
Assert.Equal("Process", alarm.Category);
|
||||
@@ -107,6 +108,41 @@ public class SiteStreamGrpcClientTests
|
||||
Assert.Equal("100", alarm.LimitValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gitea #129: the wire carries the raw priority. MxAccess and computed rows band it onto a
|
||||
/// higher-is-more-urgent Severity (AVEVA priority 1 → 1000); the raw value stays on Priority.
|
||||
/// The wire meaning is unchanged, so this is also right for a site on an older build.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData("NativeMxAccess", 1, 1000)]
|
||||
[InlineData("NativeMxAccess", 400, 750)]
|
||||
[InlineData("NativeMxAccess", 950, 250)]
|
||||
[InlineData("Computed", 100, 1000)]
|
||||
[InlineData("Computed", 700, 500)]
|
||||
[InlineData("NativeOpcUa", 950, 950)]
|
||||
public void ConvertToDomainEvent_AlarmChanged_SeverityIsOneDirection(string kind, int wirePriority, int expectedSeverity)
|
||||
{
|
||||
var evt = new SiteStreamEvent
|
||||
{
|
||||
AlarmChanged = new AlarmStateUpdate
|
||||
{
|
||||
InstanceUniqueName = "Site1.Motor01",
|
||||
AlarmName = "A",
|
||||
State = AlarmStateEnum.AlarmStateActive,
|
||||
Priority = wirePriority,
|
||||
Timestamp = Timestamp.FromDateTimeOffset(DateTimeOffset.UnixEpoch),
|
||||
Kind = kind,
|
||||
Active = true
|
||||
}
|
||||
};
|
||||
|
||||
var alarm = Assert.IsType<AlarmStateChanged>(SiteStreamGrpcClient.ConvertToDomainEvent(evt));
|
||||
|
||||
Assert.Equal(expectedSeverity, alarm.Condition.Severity);
|
||||
Assert.Equal(wirePriority, alarm.Condition.Priority);
|
||||
Assert.Equal(wirePriority, alarm.Priority);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an unknown stream event converts to null.</summary>
|
||||
[Fact]
|
||||
public void ConvertToDomainEvent_UnknownEvent_ReturnsNull()
|
||||
|
||||
@@ -436,7 +436,8 @@ public class SiteCommandDtoMapperGoldenTests
|
||||
[Fact]
|
||||
public void NativeAlarm_KeepsItsExplicitCondition()
|
||||
{
|
||||
var condition = new AlarmConditionState(true, false, true, AlarmShelveState.PermanentShelved, true, 999);
|
||||
// Priority equals the row's own Priority, as NativeAlarmActor always emits it (Gitea #129).
|
||||
var condition = new AlarmConditionState(true, false, true, AlarmShelveState.PermanentShelved, true, 999, Priority: 500);
|
||||
var original = new AlarmStateChanged("inst", "alarm", AlarmState.Active, 500, DateTimeOffset.UtcNow)
|
||||
{
|
||||
Kind = AlarmKind.NativeMxAccess,
|
||||
@@ -450,6 +451,25 @@ public class SiteCommandDtoMapperGoldenTests
|
||||
Assert.Equal(condition, restored.Condition);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The debug snapshot's condition DTO carries Severity only; the raw priority (Gitea #129) is
|
||||
/// restored from the row's Priority so the MES band keeps reading it on central too.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void NativeAlarm_RestoresTheRawPriorityOnItsCondition()
|
||||
{
|
||||
var original = new AlarmStateChanged("inst", "alarm", AlarmState.Active, 950, DateTimeOffset.UtcNow)
|
||||
{
|
||||
Kind = AlarmKind.NativeMxAccess,
|
||||
Condition = new AlarmConditionState(true, false, null, AlarmShelveState.Unshelved, false, 250, Priority: 950)
|
||||
};
|
||||
|
||||
var restored = SiteCommandDtoMapper.FromProto(SiteCommandDtoMapper.ToProto(original));
|
||||
|
||||
Assert.Equal(250, restored.Condition.Severity);
|
||||
Assert.Equal(950, restored.Condition.Priority);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Group classification + rejection
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -301,7 +301,7 @@ internal static class SiteCommandSamples
|
||||
Level = AlarmLevel.None,
|
||||
Message = "Level high",
|
||||
Kind = AlarmKind.NativeOpcUa,
|
||||
Condition = new AlarmConditionState(true, false, false, AlarmShelveState.OneShotShelved, true, 850),
|
||||
Condition = new AlarmConditionState(true, false, false, AlarmShelveState.OneShotShelved, true, 850, Priority: 850),
|
||||
SourceReference = "Tank01.Level.HiHi",
|
||||
AlarmTypeName = "AnalogLimitAlarm.HiHi",
|
||||
Category = "Process",
|
||||
|
||||
Reference in New Issue
Block a user