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

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:
Joseph Doherty
2026-11-12 12:14:48 -05:00
parent fa811feb3d
commit b61f3e0560
14 changed files with 268 additions and 83 deletions
+8 -4
View File
@@ -387,10 +387,14 @@ The proto contract carries the rich payload — alarm full reference,
source-object reference, alarm-type-name, transition kind (Raise /
Acknowledge / Clear / Retrigger), severity (raw MxAccess scale),
original raise timestamp, transition timestamp, operator user,
operator comment, alarm category, description. `MxAccessSeverityMapper`
(PR B.1) translates the raw severity onto the four-bucket
`AlarmSeverity` ladder — boundaries match v1's `GalaxyAlarmTracker`
so customers see no surprise re-classification.
operator comment, alarm category, description. The raw severity is the
AVEVA alarm **priority** (1 = most urgent … 999). `MxAccessSeverityMapper`
maps it through `GalaxyAlarmPriority`, AVEVA's default ranges (1–250 →
Critical 1000, 251–500 → High 750, 501–750 → Medium 500, 751–999 → Low 250;
lmxopcua#887, which reversed the earlier higher-is-worse ladder), and the
driver passes that exact value as `AlarmEventArgs.OpcUaSeverity`, so a live
condition and its AVEVA history row (`docs/Historian.md`) carry the same
Severity.
The richer fields surface on `Core.Abstractions.AlarmEventArgs` via
the optional properties added in PR E.7 (`OperatorComment`,
+26 -1
View File
@@ -698,13 +698,38 @@ source is read (fail closed).
| Field | Source | Notes |
|---|---|---|
| `Severity` | `priority` (AVEVA native rows) through `GalaxyAlarmPriority`, the live Galaxy mapping (`< 250` → 250, `< 500` → 500, `< 750` → 750, else 1000), else `severity` clamped to 1–1000 (OtOpcUa's own rows), else 1 | AVEVA's own `severity` (1–4, or 0 for a system event) is not on the OPC UA scale and is never used for a row that has `priority`. ⚠️ AVEVA ranks priority 1 as the most urgent and 999 as the least; the live mapping treats a higher number as more severe. History follows live so the two agree (lmxopcua#887 tracks the inversion). |
| `Severity` | `priority` (AVEVA native alarm rows) through `GalaxyAlarmPriority`, the live Galaxy mapping (see [Galaxy alarm priority → Severity](#galaxy-alarm-priority--severity-lmxopcua887)); a row whose AVEVA `severity` is 0 (an event that is not an alarm: `System.*`, `User.Write`) reads 1; else `severity` clamped to 1–1000 (OtOpcUa's own rows), else 1 | AVEVA's own `severity` (1–4) is not on the OPC UA scale and is never the value of an alarm row: the live feed carries only the priority, so using it would let history and live disagree. History and live use the same table. |
| `ConditionName` | The live condition's name when the row's `source_conditionvariable` names a native condition that fires on this folder; else `alarmname` (OtOpcUa's own rows); else the attribute part of `source_conditionvariable` (`TempHiAlarm`) | Live parity: a resolved row shows the same ConditionName as the live condition. |
| `SourceNode` | The native condition whose `attributeRef` equals the row's `source_conditionvariable` (case-insensitive, as the Galaxy driver matches it), among the conditions that fire on this folder; else the condition `alarmid` names (OtOpcUa's own rows) | A condition that does not fire on this folder is never named, so history discloses no node outside the folder's live event set. Object-level system events have none. |
| `Message` | `message` (OtOpcUa's own rows), else the event type (`Alarm.Set`, `System.Stop`) | Unchanged apart from case. |
| `AlarmId` (internal) | `alarmid`, else `alarm_id` | Used only to resolve `SourceNode`. AVEVA's `alarm_id` identifies one alarm occurrence (shared by its Set and Clear), not a condition, so it is never a ConditionName. |
##### Galaxy alarm priority → Severity (lmxopcua#887)
AVEVA alarm priority runs 1–999 and **1 is the most urgent**. AVEVA groups priorities into four severities; OtOpcUa uses
AVEVA's default ranges, for live Galaxy alarms (`MxAccessSeverityMapper`, which reads the priority mxaccessgw sends as the
alarm's `severity`) and for AVEVA history rows (`EventMapper`) alike, through the one table `GalaxyAlarmPriority`:
| 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 |
| any (history only; AVEVA `severity` 0: `System.*`, `User.Write`) | 0 (not an alarm) | – | 1 |
- Out-of-range priorities clamp: 0 or below reads Critical, above 999 reads Low.
- The live condition carries the table's exact value (the Galaxy driver sets `AlarmEventArgs.OpcUaSeverity`); before #887 it
carried only the bucket, which the host re-mapped through its generic 200/500/700/900 ladder.
- Evidence (vd03 `Runtime.dbo.Events`, read-only SQL, 2026-11-12): alarms at priority 1 and 100 carry AVEVA severity 1,
400 and 500 severity 2, 600 and 700 severity 3, and every priority-999 `System.*` / `User.*` row severity 0 with
`IsAlarm` 0. vd03 has no alarm in 751–999, so the Low edge rests on AVEVA's documented default (751–999).
- A Galaxy can reconfigure AVEVA's ranges. The live feed carries no AVEVA severity, so OtOpcUa assumes the defaults on both
paths: live and history always agree, but on a reconfigured Galaxy the bucket can differ from AVEVA's own.
- **Before #887 the table ran the other way** (`< 250` → 250 … `≥ 750` → 1000): priority 1 read Low, 999 read Critical, and
#886's history rows followed it. Every OPC UA client that ranks or filters Galaxy alarms by Severity sees the change.
Event-field projection supports the standard
`BaseEventType` select clauses — `EventId`, `SourceName`, `Time`, `ReceiveTime`, `Message`,
`Severity` — plus (client-visible: these used to be null):
@@ -108,7 +108,7 @@ each under its own raw device folder.
| Paging | 1-event pages (92 pages) and 7-event pages (14 pages) equal the whole read, row for row. |
| Reverse | Whole and 5-event pages (19 pages): newest first, ties by EventId descending, equal to the expected order. |
| SourceNode / ConditionName | **73 rows** resolve to their live condition: 70 `CPULoadAvg` alarm rows → `Plant/Galaxy/DevWinPlatform/CPULoadHiAlarm` / `CPULoadHiAlarm`, 3 `FRR_Warning` rows → `Plant/Galaxy/Left_001/FRR_Warning` / `FRR_Warning`. Every other row (system and user events) has a null SourceNode. |
| Severity | The wire equals the backend's mapping for every row. Alarm rows (`priority` 500 / 700) read **750**; priority-999 system and user events read **1000** (live Galaxy table; see lmxopcua#887 on its direction). Before: 1 for every row. |
| Severity | The wire equals the backend's mapping for every row. Alarm rows (`priority` 500 / 700) read **750**; priority-999 system and user events read **1000** (live Galaxy table, then still inverted; lmxopcua#887 reversed it, see "#887 re-run" below). Before: 1 for every row. |
`diff_runs.py` against `aveva-gate-sdk2-882.json.gz`: 226 shared cases, **221 identical**. The 5 that differ are expected:
the 4 `DevWinPlatform` event cases (same 72 events, ids, times, sources and messages; only Severity changed from 1 to
@@ -118,6 +118,27 @@ the 4 `DevWinPlatform` event cases (same 72 events, ids, times, sources and mess
Report: [`aveva-history-801/aveva-gate-sdk2-886.json.gz`](aveva-history-801/aveva-gate-sdk2-886.json.gz), sha256 of the
JSON `6ca314ee…c890` (label `sdk2-886-da948e5f-wt`: the #886 working tree on top of `da948e5f`).
## #887 re-run (2026-11-12)
The same harness after lmxopcua#887 reversed the Galaxy priority table (AVEVA priority 1 = most urgent: 1–250 → 1000,
251–500 → 750, 501–750 → 500, 751–999 → 250; AVEVA severity 0, an event that is not an alarm → 1). Same vd03
HistorianGateway `ea9f21c`, same window, read-only, network-only, **no WinRM**. The API key was read over SMB from the
`OtOpcUaHost.reg` export in vd03's `OtOpcUa-backups\pre-20261112-otopcua-5fb4b4db`, kept in a 0600 file, passed by
environment and never printed; the local copies were deleted afterwards.
`diff_runs.py` against `aveva-gate-sdk2-886.json.gz`: 233 cases, **224 identical**, harness failures 0. The 9 that differ
are the 4 `DevWinPlatform` event cases and the 5 `events886.*` wire cases, and **only Severity changed**: same events,
ids, times, sources and messages in every case. On `EQ-886-ZipsenLeft` (92 events):
| Rows | Severity #886 → #887 |
|---|---|
| 70 `DevWinPlatform.CPULoadAvg` alarm rows (priority 500, AVEVA High) | 750 → 750 |
| 3 `Left_001` `FRR_Warning` alarm rows (priority 700, AVEVA Medium) | 750 → **500** |
| 19 system and user events (`System.Start/Stop/Deploy/Undeploy`, `User.Write[.Secured]`; priority 999, AVEVA severity 0) | 1000 → **1** |
Report: [`aveva-history-801/aveva-gate-sdk2-887.json.gz`](aveva-history-801/aveva-gate-sdk2-887.json.gz), sha256 of the
JSON `232f3ee6…5545` (label `sdk2-887-wt`: the #887 working tree on top of `3a312ffd`).
## Files
- [`aveva-history-801/aveva-gate-sdk2-d46044f5.json.gz`](aveva-history-801/aveva-gate-sdk2-d46044f5.json.gz), sha256 of the JSON `00b5ecb6…fc27`.