feat(alarms): exact OPC UA severity + per-condition quality on native alarm events (#626)
AlarmEventArgs gains optional OpcUaSeverity (supersedes the four-bucket mapping, clamped 1..1000) and QualityStatusCode (projected onto the condition's Quality). An Unspecified-kind event carrying a quality is a quality-only annotation: the host updates the condition but publishes no alerts row. Both default null, so every existing native source is unchanged. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
+56
@@ -271,6 +271,62 @@ public sealed class DriverHostActorNativeAlarmTests : RuntimeActorTestBase
|
||||
evt.User.ShouldBe("device");
|
||||
}
|
||||
|
||||
/// <summary>Gitea #626 — a QUALITY-ONLY native event (an <see cref="AlarmTransitionKind.Unspecified"/> kind
|
||||
/// carrying <see cref="AlarmEventArgs.QualityStatusCode"/>, e.g. an MTConnect condition gone Unavailable while
|
||||
/// already Normal) re-annotates the condition's Quality through the ordinary state update, and publishes NO
|
||||
/// <c>alerts</c> row — it is not an alarm transition. The positive control is the Raise that follows: it
|
||||
/// MUST publish, and it must be the FIRST alerts message, so the absence is proven rather than timed.</summary>
|
||||
[Fact]
|
||||
public void Quality_only_native_event_updates_the_condition_but_publishes_no_alerts_row()
|
||||
{
|
||||
var db = NewInMemoryDbFactory();
|
||||
var deploymentId = SeedV3AlarmDeployment(db, RevA, Driver: "drv-1", Tag: "temp_hi");
|
||||
|
||||
var alerts = CreateTestProbe();
|
||||
SubscribeToAlerts(alerts);
|
||||
|
||||
var (actor, publish) = SpawnHostAndApply(db, deploymentId);
|
||||
|
||||
actor.Tell(new DriverInstanceActor.AttributeAlarmPublished("drv-1", new AlarmEventArgs(
|
||||
new StubAlarmHandle(),
|
||||
SourceNodeId: "Temp",
|
||||
ConditionId: AlarmRawPath,
|
||||
AlarmType: "SYSTEM",
|
||||
Message: "SYSTEM UNAVAILABLE",
|
||||
Severity: AlarmSeverity.Low,
|
||||
SourceTimestampUtc: Ts,
|
||||
Kind: AlarmTransitionKind.Unspecified,
|
||||
QualityStatusCode: OpcUaStatus.BadNoCommunication)));
|
||||
|
||||
var annotated = publish.ExpectMsg<OpcUaPublishActor.AlarmStateUpdate>(TimeSpan.FromSeconds(5));
|
||||
annotated.AlarmNodeId.ShouldBe(AlarmRawPath);
|
||||
annotated.State.Active.ShouldBeFalse();
|
||||
annotated.State.StatusCode.ShouldBe(OpcUaStatus.BadNoCommunication);
|
||||
|
||||
// Positive control: a real transition after it DOES publish, and is the first thing on the topic.
|
||||
actor.Tell(new DriverInstanceActor.AttributeAlarmPublished("drv-1", new AlarmEventArgs(
|
||||
new StubAlarmHandle(),
|
||||
SourceNodeId: "Temp",
|
||||
ConditionId: AlarmRawPath,
|
||||
AlarmType: "SYSTEM",
|
||||
Message: "spindle fault",
|
||||
Severity: AlarmSeverity.High,
|
||||
SourceTimestampUtc: Ts,
|
||||
Kind: AlarmTransitionKind.Raise,
|
||||
OpcUaSeverity: 800,
|
||||
QualityStatusCode: OpcUaStatus.Good)));
|
||||
|
||||
var raised = publish.ExpectMsg<OpcUaPublishActor.AlarmStateUpdate>(TimeSpan.FromSeconds(5));
|
||||
raised.State.Active.ShouldBeTrue();
|
||||
raised.State.Severity.ShouldBe((ushort)800);
|
||||
raised.State.StatusCode.ShouldBe(OpcUaStatus.Good);
|
||||
|
||||
var evt = alerts.ExpectMsg<AlarmTransitionEvent>(TimeSpan.FromSeconds(5));
|
||||
evt.Message.ShouldBe("spindle fault", "the quality-only annotation must not have published an alerts row");
|
||||
evt.TransitionKind.ShouldBe("Activated");
|
||||
evt.Severity.ShouldBe(800);
|
||||
}
|
||||
|
||||
/// <summary>#477 Layer 2 — a driver <see cref="DriverInstanceActor.ConnectivityChanged"/> annotates the
|
||||
/// Quality of the driver's native conditions via <see cref="OpcUaPublishActor.AlarmQualityUpdate"/>: comms
|
||||
/// lost → Bad, restored → Good, at the raw condition NodeId + realm. No alarm transition is involved — this
|
||||
|
||||
Reference in New Issue
Block a user