feat(alarms): condition Quality tracks source connectivity (#477)
Part 9 ConditionType.Quality was never assigned; default(StatusCode)==Good so every native + scripted condition reported Good unconditionally — a comms-lost device still showed a healthy, inactive, Good condition (a wrong-VALUE bug, distinct from the null-value #473/#475). Clients (and HMIs bucketing on IsGood) could not tell "genuinely inactive" from "lost contact". Layer 1 — make Quality a real, plumbed field: - AlarmConditionSnapshot gains OpcUaQuality Quality (default Good). - MaterialiseAlarmCondition sets it (native BadWaitingForInitialData, scripted Good). - WriteAlarmCondition projects snapshot.Quality; the delta-gate gains a Quality member so a quality-bucket change fires a Part 9 event. Layer 2 — drive native quality from driver connectivity (a comms-lost driver emits no alarm transitions, and an alarm-bearing raw tag has no value variable, so quality can't come from either existing channel): - DriverInstanceActor Tells parent ConnectivityChanged on Connected/Reconnecting. - DriverHostActor fans it to every native condition the driver owns as OpcUaPublishActor.AlarmQualityUpdate (Good on connect, Bad on disconnect). - New dedicated IOpcUaAddressSpaceSink.WriteAlarmQuality sets ONLY Quality and fires only on a bucket change — never touches Active/Acked/Retain (an active alarm that loses comms stays active). Not a full-snapshot re-projection, so it can't clobber severity/message and works for a never-fired condition. Forwarded through DeferredAddressSpaceSink (F10b trap; auto-verified by the reflection forwarding guard). Ungated by redundancy role; no /alerts row. Scripted conditions stay Good; worst-of-input quality deferred to #478 (Layer 3). Tests: node-level (materialise/project/no-clobber/unknown-node no-op), NativeAlarmProjector, DriverInstanceActor connectivity emission, DriverHostActor fan-out, OpcUaPublishActor routing, and the wire-level guard (Condition_event_Quality_tracks_source_connectivity_on_the_wire) — RED-verified against a simulated pre-fix always-Good server. Existing DriverInstanceActor parent probes ignore the new ConnectivityChanged. Docs: docs/AlarmTracking.md §"Condition source-data Quality (#477)"; design doc docs/plans/2026-07-17-alarm-condition-quality-477-design.md.
This commit is contained in:
@@ -68,6 +68,7 @@ are set explicitly. Leaving them unset shipped them as **null** on every event
|
||||
| `ConditionName` | the leaf / display name (e.g. `HR200`) | Where the short human-readable name lives |
|
||||
| `ConditionClassId` | always **`BaseConditionClassType`** | Part 9's "no condition class modelled" value. Unset shipped `NodeId.Null` (#475) |
|
||||
| `ConditionClassName` | always **`"BaseConditionClass"`** | Matches `ConditionClassId`. Unset shipped empty text (#475) |
|
||||
| `Quality` | the condition's **source-data quality** — tracks the native source's connectivity (`Good` / `Bad`) | A pure annotation; never alters Active/Acked/Retain. Unset shipped the accidentally-Good default (#477) — see below |
|
||||
|
||||
**Why `BaseConditionClassType` and not `ProcessConditionClassType`.** We hold no per-alarm
|
||||
classification at the materialize seam, and `ConditionClassId` is a wire contract clients bucket on.
|
||||
@@ -111,6 +112,43 @@ that `ConditionClassId` / `ConditionClassName` do too. The two class fields are
|
||||
> a live↔history join key. Correlate on `ConditionId` / the RawPath instead. (Pre-existing; the
|
||||
> live-path fix above does not change the history path.)
|
||||
|
||||
### Condition source-data Quality (#477)
|
||||
|
||||
`ConditionType.Quality` reports the quality of the condition's source data. It was never assigned, and
|
||||
because `StatusCodes.Good == 0x00000000` an unassigned `StatusCode` **is** `Good` — so every condition
|
||||
reported `Good` unconditionally (a wrong *value*, not a null like #473/#475). A native alarm whose device
|
||||
went offline still read `Good`, so an operator could not tell *"genuinely inactive"* from *"we have lost
|
||||
contact and do not know"*.
|
||||
|
||||
**How it is driven now (native alarms).** An alarm-bearing raw tag materializes a condition with **no
|
||||
sibling value variable**, so the value/quality path (`WriteValue`) never touches it, and a comms-lost
|
||||
driver emits **no alarm transitions** (the feed goes silent). The quality therefore comes from the
|
||||
**driver's connectivity**, out of band from alarm transitions:
|
||||
|
||||
- `DriverInstanceActor` Tells its host `ConnectivityChanged(driverInstanceId, connected)` on every
|
||||
transition into `Connected` (`true`) / `Reconnecting` (`false`).
|
||||
- `DriverHostActor.OnDriverConnectivityChanged` fans that out to **every** native condition the driver
|
||||
owns as an `OpcUaPublishActor.AlarmQualityUpdate` (`Good` on connect, `Bad` on disconnect).
|
||||
- `OtOpcUaNodeManager.WriteAlarmQuality` sets **only** the condition's `Quality` and fires a Part 9
|
||||
event **only on a quality-bucket change** — it never touches Active/Acked/Severity/Retain (an active
|
||||
alarm that loses comms stays active). This is a dedicated path, *not* a full-snapshot re-projection, so
|
||||
it cannot clobber a condition's severity/message and works for a condition that never fired a transition.
|
||||
- A freshly materialized native condition starts `BadWaitingForInitialData` (the "no driver data yet"
|
||||
convention value variables use); the first `Connected` confirms it `Good`.
|
||||
- The connectivity annotation is **ungated by redundancy role** (a Secondary keeps its condition quality
|
||||
warm for failover) and publishes **no `/alerts` row** — driver comms health already has its own status
|
||||
surface (`IDriverHealthPublisher`); a row per condition would be alarm-fatigue.
|
||||
|
||||
**Scripted alarms** are script-computed and always live in this scope, so they materialize `Good` and are
|
||||
not connectivity-driven. Deriving a scripted condition's quality from the worst of its input tags'
|
||||
qualities is a deferred follow-up (Layer 3).
|
||||
|
||||
Wire-level guard: `NativeAlarmEventIdentityFieldDeliveryTests.Condition_event_Quality_tracks_source_connectivity_on_the_wire`
|
||||
subscribes with a `[Quality, Message]` clause (Quality is declared on `ConditionType`) and asserts a
|
||||
healthy source reports `Good`, a comms-lost source reports non-`Good`, and recovery returns to `Good` — on
|
||||
a real client subscription. `NodeManagerAlarmSourceFieldsTests` guards the node itself + the no-clobber /
|
||||
unknown-node-no-op invariants.
|
||||
|
||||
## Galaxy driver path (driver-native)
|
||||
|
||||
Restored in PR B.2 of the epic. `GalaxyDriver` implements
|
||||
|
||||
Reference in New Issue
Block a user