feat(scripted-alarms): richer AlarmConditionState bridge to the OPC UA node (T15)

This commit is contained in:
Joseph Doherty
2026-06-10 19:41:16 -04:00
parent b31d7cb03f
commit 4eb1d65e2b
16 changed files with 349 additions and 108 deletions
@@ -246,12 +246,11 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
/// <param name="ts">The timestamp of the write.</param>
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime ts)
=> Calls.Enqueue($"WV:{nodeId}");
/// <summary>Records an alarm state write call.</summary>
/// <summary>Records an alarm condition write call.</summary>
/// <param name="alarmNodeId">The alarm node ID.</param>
/// <param name="active">Whether the alarm is active.</param>
/// <param name="acknowledged">Whether the alarm is acknowledged.</param>
/// <param name="state">The full condition state snapshot.</param>
/// <param name="ts">The timestamp of the state change.</param>
public void WriteAlarmState(string alarmNodeId, bool active, bool acknowledged, DateTime ts)
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime ts)
=> Calls.Enqueue($"WA:{alarmNodeId}");
/// <summary>Records a materialise-alarm-condition call.</summary>
/// <param name="alarmNodeId">The alarm node ID (== ScriptedAlarmId).</param>
@@ -18,7 +18,7 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
{
var actor = Sys.ActorOf(OpcUaPublishActor.PropsForTests());
actor.Tell(new OpcUaPublishActor.AttributeValueUpdate("ns=2;s=Tag1", 42.0, OpcUaQuality.Good, DateTime.UtcNow));
actor.Tell(new OpcUaPublishActor.AlarmStateUpdate("ns=2;s=Alarm1", true, false, DateTime.UtcNow));
actor.Tell(new OpcUaPublishActor.AlarmStateUpdate("ns=2;s=Alarm1", Snapshot(active: true), DateTime.UtcNow));
actor.Tell(new OpcUaPublishActor.RebuildAddressSpace(CorrelationId.NewId()));
actor.Tell(new OpcUaPublishActor.ServiceLevelChanged(240));
@@ -53,24 +53,38 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
}, duration: TimeSpan.FromMilliseconds(500));
}
/// <summary>Verifies that AlarmStateUpdate routes to sink WriteAlarmState.</summary>
/// <summary>Verifies that AlarmStateUpdate routes to sink WriteAlarmCondition with the full snapshot.</summary>
[Fact]
public void AlarmStateUpdate_routes_to_sink_WriteAlarmState()
public void AlarmStateUpdate_routes_to_sink_WriteAlarmCondition()
{
var sink = new RecordingSink();
var actor = Sys.ActorOf(OpcUaPublishActor.PropsForTests(sink: sink));
actor.Tell(new OpcUaPublishActor.AlarmStateUpdate("ns=2;s=A1", Active: true, Acknowledged: false, DateTime.UtcNow));
actor.Tell(new OpcUaPublishActor.AlarmStateUpdate(
"ns=2;s=A1", Snapshot(active: true, acknowledged: false, severity: 700), DateTime.UtcNow));
AwaitAssert(() =>
{
sink.Alarms.Count.ShouldBe(1);
sink.Alarms[0].AlarmNodeId.ShouldBe("ns=2;s=A1");
sink.Alarms[0].Active.ShouldBeTrue();
sink.Alarms[0].Acknowledged.ShouldBeFalse();
sink.Alarms[0].State.Active.ShouldBeTrue();
sink.Alarms[0].State.Acknowledged.ShouldBeFalse();
sink.Alarms[0].State.Severity.ShouldBe((ushort)700);
}, duration: TimeSpan.FromMilliseconds(500));
}
/// <summary>Builds a test <see cref="AlarmConditionSnapshot"/> with sensible defaults so each test
/// only specifies the fields it cares about.</summary>
private static AlarmConditionSnapshot Snapshot(
bool active = false,
bool acknowledged = true,
bool confirmed = true,
bool enabled = true,
AlarmShelvingKind shelving = AlarmShelvingKind.Unshelved,
ushort severity = 500,
string message = "test") =>
new(active, acknowledged, confirmed, enabled, shelving, severity, message);
/// <summary>Verifies that RebuildAddressSpace calls sink Rebuild.</summary>
[Fact]
public void RebuildAddressSpace_calls_sink_Rebuild()
@@ -148,16 +162,16 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
{
/// <summary>Gets the queue of recorded value updates.</summary>
public ConcurrentQueue<(string NodeId, object? Value, OpcUaQuality Quality, DateTime Ts)> ValueQueue { get; } = new();
/// <summary>Gets the queue of recorded alarm state updates.</summary>
public ConcurrentQueue<(string AlarmNodeId, bool Active, bool Acknowledged, DateTime Ts)> AlarmQueue { get; } = new();
/// <summary>Gets the queue of recorded alarm condition updates.</summary>
public ConcurrentQueue<(string AlarmNodeId, AlarmConditionSnapshot State, DateTime Ts)> AlarmQueue { get; } = new();
/// <summary>Count of rebuild calls.</summary>
public int RebuildCalls;
/// <summary>Gets the list of recorded value updates.</summary>
public List<(string NodeId, object? Value, OpcUaQuality Quality, DateTime Ts)> Values =>
ValueQueue.ToList();
/// <summary>Gets the list of recorded alarm state updates.</summary>
public List<(string AlarmNodeId, bool Active, bool Acknowledged, DateTime Ts)> Alarms =>
/// <summary>Gets the list of recorded alarm condition updates.</summary>
public List<(string AlarmNodeId, AlarmConditionSnapshot State, DateTime Ts)> Alarms =>
AlarmQueue.ToList();
/// <summary>Records a value update.</summary>
@@ -168,13 +182,12 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime ts) =>
ValueQueue.Enqueue((nodeId, value, quality, ts));
/// <summary>Records an alarm state update.</summary>
/// <summary>Records an alarm condition update.</summary>
/// <param name="alarmNodeId">The OPC UA alarm node identifier.</param>
/// <param name="active">Whether the alarm is active.</param>
/// <param name="acknowledged">Whether the alarm is acknowledged.</param>
/// <param name="state">The full condition state snapshot.</param>
/// <param name="ts">The timestamp of the update.</param>
public void WriteAlarmState(string alarmNodeId, bool active, bool acknowledged, DateTime ts) =>
AlarmQueue.Enqueue((alarmNodeId, active, acknowledged, ts));
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime ts) =>
AlarmQueue.Enqueue((alarmNodeId, state, ts));
/// <summary>Materialises an alarm condition (no-op in test).</summary>
/// <param name="alarmNodeId">The alarm node ID.</param>