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:
@@ -109,6 +109,9 @@ public sealed class DriverInstanceActorTests : RuntimeActorTestBase
|
||||
{
|
||||
var driver = new SubscribableStubDriver();
|
||||
var parent = CreateTestProbe();
|
||||
// #477: DriverInstanceActor now Tells the parent ConnectivityChanged on each connect/
|
||||
// disconnect; these tests assert on the data/alarm/discovery forwards, so ignore it.
|
||||
parent.IgnoreMessages(m => m is DriverInstanceActor.ConnectivityChanged);
|
||||
var actor = parent.ChildActorOf(DriverInstanceActor.Props(driver));
|
||||
|
||||
actor.Tell(new DriverInstanceActor.InitializeRequested("{}"));
|
||||
@@ -134,6 +137,9 @@ public sealed class DriverInstanceActorTests : RuntimeActorTestBase
|
||||
{
|
||||
var driver = new SubscribableStubDriver();
|
||||
var parent = CreateTestProbe();
|
||||
// #477: DriverInstanceActor now Tells the parent ConnectivityChanged on each connect/
|
||||
// disconnect; these tests assert on the data/alarm/discovery forwards, so ignore it.
|
||||
parent.IgnoreMessages(m => m is DriverInstanceActor.ConnectivityChanged);
|
||||
var actor = parent.ChildActorOf(DriverInstanceActor.Props(driver));
|
||||
|
||||
actor.Tell(new DriverInstanceActor.InitializeRequested("{}"));
|
||||
@@ -162,6 +168,9 @@ public sealed class DriverInstanceActorTests : RuntimeActorTestBase
|
||||
{
|
||||
var driver = new SubscribableStubDriver();
|
||||
var parent = CreateTestProbe();
|
||||
// #477: DriverInstanceActor now Tells the parent ConnectivityChanged on each connect/
|
||||
// disconnect; these tests assert on the data/alarm/discovery forwards, so ignore it.
|
||||
parent.IgnoreMessages(m => m is DriverInstanceActor.ConnectivityChanged);
|
||||
var actor = parent.ChildActorOf(DriverInstanceActor.Props(driver, reconnectInterval: TimeSpan.FromMilliseconds(50)));
|
||||
|
||||
// Desired set arrives BEFORE connect — retained, not yet applied.
|
||||
@@ -197,6 +206,9 @@ public sealed class DriverInstanceActorTests : RuntimeActorTestBase
|
||||
// is used — the exact condition that throws NotSupportedException on the subsequent Sender read.
|
||||
var driver = new SubscribableStubDriver { UnsubscribeYields = true };
|
||||
var parent = CreateTestProbe();
|
||||
// #477: DriverInstanceActor now Tells the parent ConnectivityChanged on each connect/
|
||||
// disconnect; these tests assert on the data/alarm/discovery forwards, so ignore it.
|
||||
parent.IgnoreMessages(m => m is DriverInstanceActor.ConnectivityChanged);
|
||||
var actor = parent.ChildActorOf(DriverInstanceActor.Props(driver));
|
||||
|
||||
actor.Tell(new DriverInstanceActor.InitializeRequested("{}"));
|
||||
@@ -242,6 +254,9 @@ public sealed class DriverInstanceActorTests : RuntimeActorTestBase
|
||||
{
|
||||
var driver = new SubscribableStubDriver();
|
||||
var parent = CreateTestProbe();
|
||||
// #477: DriverInstanceActor now Tells the parent ConnectivityChanged on each connect/
|
||||
// disconnect; these tests assert on the data/alarm/discovery forwards, so ignore it.
|
||||
parent.IgnoreMessages(m => m is DriverInstanceActor.ConnectivityChanged);
|
||||
var actor = parent.ChildActorOf(DriverInstanceActor.Props(driver, reconnectInterval: TimeSpan.FromSeconds(30)));
|
||||
|
||||
actor.Tell(new DriverInstanceActor.InitializeRequested("{}"));
|
||||
@@ -271,6 +286,9 @@ public sealed class DriverInstanceActorTests : RuntimeActorTestBase
|
||||
InitBehavior = cfg => cfg == good ? Task.CompletedTask : throw new InvalidOperationException("bad-cfg"),
|
||||
};
|
||||
var parent = CreateTestProbe();
|
||||
// #477: DriverInstanceActor now Tells the parent ConnectivityChanged on each connect/
|
||||
// disconnect; these tests assert on the data/alarm/discovery forwards, so ignore it.
|
||||
parent.IgnoreMessages(m => m is DriverInstanceActor.ConnectivityChanged);
|
||||
var actor = parent.ChildActorOf(DriverInstanceActor.Props(driver, reconnectInterval: TimeSpan.FromMilliseconds(50)));
|
||||
|
||||
actor.Tell(new DriverInstanceActor.SetDesiredSubscriptions(new[] { "tag-a" }, TimeSpan.FromMilliseconds(100)));
|
||||
@@ -304,6 +322,9 @@ public sealed class DriverInstanceActorTests : RuntimeActorTestBase
|
||||
InitBehavior = cfg => cfg == v1 ? gate1.Task : gate2.Task,
|
||||
};
|
||||
var parent = CreateTestProbe();
|
||||
// #477: DriverInstanceActor now Tells the parent ConnectivityChanged on each connect/
|
||||
// disconnect; these tests assert on the data/alarm/discovery forwards, so ignore it.
|
||||
parent.IgnoreMessages(m => m is DriverInstanceActor.ConnectivityChanged);
|
||||
var actor = parent.ChildActorOf(DriverInstanceActor.Props(driver, reconnectInterval: TimeSpan.FromSeconds(30)));
|
||||
actor.Tell(new DriverInstanceActor.SetDesiredSubscriptions(new[] { "tag-a" }, TimeSpan.FromMilliseconds(100)));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user