feat(native-alarms): per-binding IsMesReportable on native alarm source bindings (ScadaBridge#59)

TemplateNativeAlarmSource.IsMesReportable + tri-state InstanceNativeAlarmSourceOverride
.IsMesReportableOverride flatten onto ResolvedNativeAlarmSource; NativeAlarmActor stamps every
mirrored condition and InstanceActor the quiet-binding placeholder, so ScriptAlarm,
Alarms.MesReportableAsync() and the #60 wire fields carry it. Additive to the 900-999 severity band.
Migration AddNativeAlarmSourceMesReportable. Management commands, CLI --mes-reportable, Central UI
(template dialog checkbox, instance override select), Transport DTOs/importer/diff and the revision
hash (true-only, so existing hashes are unchanged) carry it.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
Joseph Doherty
2026-11-09 15:09:54 -05:00
parent 69a2017000
commit f98369a786
38 changed files with 2579 additions and 43 deletions
@@ -51,7 +51,7 @@ public class InstanceActorNativeAlarmTests : TestKit, IDisposable
NullLogger<InstanceActor>.Instance,
dclManager)));
private static FlattenedConfiguration ConfigWithNativeSource(string instanceName) => new()
private static FlattenedConfiguration ConfigWithNativeSource(string instanceName, bool mesReportable = false) => new()
{
InstanceUniqueName = instanceName,
NativeAlarmSources =
@@ -60,7 +60,8 @@ public class InstanceActorNativeAlarmTests : TestKit, IDisposable
{
CanonicalName = "Pressure",
ConnectionName = "Opc",
SourceReference = "ns=2;s=T01"
SourceReference = "ns=2;s=T01",
IsMesReportable = mesReportable
}
]
};
@@ -100,6 +101,22 @@ public class InstanceActorNativeAlarmTests : TestKit, IDisposable
a.SourceReference == "T01.Hi" && a.Kind == AlarmKind.NativeOpcUa && a.Condition.Severity == 800);
}
/// <summary>Gitea #59: the quiet-binding placeholder row carries the binding's MES flag.</summary>
[Theory]
[InlineData(true)]
[InlineData(false)]
public void Snapshot_QuietNativeBinding_PlaceholderCarriesBindingMesFlag(bool flag)
{
var dcl = CreateTestProbe();
var actor = CreateInstanceActorWithDcl("inst", ConfigWithNativeSource("inst", flag), dcl.Ref);
dcl.ExpectMsg<SubscribeAlarmsRequest>();
actor.Tell(new SubscribeDebugViewRequest("inst", "c"));
var snap = ExpectMsg<DebugViewSnapshot>();
Assert.Equal(flag, Assert.Single(snap.AlarmStates, a => a.IsConfiguredPlaceholder).IsMesReportable);
}
/// <summary>Verifies that a quiet native binding emits a placeholder in the snapshot.</summary>
[Fact]
public void Snapshot_QuietNativeBinding_EmitsPlaceholder()
@@ -36,11 +36,12 @@ public class NativeAlarmActorTests : TestKit, IDisposable
_storage.InitializeAsync().GetAwaiter().GetResult();
}
private static ResolvedNativeAlarmSource Source() => new()
private static ResolvedNativeAlarmSource Source(bool mesReportable = false) => new()
{
CanonicalName = "Pressure",
ConnectionName = "Opc",
SourceReference = "ns=2;s=T01"
SourceReference = "ns=2;s=T01",
IsMesReportable = mesReportable
};
private static NativeAlarmTransition Transition(
@@ -48,6 +49,11 @@ public class NativeAlarmActorTests : TestKit, IDisposable
new(sourceRef, "T01", "AnalogLimit.Hi", kind, condition,
"Process", "hi", "hi", "", "", null, time ?? DateTimeOffset.UtcNow, "92", "90");
private IActorRef SpawnMes(IActorRef instanceActor, IActorRef dclManager, bool mesReportable) =>
ActorOf(Props.Create(() => new NativeAlarmActor(
Source(mesReportable), "inst", instanceActor, dclManager, _storage, _options,
NullLogger<NativeAlarmActor>.Instance, AlarmKind.NativeOpcUa, null)));
private IActorRef Spawn(IActorRef instanceActor, IActorRef dclManager, IServiceProvider? serviceProvider = null) =>
Spawn(instanceActor, dclManager, _options, serviceProvider);
@@ -157,6 +163,25 @@ public class NativeAlarmActorTests : TestKit, IDisposable
Assert.Equal("Pressure", emitted.NativeSourceCanonicalName);
}
/// <summary>Gitea #59: a binding flagged MES-reportable stamps every condition it mirrors; an unflagged one does not.</summary>
[Theory]
[InlineData(true)]
[InlineData(false)]
public void Raise_StampsIsMesReportable_FromBindingFlag(bool bindingFlag)
{
var instance = CreateTestProbe();
var dcl = CreateTestProbe();
var actor = SpawnMes(instance.Ref, dcl.Ref, bindingFlag);
dcl.ExpectMsg<SubscribeAlarmsRequest>();
actor.Tell(new NativeAlarmTransitionUpdate("Opc", Transition(
"T01.Hi", AlarmTransitionKind.Raise,
// Severity 100 is OUTSIDE the 900-999 band, so only the binding flag can set it.
new AlarmConditionState(true, false, null, AlarmShelveState.Unshelved, false, 100))));
Assert.Equal(bindingFlag, instance.ExpectMsg<AlarmStateChanged>().IsMesReportable);
}
/// <summary>Verifies that a snapshot completing with a missing prior alarm emits a return to normal.</summary>
[Fact]
public void SnapshotComplete_WithMissingPriorAlarm_EmitsReturnToNormal()