diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Streaming/AlarmStateChanged.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Streaming/AlarmStateChanged.cs index 409bf90d..7aa3fb61 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Streaming/AlarmStateChanged.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Streaming/AlarmStateChanged.cs @@ -1,3 +1,4 @@ +using ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms; using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums; namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming; @@ -26,4 +27,48 @@ public record AlarmStateChanged( /// surface this to operators. /// public string Message { get; init; } = string.Empty; + + /// + /// Whether this alarm is computed at the site or mirrored from a native + /// source. Defaults to . + /// + public AlarmKind Kind { get; init; } = AlarmKind.Computed; + + private AlarmConditionState? _condition; + + /// + /// Unified A&C-style condition (active/acked/shelved/suppressed + severity). + /// When not explicitly set, defaults to a computed mapping of + /// + so existing callers and + /// computed alarms carry a correct condition without extra work. + /// + public AlarmConditionState Condition + { + get => _condition ?? AlarmConditionStateFactory.ForComputed(State, Priority); + init => _condition = value; + } + + /// Native per-condition key (e.g. "Tank01.Level.HiHi"); empty for computed alarms. + public string SourceReference { get; init; } = string.Empty; + + /// Native alarm type name (e.g. "AnalogLimitAlarm.HiHi"); empty for computed alarms. + public string AlarmTypeName { get; init; } = string.Empty; + + /// Native alarm category/taxonomy; empty for computed alarms. + public string Category { get; init; } = string.Empty; + + /// Operator who acknowledged at the source (display-only); empty otherwise. + public string OperatorUser { get; init; } = string.Empty; + + /// Operator comment captured at the source (display-only); empty otherwise. + public string OperatorComment { get; init; } = string.Empty; + + /// When the native condition originally became active, if known. + public DateTimeOffset? OriginalRaiseTime { get; init; } + + /// Current source value (display-only); empty for computed alarms. + public string CurrentValue { get; init; } = string.Empty; + + /// Limit/threshold value for native limit alarms (display-only); empty otherwise. + public string LimitValue { get; init; } = string.Empty; } diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Alarms/AlarmConditionStateFactory.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Alarms/AlarmConditionStateFactory.cs new file mode 100644 index 00000000..1e0f36cb --- /dev/null +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Alarms/AlarmConditionStateFactory.cs @@ -0,0 +1,16 @@ +using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums; + +namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms; + +/// Builds values for the supported alarm kinds. +public static class AlarmConditionStateFactory +{ + /// + /// Computed alarms have no native ack/shelve/suppress lifecycle: they are + /// auto-acked, never shelved or suppressed, not confirmable, and their + /// severity is the configured priority. Active mirrors the alarm State. + /// + public static AlarmConditionState ForComputed(AlarmState state, int priority) => + new(Active: state == AlarmState.Active, Acknowledged: true, Confirmed: null, + Shelve: AlarmShelveState.Unshelved, Suppressed: false, Severity: priority); +} diff --git a/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Messages/AlarmStateChangedEnrichmentTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Messages/AlarmStateChangedEnrichmentTests.cs new file mode 100644 index 00000000..bb9ccf0c --- /dev/null +++ b/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Messages/AlarmStateChangedEnrichmentTests.cs @@ -0,0 +1,28 @@ +using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming; +using ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms; +using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums; + +namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Messages; + +public class AlarmStateChangedEnrichmentTests +{ + [Fact] + public void Defaults_AreComputedKind_WithAutoAck() + { + var m = new AlarmStateChanged("inst", "HiAlarm", AlarmState.Active, 700, DateTimeOffset.UnixEpoch); + Assert.Equal(AlarmKind.Computed, m.Kind); + Assert.True(m.Condition.Acknowledged); // computed = auto-acked + Assert.Equal(700, m.Condition.Severity); // severity defaults to Priority + Assert.True(m.Condition.Active); // derived from State + Assert.Equal("", m.SourceReference); + } + + [Fact] + public void Factory_ForComputed_MapsPriorityAndState() + { + var c = AlarmConditionStateFactory.ForComputed(AlarmState.Normal, priority: 250); + Assert.False(c.Active); + Assert.True(c.Acknowledged); + Assert.Equal(250, c.Severity); + } +}