feat(runtime): ScriptedAlarmActor state machine (engine wiring tracked as F9)
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
using Akka.Actor;
|
||||
using Akka.Event;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Runtime.ScriptedAlarms;
|
||||
|
||||
public enum ScriptedAlarmActorState { Inactive, Active, Acknowledged }
|
||||
|
||||
/// <summary>
|
||||
/// State machine wrapping a single scripted alarm. Transitions:
|
||||
/// <c>Inactive → Active → Acknowledged → Inactive</c>.
|
||||
///
|
||||
/// Engine wiring (compile alarm expression via <c>AlarmConditionService</c>, persist state to
|
||||
/// <c>ScriptedAlarmState</c> ConfigDb table on <c>PreRestart</c>, emit history rows to
|
||||
/// <c>HistorianAdapter</c>) is staged for follow-up F9. This skeleton owns the state machine
|
||||
/// so DriverHostActor can spawn it as a child.
|
||||
/// </summary>
|
||||
public sealed class ScriptedAlarmActor : ReceiveActor
|
||||
{
|
||||
public sealed record ConditionMet(string Reason);
|
||||
public sealed record AcknowledgeAlarm(string Actor);
|
||||
public sealed record ConditionCleared;
|
||||
public sealed record StateChanged(string AlarmId, ScriptedAlarmActorState State, DateTime AtUtc);
|
||||
|
||||
private readonly string _alarmId;
|
||||
private readonly ILoggingAdapter _log = Context.GetLogger();
|
||||
private ScriptedAlarmActorState _state = ScriptedAlarmActorState.Inactive;
|
||||
|
||||
public static Props Props(string alarmId) =>
|
||||
Akka.Actor.Props.Create(() => new ScriptedAlarmActor(alarmId));
|
||||
|
||||
public ScriptedAlarmActor(string alarmId)
|
||||
{
|
||||
_alarmId = alarmId;
|
||||
|
||||
Receive<ConditionMet>(msg =>
|
||||
{
|
||||
if (_state != ScriptedAlarmActorState.Inactive) return;
|
||||
Transition(ScriptedAlarmActorState.Active);
|
||||
});
|
||||
Receive<AcknowledgeAlarm>(msg =>
|
||||
{
|
||||
if (_state != ScriptedAlarmActorState.Active) return;
|
||||
Transition(ScriptedAlarmActorState.Acknowledged);
|
||||
});
|
||||
Receive<ConditionCleared>(_ =>
|
||||
{
|
||||
if (_state == ScriptedAlarmActorState.Inactive) return;
|
||||
Transition(ScriptedAlarmActorState.Inactive);
|
||||
});
|
||||
}
|
||||
|
||||
private void Transition(ScriptedAlarmActorState next)
|
||||
{
|
||||
var prev = _state;
|
||||
_state = next;
|
||||
_log.Info("ScriptedAlarm {Id}: {From} → {To}", _alarmId, prev, next);
|
||||
Context.Parent.Tell(new StateChanged(_alarmId, next, DateTime.UtcNow));
|
||||
// F9: emit history row via HistorianAdapter; persist state to ScriptedAlarmState DB.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using Akka.Actor;
|
||||
using Akka.TestKit;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Runtime.ScriptedAlarms;
|
||||
using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.ScriptedAlarms;
|
||||
|
||||
public sealed class ScriptedAlarmActorTests : RuntimeActorTestBase
|
||||
{
|
||||
[Fact]
|
||||
public void Full_state_cycle_publishes_StateChanged_to_parent_at_each_transition()
|
||||
{
|
||||
var parent = CreateTestProbe();
|
||||
// Wrap the alarm actor under our probe as parent so StateChanged lands on the probe.
|
||||
var actor = parent.ChildActorOf(ScriptedAlarmActor.Props("alarm-1"));
|
||||
|
||||
actor.Tell(new ScriptedAlarmActor.ConditionMet("threshold"));
|
||||
var t1 = parent.ExpectMsg<ScriptedAlarmActor.StateChanged>();
|
||||
t1.State.ShouldBe(ScriptedAlarmActorState.Active);
|
||||
|
||||
actor.Tell(new ScriptedAlarmActor.AcknowledgeAlarm("joe"));
|
||||
var t2 = parent.ExpectMsg<ScriptedAlarmActor.StateChanged>();
|
||||
t2.State.ShouldBe(ScriptedAlarmActorState.Acknowledged);
|
||||
|
||||
actor.Tell(new ScriptedAlarmActor.ConditionCleared());
|
||||
var t3 = parent.ExpectMsg<ScriptedAlarmActor.StateChanged>();
|
||||
t3.State.ShouldBe(ScriptedAlarmActorState.Inactive);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Duplicate_ConditionMet_in_Active_is_ignored()
|
||||
{
|
||||
var parent = CreateTestProbe();
|
||||
var actor = parent.ChildActorOf(ScriptedAlarmActor.Props("alarm-1"));
|
||||
|
||||
actor.Tell(new ScriptedAlarmActor.ConditionMet("first"));
|
||||
parent.ExpectMsg<ScriptedAlarmActor.StateChanged>();
|
||||
|
||||
actor.Tell(new ScriptedAlarmActor.ConditionMet("second"));
|
||||
parent.ExpectNoMsg(TimeSpan.FromMilliseconds(200));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user