fix(galaxy): AVEVA priority 1 is the most urgent: Galaxy alarm Severity follows AVEVA's priority bands, live and history alike (#887)
v2-ci / unit-tests (push) Has been cancelled
v2-ci / unit-tests (push) Has been cancelled
GalaxyAlarmPriority, the one table live Galaxy alarms (MxAccessSeverityMapper) and AVEVA A&E history (EventMapper, #886) map the AVEVA priority through, ranked a higher number as more severe. AVEVA ranks priority 1 as the most urgent, so a priority-1 alarm read 250 and a priority-999 system event 1000. It now uses AVEVA's default priority ranges, most urgent first: AVEVA priority AVEVA severity bucket OPC UA Severity 1-250 1 Critical Critical 1000 251-500 2 High High 750 501-750 3 Medium Medium 500 751-999 4 Low Low 250 (history only) 0 not an alarm - 1 Out-of-range priorities clamp (<= 0 Critical, > 999 Low). Evidence, vd03 Runtime.dbo.Events read-only: priority 1/100 alarms carry AVEVA severity 1, 400/500 severity 2, 600/700 severity 3, and every priority-999 System.* / User.* row severity 0 with IsAlarm 0. - Live and history now use the same number. GalaxyDriver passes the table's exact value as AlarmEventArgs.OpcUaSeverity. Before, it passed only the bucket, which NativeAlarmProjector re-mapped through its generic 200/500/700/900 ladder, so live and history disagreed even before the direction fix. - History: a row whose AVEVA severity is 0 (System.Start/Stop/Deploy, User.Write...) is not an alarm and reads Severity 1, below every alarm band. For alarms AVEVA's 1-4 severity is still never the value: the live feed carries only the priority, so it cannot use it, and history must agree. - Docs: docs/Historian.md gains "Galaxy alarm priority -> Severity (lmxopcua#887)"; docs/AlarmTracking.md updated. - Live re-run of the #801/#886 harness against vd03 HistorianGateway ea9f21c (read-only, network-only): 224 of 233 cases identical to the #886 run, the 9 others differ only in Severity (priority-700 alarm rows 750 -> 500, 19 system/user events 1000 -> 1). Record: aveva-history-801.md "#887 re-run", report aveva-gate-sdk2-887.json.gz. Client-visible: every OPC UA client that ranks or filters Galaxy alarms by Severity sees the order reversed (live and HistoryReadEvents). ScadaBridge has no code that depends on the old values; its own mixed severity directions are ScadaBridge#129. Tests updated because they pinned the inverted table: - Driver.Galaxy.Tests MxAccessSeverityMapperTests (rewritten: edges 1/250/251/ 500/501/750/751/999, 0/-100/int.MinValue/1000/int.MaxValue clamps, monotonic-decreasing sweep, equals GalaxyAlarmPriority for -2..1001) - Driver.Galaxy.Tests GatewayGalaxyAlarmFeedTests (750 and 600 -> Medium) - Driver.Historian.Gateway.Tests EventMapperTests (band table, priority 999 -> 250, 1 -> 1000; system events -> 1) - OpcUaServer.IntegrationTests GalaxyEquipmentEventHistoryWireTests (700 -> 500, 100 -> 1000, System.Stop -> 1; the fake now stores AVEVA's real 0-4 severity) New tests: - Runtime.Tests NativeAlarmRestatementEndToEndTests .Galaxy_live_condition_severity_is_the_AVEVA_priority_band (8 rows: the real feed -> real GalaxyDriver -> host -> projector; the same rows EventMapperTests pins on the history path) - EventMapperTests.Aveva_severity_zero_marks_an_event_that_is_not_an_alarm (5 rows, incl. the priority-999 alarm positive control) - GalaxyDriverAlarmSourceTests: OpcUaSeverity passes through Mutation check, 8 mutants, all killed: the pre-#887 inverted table; the driver dropping OpcUaSeverity; no not-an-alarm rule; each of the three band edges off by one; history ignoring priority; a missing AVEVA severity read as 0. Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
This commit is contained in:
+9
-5
@@ -100,11 +100,12 @@ public sealed class GalaxyEquipmentEventHistoryWireTests
|
||||
// A system event has no condition at all.
|
||||
byId["o-stop"].SourceNode.ShouldBeNull();
|
||||
byId["o-stop"].ConditionName.ShouldBeNull();
|
||||
// Severity follows the live Galaxy table (priority 500 → 750, 700 → 750, 100 → 250, 999 → 1000), not Severity 1.
|
||||
// Severity follows the live Galaxy table, AVEVA priority 1 = most urgent (lmxopcua#887): 500 → 750 (High),
|
||||
// 700 → 500 (Medium), 100 → 1000 (Critical); a system event (priority 999, AVEVA severity 0) is not an alarm → 1.
|
||||
byId["o-temp-set"].Severity.ShouldBe((ushort)750);
|
||||
byId["d-set"].Severity.ShouldBe((ushort)750);
|
||||
byId["o-press-set"].Severity.ShouldBe((ushort)250);
|
||||
byId["o-stop"].Severity.ShouldBe((ushort)1000);
|
||||
byId["d-set"].Severity.ShouldBe((ushort)500);
|
||||
byId["o-press-set"].Severity.ShouldBe((ushort)1000);
|
||||
byId["o-stop"].Severity.ShouldBe((ushort)1);
|
||||
|
||||
// The raw device folder the conditions hang under reads the same objects (live: a condition fires on its parent).
|
||||
var raw = await ReadAsync(session, Raw(session, OvenFolder), forward: true, page: 0, ct);
|
||||
@@ -292,7 +293,10 @@ public sealed class GalaxyEquipmentEventHistoryWireTests
|
||||
e.Properties["source_object"] = s.Object;
|
||||
e.Properties["source_processvariable"] = s.Source;
|
||||
e.Properties["priority"] = s.Priority.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||
e.Properties["severity"] = s.ConditionVariable is null ? "0" : "2"; // AVEVA's own scale: never used
|
||||
// AVEVA's own scale, as vd03 stores it: 0 for an event that is not an alarm, else the priority's 1–4 band.
|
||||
// Only the 0 is read (not an alarm → Severity 1); an alarm's value comes from its priority.
|
||||
e.Properties["severity"] = s.ConditionVariable is null ? "0"
|
||||
: (s.Priority <= 250 ? "1" : s.Priority <= 500 ? "2" : s.Priority <= 750 ? "3" : "4");
|
||||
if (s.ConditionVariable is not null)
|
||||
{
|
||||
e.Properties["source_conditionvariable"] = s.ConditionVariable;
|
||||
|
||||
+39
-2
@@ -150,6 +150,43 @@ public sealed class NativeAlarmRestatementEndToEndTests : RuntimeActorTestBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// lmxopcua#887 — the live Galaxy condition's OPC UA Severity is AVEVA's priority band, most urgent first: the
|
||||
/// REAL feed → REAL driver → host → projector path carries <see cref="GalaxyAlarmPriority"/>'s exact value, the
|
||||
/// same table AVEVA history rows map their <c>priority</c> through (<c>EventMapperTests</c> pins the same rows),
|
||||
/// so a condition's live event and its history row agree. Before the fix priority 1 read 200 and 999 read 900
|
||||
/// (inverted, and re-mapped through the host's generic bucket ladder).
|
||||
/// </summary>
|
||||
/// <param name="priority">The AVEVA alarm priority (1 = most urgent).</param>
|
||||
/// <param name="expected">The OPC UA Severity the live condition must carry.</param>
|
||||
[Theory]
|
||||
[InlineData(1, 1000)]
|
||||
[InlineData(250, 1000)]
|
||||
[InlineData(251, 750)]
|
||||
[InlineData(500, 750)]
|
||||
[InlineData(501, 500)]
|
||||
[InlineData(750, 500)]
|
||||
[InlineData(751, 250)]
|
||||
[InlineData(999, 250)]
|
||||
public async Task Galaxy_live_condition_severity_is_the_AVEVA_priority_band(int priority, int expected)
|
||||
{
|
||||
var rig = await GalaxyRig.StartAsync(this);
|
||||
try
|
||||
{
|
||||
rig.Send(Live(Proto.AlarmTransitionKind.Raise, new DateTime(2026, 11, 12, 9, 0, 0, DateTimeKind.Utc), priority));
|
||||
|
||||
var update = NextAlarmUpdate(rig.Publish);
|
||||
update.State.Active.ShouldBeTrue();
|
||||
update.State.Severity.ShouldBe((ushort)expected);
|
||||
update.State.Severity.ShouldBe((ushort)GalaxyAlarmPriority.Map(priority).OpcUaSeverity,
|
||||
"live must use the table history uses");
|
||||
}
|
||||
finally
|
||||
{
|
||||
await rig.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private static Proto.AlarmFeedMessage Snapshot(
|
||||
Proto.AlarmConditionState state, DateTime raisedAt, DateTime lastTransition, string user = "", string comment = "")
|
||||
=> new()
|
||||
@@ -170,7 +207,7 @@ public sealed class NativeAlarmRestatementEndToEndTests : RuntimeActorTestBase
|
||||
},
|
||||
};
|
||||
|
||||
private static Proto.AlarmFeedMessage Live(Proto.AlarmTransitionKind kind, DateTime at)
|
||||
private static Proto.AlarmFeedMessage Live(Proto.AlarmTransitionKind kind, DateTime at, int priority = 750)
|
||||
=> new()
|
||||
{
|
||||
Transition = new Proto.OnAlarmTransitionEvent
|
||||
@@ -179,7 +216,7 @@ public sealed class NativeAlarmRestatementEndToEndTests : RuntimeActorTestBase
|
||||
SourceObjectReference = "Tank01",
|
||||
AlarmTypeName = "AnalogLimitAlarm.HiHi",
|
||||
TransitionKind = kind,
|
||||
Severity = 750,
|
||||
Severity = priority, // mxaccessgw sends the AVEVA priority in this field
|
||||
Category = "Process",
|
||||
Description = "Tank 01 high-high level",
|
||||
TransitionTimestamp = Timestamp.FromDateTime(at),
|
||||
|
||||
Reference in New Issue
Block a user