feat(alarms): scripted condition Quality from worst-of-input tag quality (#478)
Layer 3 of #477: a scripted alarm's condition Quality now reflects the WORST quality across its input tags, mirroring the native OT semantic (#477 L2). Plumbing (quality was silently discarded twice on the live path): - VirtualTagActor.DependencyValueChanged gains Quality (defaulted Good); the DependencyMuxActor forwards the published AttributeValuePublished.Quality it already carried; ScriptedAlarmHostActor.OnDependencyChanged pushes the real quality into the engine (was hardcoded 0u/Good). Engine (Core.ScriptedAlarms): - ScriptedAlarmEngine computes worst-of-input quality each eval (skipping not-yet-published inputs, which are a readiness concern, not a quality signal) and carries it on ScriptedAlarmEvent.WorstInputStatusCode. - A real transition carries the current worst quality so ToSnapshot's full snapshot doesn't clobber quality back to Good (e.g. transition while Uncertain). - A Bad input freezes the condition (no transition), like a comms-lost native driver; a quality-bucket change with no transition emits the new EmissionKind.QualityChanged, routed to the existing #477-L2 AlarmQualityUpdate -> WriteAlarmQuality node path (quality only, no /alerts row, no historian write). ScriptedAlarmSource skips QualityChanged so it never fabricates a phantom IAlarmSource event. Host: ToSnapshot maps WorstInputStatusCode -> OpcUaQuality; OnEngineEmission routes QualityChanged out of band. Tests (TDD, RED-first): engine worst-carry + Bad/restore QualityChanged + unchanged-bucket-no-emit; source swallows QualityChanged; mux forwards quality; host Bad-dep -> AlarmQualityUpdate(no alerts) + transition snapshot carries worst. Docs: AlarmTracking.md Layer-3 section + design doc. Closes #478
This commit is contained in:
+51
@@ -664,6 +664,57 @@ public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase
|
||||
evtFalse.HistorizeToAveva.ShouldBe(false);
|
||||
}
|
||||
|
||||
/// <summary>#478 — a Bad-quality dependency (no threshold crossing → no state transition) drives the
|
||||
/// scripted condition's Quality out of band: the host publishes an <see cref="OpcUaPublishActor.AlarmQualityUpdate"/>
|
||||
/// (Bad, Uns realm) and NO <c>/alerts</c> transition — quality is an annotation, not a state change.</summary>
|
||||
[Fact]
|
||||
public void Bad_quality_dependency_publishes_AlarmQualityUpdate_and_no_alerts()
|
||||
{
|
||||
var publish = CreateTestProbe();
|
||||
var mux = CreateTestProbe();
|
||||
var alerts = CreateTestProbe();
|
||||
SubscribeToAlerts(alerts);
|
||||
|
||||
var (host, _) = Spawn(publish, mux);
|
||||
host.Tell(new ScriptedAlarmHostActor.ApplyScriptedAlarms(new[] { Plan(id: "alm-1", depRef: "M.T", threshold: 90) }));
|
||||
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout); // load completed
|
||||
|
||||
// Baseline is Good (unread inputs are not a quality signal — no load-time annotation). Drive the
|
||||
// input Bad with a value below threshold: the predicate freezes (no transition), but the worst-input
|
||||
// quality bucket moves Good→Bad → a QualityChanged annotation flows out of band.
|
||||
host.Tell(new VirtualTagActor.DependencyValueChanged("M.T", 10, DateTime.UtcNow, OpcUaQuality.Bad));
|
||||
|
||||
var q = publish.FishForMessage<OpcUaPublishActor.AlarmQualityUpdate>(m => m.Quality == OpcUaQuality.Bad, Timeout);
|
||||
q.AlarmNodeId.ShouldBe("alm-1");
|
||||
q.Realm.ShouldBe(AddressSpaceRealm.Uns);
|
||||
|
||||
// A pure quality change is NOT a state transition: no /alerts row.
|
||||
alerts.ExpectNoMsg(TimeSpan.FromMilliseconds(400));
|
||||
}
|
||||
|
||||
/// <summary>#478 — when a transition fires while an input is Uncertain, the projected full snapshot
|
||||
/// carries that worst-of-input quality (not a clobbered Good), so the condition node reflects that its
|
||||
/// state is derived from untrustworthy inputs.</summary>
|
||||
[Fact]
|
||||
public void Transition_snapshot_carries_worst_input_quality()
|
||||
{
|
||||
var publish = CreateTestProbe();
|
||||
var mux = CreateTestProbe();
|
||||
var alerts = CreateTestProbe();
|
||||
SubscribeToAlerts(alerts);
|
||||
|
||||
var (host, _) = Spawn(publish, mux);
|
||||
host.Tell(new ScriptedAlarmHostActor.ApplyScriptedAlarms(new[] { Plan(id: "alm-1", depRef: "M.T", threshold: 90) }));
|
||||
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout); // load completed
|
||||
|
||||
// Above threshold (activates) but Uncertain quality — Uncertain is "ready", so the predicate runs.
|
||||
host.Tell(new VirtualTagActor.DependencyValueChanged("M.T", 99, DateTime.UtcNow, OpcUaQuality.Uncertain));
|
||||
|
||||
var state = publish.FishForMessage<OpcUaPublishActor.AlarmStateUpdate>(m => m.State.Active, Timeout);
|
||||
state.AlarmNodeId.ShouldBe("alm-1");
|
||||
state.State.Quality.ShouldBe(OpcUaQuality.Uncertain);
|
||||
}
|
||||
|
||||
/// <summary>OneShotShelve transition carries the operator's identity: an operator-driven OneShotShelve
|
||||
/// drives <c>OneShotShelveAsync</c> — the resulting <see cref="AlarmTransitionEvent"/>(<c>"Shelved"</c>)
|
||||
/// on the alerts topic must carry <c>User == cmd.User</c> (the acting operator), NOT the generic
|
||||
|
||||
@@ -149,6 +149,21 @@ public sealed class DependencyMuxActorTests : RuntimeActorTestBase
|
||||
subscriber.ExpectMsg<VirtualTagActor.DependencyValueChanged>().TagId.ShouldBe("ref-1");
|
||||
}
|
||||
|
||||
/// <summary>#478 — the mux forwards the published source quality onto DependencyValueChanged so the
|
||||
/// scripted-alarm host can derive a condition's worst-of-input quality.</summary>
|
||||
[Fact]
|
||||
public void Publish_quality_is_forwarded_on_DependencyValueChanged()
|
||||
{
|
||||
var mux = Sys.ActorOf(DependencyMuxActor.Props());
|
||||
var sub = CreateTestProbe();
|
||||
mux.Tell(new DependencyMuxActor.RegisterInterest(new[] { "tag-1" }, sub.Ref));
|
||||
|
||||
mux.Tell(new DriverInstanceActor.AttributeValuePublished(
|
||||
"driver-1", "tag-1", 10, OpcUaQuality.Bad, DateTime.UtcNow));
|
||||
|
||||
sub.ExpectMsg<VirtualTagActor.DependencyValueChanged>().Quality.ShouldBe(OpcUaQuality.Bad);
|
||||
}
|
||||
|
||||
private sealed class EchoSumEvaluator : ZB.MOM.WW.OtOpcUa.Commons.Engines.IVirtualTagEvaluator
|
||||
{
|
||||
/// <summary>Evaluates the expression by summing all dependency values as integers.</summary>
|
||||
|
||||
Reference in New Issue
Block a user