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:
Joseph Doherty
2026-11-12 13:58:28 -05:00
parent e36cd6055e
commit dd4f1c2aa3
39 changed files with 686 additions and 120 deletions
@@ -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()