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
@@ -164,8 +164,10 @@ The override **routes left vs right off the SAPID suffix** — `_A` ⇒ Left, `_
```csharp
// CvdReactor.SimpleAlarmStatus (sketch)
// Q1 DECIDED: MES relevance = dedicated severity band. Galaxy alarm priorities for
// Q1 DECIDED: MES relevance = dedicated band. Galaxy alarm priorities for
// MES-relevant alarms are configured into 900-999; nothing else may use that band.
// Gitea #129: the band is the RAW priority, ScriptAlarm.Priority. ScriptAlarm.Severity is
// banded (higher = more urgent) since #129 and never lands in 900-999 for these alarms.
const int MesBandMin = 900;
const int MesBandMax = 999;
try {
@@ -189,13 +191,13 @@ try {
.Where(a => a.Active && !a.IsConfiguredPlaceholder) // only triggered
.Where(a => InScope(a.NativeSourceCanonicalName)) // _A => Left*, _B => Right*, + ReactorAlarms
// SimpleAlarmStatus semantics: flagged-only (= MES band, Q1) + acked always included
.Where(a => a.Severity >= MesBandMin && a.Severity <= MesBandMax)
.Where(a => a.Priority >= MesBandMin && a.Priority <= MesBandMax)
.Select(a => new {
Name = a.Name,
HierarchicalName = code + "." + a.Name,
Description = string.IsNullOrEmpty(a.Message) ? a.AlarmTypeName : a.Message, // Q3 DECIDED
IsFlaggedForMES = a.Severity >= MesBandMin && a.Severity <= MesBandMax, // Q1 DECIDED: real predicate
Severity = a.Severity,
IsFlaggedForMES = a.Priority >= MesBandMin && a.Priority <= MesBandMax, // Q1 DECIDED: real predicate
Severity = a.Priority, // MES Severity = raw 0-999 priority (#129)
StatusCode = a.Acknowledged ? "Triggered.Acked" : "Triggered",
TriggeredDT = (a.OriginalRaiseTime ?? a.Timestamp),
AckDT = a.AckTime, // Q4 DECIDED: enriched mirror
@@ -214,7 +216,7 @@ try {
## 6. Key design decisions — ALL DECIDED 2026-08-01 (design review with user)
1. **Superseded for COMPUTED alarms 2026-09-11** by `2026-09-11-mes-reportable-alarm-flag-design.md` — still the rule for native alarms. **`IsFlaggedForMES` for native alarms — DECIDED: severity band.** MES relevance is encoded in the alarm severity itself: MES-relevant alarms are configured (in the Galaxy alarm priority) into a **dedicated band 900–999** reserved exclusively for MES-relevant alarms, so it cannot collide with ordinary criticality tuning. Scripts carry `MesBandMin = 900` / `MesBandMax = 999` as named constants; `IsFlaggedForMES = (Severity in band)` is reported honestly per row; `SimpleAlarmStatus` (always flagged-only) and `AlarmStatus` with `FlaggedOnly=true` filter by the band predicate. Since `Severity` is returned verbatim, MES's own `MinSeverity`/`MaxSeverity` filters compose naturally with the band. No new tables, code flags, or allow-lists. **Operational prerequisite:** the Galaxy alarm priorities for MES-relevant CvdReactor alarms must be set into 900–999 before the endpoints are meaningful.
1. **Superseded for COMPUTED alarms 2026-09-11** by `2026-09-11-mes-reportable-alarm-flag-design.md` — still the rule for native alarms. **`IsFlaggedForMES` for native alarms — DECIDED: severity band.** MES relevance is encoded in the alarm severity itself: MES-relevant alarms are configured (in the Galaxy alarm priority) into a **dedicated band 900–999** reserved exclusively for MES-relevant alarms, so it cannot collide with ordinary criticality tuning. Scripts carry `MesBandMin = 900` / `MesBandMax = 999` as named constants; `IsFlaggedForMES = (Priority in band)` is reported honestly per row (**Gitea #129, 2026-11-12:** the band and the MES `Severity` field read `ScriptAlarm.Priority`, the raw priority — `ScriptAlarm.Severity` is now the banded, higher-is-more-urgent severity; deployed scripts that test `a.Severity` must switch to `a.Priority`); `SimpleAlarmStatus` (always flagged-only) and `AlarmStatus` with `FlaggedOnly=true` filter by the band predicate. Since `Severity` is returned verbatim, MES's own `MinSeverity`/`MaxSeverity` filters compose naturally with the band. No new tables, code flags, or allow-lists. **Operational prerequisite:** the Galaxy alarm priorities for MES-relevant CvdReactor alarms must be set into 900–999 before the endpoints are meaningful.
2. **MESReceiver version — DECIDED: dropped entirely.** With no native alarm sources there is no live state; a config-only catalog answer was judged not worth shipping. Only CvdReactor implements the scripts; other machines get the router's clean "not supported on machine" error (§5.1). No BTDB `MachineAlarm` dependency remains.
+1 -1
View File
@@ -236,7 +236,7 @@ Both enriched `AlarmStateChanged` events (live, via the gRPC stream) and the ini
Per-leaf alarm rendering (leaf nodes are individual conditions for native alarms, and the alarm itself for computed alarms):
- **Kind badge** — distinguishes **Computed** alarms from native ones (**OPC UA** or **MxAccess**), driven by the event's `AlarmKind` discriminator.
- **Sev** — the unified **0–1000 severity** (`AlarmConditionState.Severity`). Computed rows surface their integer priority on the same scale.
- **Sev** — the unified **0–1000 severity** (`AlarmConditionState.Severity`), **higher = more urgent for every kind** (Gitea #129). OPC UA rows show their Part 9 Severity; MxAccess and computed rows show their priority banded: AVEVA priority 1–250 → 1000, 251–500 → 750, 501–750 → 500, 751–999 → 250 (`AlarmPriorityBands`, the same table as OtOpcUa `GalaxyAlarmPriority`, lmxopcua#887). The Alarm Summary's worst-severity tile (max), severity-threshold filter (`>=`) and severity sort (descending default) all read this one direction. The raw priority stays on `AlarmConditionState.Priority` / `AlarmStateChanged.Priority`.
- **Source reference subtitle** — for native rows, the `SourceReference` (e.g. `Tank01.Level.HiHi`) renders as a monospace subtitle. Computed rows have no subtitle.
- **State badges** — orthogonal condition sub-states: **Unacked**, **Shelved**, and **Suppressed** appear only when the corresponding `AlarmConditionState` flag is set. Computed alarms are auto-acked and never shelved/suppressed.
- **Row tooltip** — surfaces native metadata not warranting its own column: `AlarmTypeName`, category, operator user and comment, original raise time, current/limit value.
+1 -1
View File
@@ -37,7 +37,7 @@ Commons must define shared primitive and utility types used across multiple comp
- **`AlarmKind` enum**: Computed, NativeOpcUa, NativeMxAccess. Discriminates how an alarm's state is produced — evaluated at the site by an `AlarmActor` from attribute triggers (`Computed`) vs. mirrored read-only from a native source (OPC UA Alarms & Conditions / MxAccess Gateway).
- **`AlarmShelveState` enum**: Unshelved, OneShotShelved, TimedShelved, PermanentShelved. OPC UA Part 9 shelving sub-state of an alarm condition; mirrored read-only from the source. Computed alarms are always `Unshelved`.
- **`AlarmTransitionKind` enum**: Snapshot, SnapshotComplete, Raise, Acknowledge, Clear, Retrigger, StateChange. Classifies a `NativeAlarmTransition`; `Snapshot`/`SnapshotComplete` carry the initial active-condition replay produced on every (re)subscribe so consumers can re-seed state.
- **`AlarmConditionState`**: The unified, read-only alarm condition state. The OPC UA Part 9 sub-conditions are orthogonal (and MxAccess's ACTIVE / ACTIVE_ACKED / INACTIVE map cleanly onto them), so they are modeled as independent flags — `Active`, `Acknowledged`, `Confirmed` (`bool?`; null when not confirmable), `Shelve` (`AlarmShelveState`), `Suppressed` — plus a `Severity` on the unified 0–1000 scale. Computed alarms populate it from State + Priority; native alarms mirror it from the source. (Helper `AlarmConditionStateFactory` builds the computed-alarm projection.)
- **`AlarmConditionState`**: The unified, read-only alarm condition state. The OPC UA Part 9 sub-conditions are orthogonal (and MxAccess's ACTIVE / ACTIVE_ACKED / INACTIVE map cleanly onto them), so they are modeled as independent flags — `Active`, `Acknowledged`, `Confirmed` (`bool?`; null when not confirmable), `Shelve` (`AlarmShelveState`), `Suppressed` — plus a `Severity` on the unified 0–1000 scale, **higher = more urgent for every source** (Gitea #129), and a trailing `Priority` (`int?`). OPC UA rows carry their Part 9 Severity (and repeat it as `Priority` — OPC UA has no separate priority); MxAccess Gateway rows and computed alarms band their AVEVA-style priority (lower = more urgent) onto `Severity` — AVEVA priority 1–250 → 1000, 251–500 → 750, 501–750 → 500, 751–999 → 250 (`AlarmPriorityBands`, the same table as OtOpcUa `GalaxyAlarmPriority`, lmxopcua#887) — and keep the raw value on `Priority`, which is exactly what `Severity` held before #129 and what the native MES 900–999 band keys off. Never sort or threshold on `Priority`; its direction depends on the source. `Priority` is null only on a pre-#129 persisted row (the native mirror upgrades it on rehydrate). (Helper `AlarmConditionStateFactory` builds the computed-alarm projection and `SeverityFromPriority(kind, priority)` for central's stream decode.)
- **`NativeAlarmTransition`**: The protocol-neutral alarm transition emitted by an `IAlarmSubscribableConnection` adapter — `SourceReference` (stable per-condition key), `SourceObjectReference` (owning source object, used for instance routing), `AlarmTypeName`, `Kind` (`AlarmTransitionKind`), `Condition` (`AlarmConditionState`), source taxonomy/operator/value metadata (`Category`, `Description`, `Message`, `OperatorUser`, `OperatorComment`, `CurrentValue`, `LimitValue`), and `OriginalRaiseTime?` / `TransitionTime`.
- **`ConnectionHealth` enum**: Connected, Disconnected, Connecting, Error.
- **`TrackedOperationId`**: A GUID identifying a tracked store-and-forward operation (`ExternalSystem.CachedCall`, `Database.CachedWrite`, `Notify.Send`). Generated caller-side at the site at call time, returned to the script as a tracking handle, and reused as the idempotency key for telemetry sent to central. The notification domain's existing `NotificationId` is the notification-specific name for this same concept.
+1 -1
View File
@@ -251,7 +251,7 @@ read the traffic". TLS is follow-on hardening and does not change this design.
- **Server-side mapping (`StreamRelayActor.HandleAlarmStateChanged`)**: maps the enriched domain `AlarmStateChanged` event — `Kind` + `AlarmConditionState` + native metadata — out to the proto `AlarmStateUpdate`. The nullable `original_raise_time` is emitted only when present, and `shelve_state` is mapped from the domain shelve enum to its wire string via a new **`AlarmShelveStateCodec`** (string↔enum, defaulting to `Unshelved`). The domain `Confirmed` (`bool?`) is collapsed to a definite bool for field 11.
- **Placeholder rows are dropped at the relay**: `is_configured_placeholder` (field 23) is a **Debug View snapshot-only** concept emitted by `InstanceActor.BuildAlarmStatesSnapshot` for quiet bindings — it is never a real alarm transition (its timestamp may be `DateTimeOffset.MinValue`, the Protobuf `Timestamp` lower boundary). `StreamRelayActor.HandleAlarmStateChanged` therefore returns early — **never relaying a placeholder row to the live gRPC stream** — so field 23 is always `false` on the live stream and only ever carries `true` in the snapshot path.
- **Client-side mapping (`SiteStreamGrpcClient.ConvertToDomainEvent`)**: reconstructs the domain `AlarmStateChanged` from the proto — `Kind` is parsed via `ParseAlarmKind`, the `Condition` is rebuilt with `severity` taken from the existing wire `priority`, and native metadata is repopulated from fields 8–23 (`native_source_canonical_name` → `NativeSourceCanonicalName`, `is_configured_placeholder` → `IsConfiguredPlaceholder`) — so central-side consumers receive the same domain event the site emitted.
- **Client-side mapping (`SiteStreamGrpcClient.ConvertToDomainEvent`)**: reconstructs the domain `AlarmStateChanged` from the proto — `Kind` is parsed via `ParseAlarmKind`, the `Condition` is rebuilt from the existing wire `priority`, which keeps its pre-#129 meaning (the raw source number): `Condition.Priority` = that value and `Condition.Severity` = `AlarmConditionStateFactory.SeverityFromPriority(kind, priority)` — banded for `NativeMxAccess`/`Computed`, verbatim for `NativeOpcUa` (Gitea #129; correct for an un-upgraded site too, since the wire is unchanged). The debug-snapshot `DebugAlarmStateDto` carries `Condition.Severity` explicitly and the decoder restores `Condition.Priority` from the row's `priority`; and native metadata is repopulated from fields 8–23 (`native_source_canonical_name` → `NativeSourceCanonicalName`, `is_configured_placeholder` → `IsConfiguredPlaceholder`) — so central-side consumers receive the same domain event the site emitted.
> **Regeneration is manual (macOS-only).** `sitestream.proto` is **not** auto-compiled: the `<Protobuf>` include is commented out in the `.csproj`, and the generated C# is **vendored** under `SiteStreamGrpc/`. To regenerate after editing the proto: toggle the `<Protobuf>` include on, build so `Grpc.Tools` regenerates the C#, copy the generated files into `SiteStreamGrpc/`, then re-comment the include. Adding `AlarmStateUpdate` fields 8–23 and the four unary RPCs (`IngestAuditEvents`, `IngestCachedTelemetry`, `PullAuditEvents`, `PullSiteCalls`) plus their message types followed this process, as did WP2.3's two additive fields — `PullAuditEventsRequest.after_id` (field 3, `sitestream.proto`) and `DebugSnapshotRequestDto.alarms_only` (field 3, `site_command.proto`) — and R2's batching additions (`InstanceStreamRequest.batching_supported` field 3, `SiteStreamRequest.batching_supported` field 2, `SiteStreamEvent.batch` field 4 and the new `SiteStreamEventBatch` message). The same applies to `site_command.proto`/`SiteCommandGrpc/` and `central_control.proto`/`CentralControlGrpc/`; `docker/regen-proto.sh [sitestream|centralcontrol|sitecommand|all]` automates the toggle-build-copy-untoggle and always restores the csproj.
@@ -355,7 +355,9 @@ All defined in Commons so the feed is identical across protocols:
| Type | Shape |
|------|-------|
| `NativeAlarmTransition` | `SourceReference`, `SourceObjectReference`, `AlarmTypeName`, `Kind`, `Condition`, `Category`, `Description`, `Message`, `OperatorUser`, `OperatorComment`, `OriginalRaiseTime?`, `TransitionTime`, `CurrentValue`, `LimitValue`, `AckTime?` |
| `AlarmConditionState` | `Active`, `Acknowledged`, `Confirmed?` (null when not confirmable), `Shelve`, `Suppressed`, `Severity` (0–1000) |
| `AlarmConditionState` | `Active`, `Acknowledged`, `Confirmed?` (null when not confirmable), `Shelve`, `Suppressed`, `Severity` (0–1000, higher = more urgent), `Priority?` (raw source number) |
**Severity direction (Gitea #129).** `Severity` is OPC UA Part 9 direction for both adapters. `OpcUaAlarmMapper` clamps the server's Severity to 0–1000 and repeats it as `Priority`. `MxGatewayAlarmMapper` reads the gateway's `severity` field — which mxaccessgw fills from the AVEVA alarm **priority** (1 = most urgent) — bands it onto `Severity` (AVEVA priority 1–250 → 1000, 251–500 → 750, 501–750 → 500, 751–999 → 250 (`AlarmPriorityBands`, the same table as OtOpcUa `GalaxyAlarmPriority`, lmxopcua#887); out-of-range clamps: ≤ 0 → 1000, > 999 → 250) and keeps it on `Priority` clamped to 0–1000, i.e. the value `Severity` carried before #129. A Galaxy alarm therefore reads the same `Severity` through OtOpcUa and straight from the gateway.
| `AlarmTransitionKind` (enum) | `Snapshot`, `SnapshotComplete`, `Raise`, `Acknowledge`, `Clear`, `Retrigger`, `StateChange`, `SnapshotStart` (appended) |
`OperatorUser` / `OperatorComment` and `CurrentValue` / `LimitValue` are display-only mirrors from the source.
+1 -1
View File
@@ -254,7 +254,7 @@ Inbound API scripts **cannot** call shared scripts directly — shared scripts a
> **Reading alarm state via a routed script.** There is deliberately **no** `Route.To(...).GetAlarms(...)` verb. An inbound method that needs a machine's current alarms resolves the instance and calls a **site script** that uses the Site Runtime `Alarms.CurrentAsync()` accessor — `Route.To(code).Call("SimpleAlarmStatus", new { ... })`. Alarm state is per-instance and lives on the site's Instance Actor; keeping the read inside a site script means the filtering/shaping happens where the data is, and central stays a thin router. This is the shape the MES alarm-status endpoints use (`docs/plans/2026-06-30-mes-alarm-status-api.md` §5.1); see [Component-SiteRuntime.md](Component-SiteRuntime.md) → *Alarm State (read-only)* for the accessor.
>
> **Example — `AlarmStatus`.** The rig demo template `MesAlarmDemo`'s site script (`docker/seed-mes-alarm-demo.sh`) answers `Route.To(MachineCode).Call("AlarmStatus", new { AlarmFilter = new { FlaggedOnly, IncludeAcked, MinSeverity, MaxSeverity, NameFilter } })` with the legacy `AlarmStatusResponse { WasSuccessful, ErrorText, Alarms: AlarmInfo[] }`, setting `IsFlaggedForMES = a.IsMesReportable` per row for **computed** alarms (the template's `TemplateAlarm.IsMesReportable` flag, instance-overridable) and the **900–999 severity band** for **native** alarms, which carry no per-alarm `IsMesReportable` row. `SimpleAlarmStatus` is the same shape with `FlaggedOnly` hard-wired true.
> **Example — `AlarmStatus`.** The rig demo template `MesAlarmDemo`'s site script (`docker/seed-mes-alarm-demo.sh`) answers `Route.To(MachineCode).Call("AlarmStatus", new { AlarmFilter = new { FlaggedOnly, IncludeAcked, MinSeverity, MaxSeverity, NameFilter } })` with the legacy `AlarmStatusResponse { WasSuccessful, ErrorText, Alarms: AlarmInfo[] }`, setting `IsFlaggedForMES = a.IsMesReportable` per row for **computed** alarms (the template's `TemplateAlarm.IsMesReportable` flag, instance-overridable) and the **900–999 priority band** (on `ScriptAlarm.Priority`) for **native** alarms, which carry no per-alarm `IsMesReportable` row. The MES `Severity` field and `MinSeverity`/`MaxSeverity` filters are that raw 0–999 priority (`ScriptAlarm.Priority`), not the unified `ScriptAlarm.Severity`, which since Gitea #129 is banded and runs higher = more urgent. `SimpleAlarmStatus` is the same shape with `FlaggedOnly` hard-wired true.
#### Input/Output
- **Input parameters** are available as defined in the method definition.
+5 -3
View File
@@ -477,7 +477,9 @@ The `AlarmStateChanged` message published by both Alarm Actors and Native Alarm
- **`Kind`** (`AlarmKind`): `Computed` for ScadaBridge-evaluated alarms; `NativeOpcUa` / `NativeMxAccess` for mirrored native alarms.
- **`Condition`** (`AlarmConditionState`): the unified condition view. Computed alarms supply a computed default; native alarms carry the mirrored source condition.
- **Native metadata** (populated for native alarms; defaulted/empty for computed): `SourceReference`, `AlarmTypeName`, `Category`, `OperatorUser`, `OperatorComment`, `OriginalRaiseTime`, `CurrentValue`, `LimitValue`.
- **Computed-alarm projection**: computed alarms are surfaced as **auto-acknowledged** with `Severity = Priority`, so a single enriched shape carries both computed and native alarms onto the stream and into the DebugView snapshot.
- **Computed-alarm projection**: computed alarms are surfaced as **auto-acknowledged** with `Severity` = their priority banded through `AlarmPriorityBands` (AVEVA priority 1–250 → 1000, 251–500 → 750, 501–750 → 500, 751–999 → 250 (`AlarmPriorityBands`, the same table as OtOpcUa `GalaxyAlarmPriority`, lmxopcua#887); Gitea #129 — `PriorityLevel` is lower = more urgent) and `Condition.Priority` = the raw priority, so a single enriched shape carries both computed and native alarms onto the stream and into the DebugView snapshot. `AlarmStateChanged.Priority` is the raw number for every kind (a native row's `Condition.Priority`).
- **`alarm` site-event severity**: a raise logs `Error` when its unified `Severity` is ≥ 700 (Critical/High: priority 1–500, or OPC UA Severity ≥ 700), else `Warning` — the same threshold in `AlarmActor` and `NativeAlarmActor`. Before #129 the computed path compared the raw priority, so priority 700+ (the least urgent) logged as `Error`.
- **Pre-#129 persisted native rows**: `native_alarm_state.condition_json` written by an older build has the raw number in `Severity` and no `Priority`; `NativeAlarmActor` rehydration moves it to `Priority` and re-derives `Severity` (banded for MxAccess, kept for OPC UA).
- **`NativeSourceCanonicalName`** (`string?`): the canonical name of the native alarm source binding a native condition belongs to (e.g. `Motor1.NativeAlarms`). `null` for computed alarms. Stamped by the `NativeAlarmActor` on every emitted event; also carried on idle-binding placeholder rows emitted by the Instance Actor.
- **`IsConfiguredPlaceholder`** (`bool`): `true` for a synthetic row emitted by the Instance Actor to represent a configured native source binding that currently has no live conditions. These rows are never persisted or forwarded as operational alarms — they exist solely so the Debug View can render every configured binding as a tree node. `false` on all other rows.
- **`AckTime`** (`DateTimeOffset?`): the instant the condition was acknowledged; `null` while unacknowledged, `null` on computed alarms (auto-acked but with no operator ack event), and cleared on re-raise. Stamped by the DCL adapter — the source's own ack instant where the protocol supplies one (OPC UA A&C), the DCL's observation time of the ack transition where it does not (MxAccess Gateway) — and carried verbatim by the `NativeAlarmActor`, which never invents or suppresses one. See [Component-DataConnectionLayer.md](Component-DataConnectionLayer.md) → *Ack Timestamp*.
@@ -556,12 +558,12 @@ Available to all script runs and alarm on-trigger runs:
### Alarm State (read-only)
- `Alarms.CurrentAsync()` — Returns a snapshot of the instance's **current** alarm conditions as `IReadOnlyList<ScriptAlarm>` (`Commons.Types.Scripts`), covering computed alarms and mirrored native (OPC UA A&C / MxAccess Gateway) conditions alike. Served by a **local Ask** (`GetAlarmSnapshotRequest`/`GetAlarmSnapshotResponse`) to the script's own Instance Actor — the same mechanism attribute reads use, never a cross-cluster hop.
- `Alarms.MesReportableAsync()` — `CurrentAsync()` filtered to rows where `ScriptAlarm.IsMesReportable` is true (2026-09-11). Native alarms carry no per-alarm config row, so they never satisfy the filter — they still convey MES relevance via the 900–999 severity band (see `docs/plans/2026-06-30-mes-alarm-status-api.md`).
- `Alarms.MesReportableAsync()` — `CurrentAsync()` filtered to rows where `ScriptAlarm.IsMesReportable` is true (2026-09-11). Native alarms carry no per-alarm config row, so they never satisfy the filter — they still convey MES relevance via the 900–999 **priority** band — tested on `ScriptAlarm.Priority`, never on `Severity` (Gitea #129; see `docs/plans/2026-06-30-mes-alarm-status-api.md`).
- Available to **every** script, unlike the `Alarm` global, which exists only inside an on-trigger handler and describes the single alarm that fired. This accessor is what lets an inbound-API-routed `Call` script answer "what is in alarm on this machine right now?"; before it, native mirrored conditions were reachable only from the Debug View.
- **Read-only by design** — native alarms are a read-only mirror of the source (no ack-back), so the accessor exposes no acknowledge/shelve operation.
- **Not scope-prefixed**, unlike `Attributes`: alarm identity is not a scope-relative attribute name (computed alarms are keyed by configured name, native conditions by a source-supplied reference), so every scope sees the same list rather than a silently truncated one.
- The snapshot is built by the same `BuildAlarmStatesSnapshot()` the Debug View uses, so it **includes placeholder rows** for configured-but-quiet native source bindings (`IsConfiguredPlaceholder`). Callers enumerating real alarms filter on `Active && !IsConfiguredPlaceholder`.
- `ScriptAlarm` is a flat projection of the retained `AlarmStateChanged`: `Name`, `SourceReference`, `NativeSourceCanonicalName`, `Active`, `Acknowledged`, `Confirmed?`, `Shelved` (all shelve sub-states collapse to one boolean), `Suppressed`, `Severity` (0–1000), `Kind`, `Message`, `AlarmTypeName`, `Category`, `OperatorUser`, `OperatorComment`, `OriginalRaiseTime?`, `Timestamp`, `AckTime?`, `CurrentValue`, `LimitValue`, `IsConfiguredPlaceholder`, `IsMesReportable` (trailing, 2026-09-11 — always `false` on native rows). The `Condition` is the authority for active/acked/severity, so computed and native alarms read identically from one filter expression.
- `ScriptAlarm` is a flat projection of the retained `AlarmStateChanged`: `Name`, `SourceReference`, `NativeSourceCanonicalName`, `Active`, `Acknowledged`, `Confirmed?`, `Shelved` (all shelve sub-states collapse to one boolean), `Suppressed`, `Severity` (0–1000, higher = more urgent for every kind — Gitea #129), `Kind`, `Message`, `AlarmTypeName`, `Category`, `OperatorUser`, `OperatorComment`, `OriginalRaiseTime?`, `Timestamp`, `AckTime?`, `CurrentValue`, `LimitValue`, `IsConfiguredPlaceholder`, `IsMesReportable` (trailing, 2026-09-11 — always `false` on native rows), `Priority` (trailing, Gitea #129 — the raw source number: AVEVA priority for MxAccess, `PriorityLevel` for computed, Part 9 Severity for OPC UA; the pre-#129 `Severity` value, which the MES 900–999 band and the MES API's `Severity`/`MinSeverity`/`MaxSeverity` read). The `Condition` is the authority for active/acked/severity, so computed and native alarms read identically from one filter expression.
- **`IsMesReportable` is stamped at two points.** `InstanceActor.BuildAlarmStatesSnapshot()` sets `AlarmStateChanged.IsMesReportable` from `ResolvedAlarm.IsMesReportable` on every row it produces — retained (fired/active) rows and the fallback/idle rows alike — and `InstanceActor.HandleAlarmStateChanged` stamps the Alarm Actor's own live events before storing and publishing them, so `Alarms.CurrentAsync()`/`MesReportableAsync()`, the Debug View snapshot and the live stream always agree. Since ScadaBridge#60 the flag rides the wire as `AlarmStateUpdate.is_mes_reportable` (25) and `DebugAlarmStateDto.is_mes_reportable` (20); `DebugAlarmStateDto` also carries `ack_time` (21, ScadaBridge#84). Both are additive — an older peer reads `false`/null.
- The type lives in Commons so the runtime accessor and the Script Analysis compile-only surface (`ScriptCompileSurface.CompileAlarmsAccessor`) project to the **same** type — a script binding at the design-time gate binds identically at the site. Parity is pinned by `CompileSurfaceParityTests`.
- Design record: `docs/plans/2026-06-30-mes-alarm-status-api.md` §5.2.
+1 -1
View File
@@ -133,7 +133,7 @@ Sections 3.4 and 3.4.1 describe **computed alarms** — alarms ScadaBridge evalu
Because the source reference typically varies per physical instance (e.g. `Tank01` vs `Tank02`), an instance may **override** a non-locked binding's connection, source reference, and/or condition filter individually — a null override field keeps the inherited value. Bindings cannot be added or removed at the instance level, only retargeted.
**Unified condition model.** Computed and native alarms share one **condition state** so they present uniformly: orthogonal **Active**, **Acknowledged**, **Confirmed** (when confirmable), **Shelved**, and **Suppressed** flags plus a **0–1000 severity**. An **alarm kind** discriminator distinguishes `Computed`, `NativeOpcUa`, and `NativeMxAccess`. Computed alarms project onto this model as auto-acknowledged with severity equal to their priority; native alarms mirror the source condition directly.
**Unified condition model.** Computed and native alarms share one **condition state** so they present uniformly: orthogonal **Active**, **Acknowledged**, **Confirmed** (when confirmable), **Shelved**, and **Suppressed** flags plus a **0–1000 severity** in one direction for every kind — **higher = more urgent** (OPC UA Part 9) — and the source's raw **priority** alongside it. An **alarm kind** discriminator distinguishes `Computed`, `NativeOpcUa`, and `NativeMxAccess`. Computed alarms project onto this model as auto-acknowledged with their priority (lower = more urgent) banded onto severity with AVEVA's default ranges (1–250 → 1000, 251–500 → 750, 501–750 → 500, 751–999 → 250); MxAccess native alarms band their AVEVA priority the same way; OPC UA native alarms mirror the source Severity directly.
**Runtime behavior:**
- Native alarm sources are **discovered and subscribed at runtime** — at deploy time the site subscribes each resolved binding through the Data Connection Layer, which opens **one alarm feed per connection** and routes transitions to instances by source-object reference. A site-side actor (peer to the computed alarm actor) mirrors each source binding.