feat(alarms): condition Quality tracks source connectivity (#477) #479

Merged
dohertj2 merged 1 commits from fix/alarm-condition-quality-477 into master 2026-07-17 15:18:37 -04:00
Owner

Closes #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. This is a wrong-value bug, distinct from the null-value class of #473/#475. Clients (and HMIs bucketing on IsGood) could not tell "genuinely inactive" from "we have lost contact".

Scope (user-chosen): Layer 1 + Layer 2, Bad-direct, annotate-only. Layer 3 (scripted worst-of-input) deferred → #478.

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 (AddressSpaceApplier: tag.Alarm is not null ⇒ condition, else value), so quality can't come from either existing channel.

  • DriverInstanceActor Tells its parent ConnectivityChanged(id, connected) on entering Connected / Reconnecting.
  • DriverHostActor.OnDriverConnectivityChanged 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). It is a dedicated path (not a full-snapshot re-projection), so it can't clobber severity/message and works for a condition that never fired a transition. Forwarded through DeferredAddressSpaceSink (the F10b inertness trap) — auto-verified by DeferredSinkForwardingReflectionTests. Ungated by redundancy role; publishes no /alerts row (driver health has its own surface).

Scripted conditions stay Good (hardcoded in ScriptedAlarmHostActor.ToSnapshot with a comment pointing at #478).

Tests

  • Node-level (NodeManagerAlarmSourceFieldsTests): materialise / project / no-clobber / unknown-node no-op.
  • NativeAlarmProjectorTests, DriverInstanceActor connectivity emission, DriverHostActor fan-out, OpcUaPublishActor routing.
  • Wire-level (NativeAlarmEventIdentityFieldDeliveryTests.Condition_event_Quality_tracks_source_connectivity_on_the_wire): healthy → Good, comms-lost → non-Good, recovery → Good on a real client subscription. RED-verified against a simulated pre-fix always-Good server.
  • Existing DriverInstanceActor*Tests parent probes now IgnoreMessages the new ConnectivityChanged (they assert data/alarm/discovery forwards, not connectivity).

Regression: Runtime.Tests 383/0, OpcUaServer.Tests 386/0, Commons.Tests 306/0; wire tests 3/3; solution builds clean.

Docs

  • docs/AlarmTracking.md §"Condition source-data Quality (#477)".
  • Design doc docs/plans/2026-07-17-alarm-condition-quality-477-design.md.
Closes #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. This is a wrong-**value** bug, distinct from the null-value class of #473/#475. Clients (and HMIs bucketing on `IsGood`) could not tell *"genuinely inactive"* from *"we have lost contact"*. Scope (user-chosen): **Layer 1 + Layer 2, Bad-direct, annotate-only.** Layer 3 (scripted worst-of-input) deferred → #478. ## 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 (`AddressSpaceApplier`: `tag.Alarm is not null` ⇒ condition, else value), so quality can't come from either existing channel. - `DriverInstanceActor` Tells its parent `ConnectivityChanged(id, connected)` on entering `Connected` / `Reconnecting`. - `DriverHostActor.OnDriverConnectivityChanged` 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). It is a dedicated path (not a full-snapshot re-projection), so it can't clobber severity/message and works for a condition that never fired a transition. Forwarded through `DeferredAddressSpaceSink` (the F10b inertness trap) — auto-verified by `DeferredSinkForwardingReflectionTests`. Ungated by redundancy role; publishes no `/alerts` row (driver health has its own surface). Scripted conditions stay `Good` (hardcoded in `ScriptedAlarmHostActor.ToSnapshot` with a comment pointing at #478). ## Tests - Node-level (`NodeManagerAlarmSourceFieldsTests`): materialise / project / no-clobber / unknown-node no-op. - `NativeAlarmProjectorTests`, `DriverInstanceActor` connectivity emission, `DriverHostActor` fan-out, `OpcUaPublishActor` routing. - **Wire-level** (`NativeAlarmEventIdentityFieldDeliveryTests.Condition_event_Quality_tracks_source_connectivity_on_the_wire`): healthy → Good, comms-lost → non-Good, recovery → Good on a real client subscription. **RED-verified** against a simulated pre-fix always-Good server. - Existing `DriverInstanceActor*Tests` parent probes now `IgnoreMessages` the new `ConnectivityChanged` (they assert data/alarm/discovery forwards, not connectivity). Regression: Runtime.Tests 383/0, OpcUaServer.Tests 386/0, Commons.Tests 306/0; wire tests 3/3; solution builds clean. ## Docs - `docs/AlarmTracking.md` §"Condition source-data Quality (#477)". - Design doc `docs/plans/2026-07-17-alarm-condition-quality-477-design.md`.
dohertj2 added 1 commit 2026-07-17 15:18:26 -04:00
feat(alarms): condition Quality tracks source connectivity (#477)
v2-ci / build (pull_request) Successful in 4m49s
v2-ci / unit-tests (pull_request) Failing after 3h11m25s
db751d12a5
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.
dohertj2 merged commit 6dda0549e2 into master 2026-07-17 15:18:37 -04:00
dohertj2 deleted branch fix/alarm-condition-quality-477 2026-07-17 15:18:37 -04:00
Sign in to join this conversation.