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:
@@ -5,33 +5,71 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
/// <c>MxAccessSeverityMapper</c>) and AVEVA alarm history (<c>priority</c>, Gitea #886) so the two agree.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The four-bucket ladder (250 / 500 / 750 / 1000 — Low / Medium / High / Critical) is the one v1's
|
||||
/// <c>GalaxyAlarmTracker</c> exposed; out-of-range inputs clamp into the nearest bucket. ⚠️ It treats a higher number
|
||||
/// as more severe, while AVEVA ranks priority 1 as the most urgent: lmxopcua#887 tracks that. Change it here only, so
|
||||
/// live and history move together.
|
||||
/// <para>
|
||||
/// AVEVA System Platform alarm priority runs 1–999 and <b>1 is the most urgent</b>. AVEVA groups priorities into
|
||||
/// four severities with these default ranges, and OtOpcUa uses the same boundaries (lmxopcua#887):
|
||||
/// </para>
|
||||
/// <list type="table">
|
||||
/// <listheader><term>AVEVA priority</term><description>AVEVA severity → bucket, OPC UA Severity</description></listheader>
|
||||
/// <item><term>1–250</term><description>1 Critical → <see cref="AlarmSeverity.Critical"/>, 1000</description></item>
|
||||
/// <item><term>251–500</term><description>2 High → <see cref="AlarmSeverity.High"/>, 750</description></item>
|
||||
/// <item><term>501–750</term><description>3 Medium → <see cref="AlarmSeverity.Medium"/>, 500</description></item>
|
||||
/// <item><term>751–999</term><description>4 Low → <see cref="AlarmSeverity.Low"/>, 250</description></item>
|
||||
/// </list>
|
||||
/// <para>
|
||||
/// The vd03 A&E history agrees: alarms at priority 1/100 carry AVEVA severity 1, 400/500 severity 2, 600/700
|
||||
/// severity 3 (docs/Historian.md, "Galaxy alarm priority → Severity").
|
||||
/// A priority outside 1–999 is clamped into it first, so 0 or a negative reads as Critical and anything above 999
|
||||
/// as Low. The mapping is monotonic: a lower priority number never yields a lower OPC UA Severity.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// AVEVA lets a Galaxy reconfigure the four ranges. The live alarm feed carries only the priority, never AVEVA's
|
||||
/// severity, so this table assumes the defaults on both paths; live and history therefore always agree, even on a
|
||||
/// Galaxy whose ranges were changed.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Before lmxopcua#887 the table ran the other way (a higher number was more severe), so a priority-1 alarm read
|
||||
/// 250 and a priority-999 one 1000. Change the table here only, so live and history move together.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static class GalaxyAlarmPriority
|
||||
{
|
||||
/// <summary>OPC UA severity of the Low bucket (raw < 250).</summary>
|
||||
/// <summary>The last priority of AVEVA's default Critical range (1–250).</summary>
|
||||
public const int CriticalMaxPriority = 250;
|
||||
|
||||
/// <summary>The last priority of AVEVA's default High range (251–500).</summary>
|
||||
public const int HighMaxPriority = 500;
|
||||
|
||||
/// <summary>The last priority of AVEVA's default Medium range (501–750); 751–999 is Low.</summary>
|
||||
public const int MediumMaxPriority = 750;
|
||||
|
||||
/// <summary>OPC UA severity of the Low bucket (priority 751–999).</summary>
|
||||
public const int OpcUaSeverityLow = 250;
|
||||
|
||||
/// <summary>OPC UA severity of the Medium bucket (raw 250–499).</summary>
|
||||
/// <summary>OPC UA severity of the Medium bucket (priority 501–750).</summary>
|
||||
public const int OpcUaSeverityMedium = 500;
|
||||
|
||||
/// <summary>OPC UA severity of the High bucket (raw 500–749).</summary>
|
||||
/// <summary>OPC UA severity of the High bucket (priority 251–500).</summary>
|
||||
public const int OpcUaSeverityHigh = 750;
|
||||
|
||||
/// <summary>OPC UA severity of the Critical bucket (raw 750 and above).</summary>
|
||||
/// <summary>OPC UA severity of the Critical bucket (priority 1–250).</summary>
|
||||
public const int OpcUaSeverityCritical = 1000;
|
||||
|
||||
/// <summary>Maps a raw Galaxy priority onto the bucket and its OPC UA severity.</summary>
|
||||
/// <param name="rawPriority">The raw value (0–999; clamped).</param>
|
||||
/// <returns>The bucket and its OPC UA Part 9 numeric severity.</returns>
|
||||
/// <summary>
|
||||
/// OPC UA severity of an AVEVA event that is not an alarm (System.Start/Stop/Deploy, User.Write, …: priority 999,
|
||||
/// AVEVA severity 0). It sits below every alarm bucket. Only history has such rows; the live feed carries alarms.
|
||||
/// </summary>
|
||||
public const int OpcUaSeverityNotAnAlarm = 1;
|
||||
|
||||
/// <summary>Maps a Galaxy alarm priority onto the bucket and its OPC UA severity.</summary>
|
||||
/// <param name="rawPriority">The AVEVA priority (1 = most urgent … 999 = least; clamped into 1–999).</param>
|
||||
/// <returns>The bucket and its OPC UA Part 9 numeric severity (1000 = most severe).</returns>
|
||||
public static (AlarmSeverity Bucket, int OpcUaSeverity) Map(int rawPriority) => rawPriority switch
|
||||
{
|
||||
< 250 => (AlarmSeverity.Low, OpcUaSeverityLow),
|
||||
< 500 => (AlarmSeverity.Medium, OpcUaSeverityMedium),
|
||||
< 750 => (AlarmSeverity.High, OpcUaSeverityHigh),
|
||||
_ => (AlarmSeverity.Critical, OpcUaSeverityCritical),
|
||||
// The first arm also takes 0 and negatives, the last anything above 999: the clamp into 1–999.
|
||||
<= CriticalMaxPriority => (AlarmSeverity.Critical, OpcUaSeverityCritical),
|
||||
<= HighMaxPriority => (AlarmSeverity.High, OpcUaSeverityHigh),
|
||||
<= MediumMaxPriority => (AlarmSeverity.Medium, OpcUaSeverityMedium),
|
||||
_ => (AlarmSeverity.Low, OpcUaSeverityLow),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user