diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/StreamRelayActor.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/StreamRelayActor.cs index b4d183dd..ecb11452 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/StreamRelayActor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/StreamRelayActor.cs @@ -67,7 +67,24 @@ public class StreamRelayActor : ReceiveActor Priority = msg.Priority, Timestamp = Timestamp.FromDateTimeOffset(msg.Timestamp), Level = MapAlarmLevel(msg.Level), - Message = msg.Message ?? string.Empty + Message = msg.Message ?? string.Empty, + // Native alarm enrichment (additive — computed alarms map their default condition). + Kind = msg.Kind.ToString(), + Active = msg.Condition.Active, + Acknowledged = msg.Condition.Acknowledged, + Confirmed = msg.Condition.Confirmed ?? false, + ShelveState = AlarmShelveStateCodec.ToWire(msg.Condition.Shelve), + Suppressed = msg.Condition.Suppressed, + SourceReference = msg.SourceReference ?? string.Empty, + AlarmTypeName = msg.AlarmTypeName ?? string.Empty, + Category = msg.Category ?? string.Empty, + OperatorUser = msg.OperatorUser ?? string.Empty, + OperatorComment = msg.OperatorComment ?? string.Empty, + OriginalRaiseTime = msg.OriginalRaiseTime.HasValue + ? Timestamp.FromDateTimeOffset(msg.OriginalRaiseTime.Value) + : null, + CurrentValue = msg.CurrentValue ?? string.Empty, + LimitValue = msg.LimitValue ?? string.Empty } }; diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/AlarmShelveStateCodec.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/AlarmShelveStateCodec.cs new file mode 100644 index 00000000..a1512e90 --- /dev/null +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/AlarmShelveStateCodec.cs @@ -0,0 +1,21 @@ +using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums; + +namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc; + +/// +/// Maps to/from the wire string carried in the +/// AlarmStateUpdate.shelve_state proto field. The wire form is the enum +/// member name; an empty or unrecognized string parses back to +/// (the safe default). +/// +public static class AlarmShelveStateCodec +{ + /// Returns the wire string for a shelve state (the enum member name). + public static string ToWire(AlarmShelveState state) => state.ToString(); + + /// Parses a wire string back to a shelve state; defaults to . + public static AlarmShelveState Parse(string? wire) => + Enum.TryParse(wire, ignoreCase: true, out var state) + ? state + : AlarmShelveState.Unshelved; +} diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteStreamGrpcClient.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteStreamGrpcClient.cs index 118dac99..da1665e4 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteStreamGrpcClient.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteStreamGrpcClient.cs @@ -3,6 +3,7 @@ using Grpc.Core; using Grpc.Net.Client; using Microsoft.Extensions.Logging; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming; +using ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms; using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums; using Google.Protobuf.WellKnownTypes; @@ -233,11 +234,32 @@ public class SiteStreamGrpcClient : IAsyncDisposable, IDisposable evt.AlarmChanged.Timestamp.ToDateTimeOffset()) { Level = MapAlarmLevel(evt.AlarmChanged.Level), - Message = evt.AlarmChanged.Message ?? string.Empty + Message = evt.AlarmChanged.Message ?? string.Empty, + // Native alarm enrichment (additive — computed alarms carry their default condition). + Kind = ParseAlarmKind(evt.AlarmChanged.Kind), + Condition = new AlarmConditionState( + Active: evt.AlarmChanged.Active, + Acknowledged: evt.AlarmChanged.Acknowledged, + Confirmed: evt.AlarmChanged.Confirmed, + Shelve: AlarmShelveStateCodec.Parse(evt.AlarmChanged.ShelveState), + Suppressed: evt.AlarmChanged.Suppressed, + Severity: evt.AlarmChanged.Priority), + SourceReference = evt.AlarmChanged.SourceReference ?? string.Empty, + AlarmTypeName = evt.AlarmChanged.AlarmTypeName ?? string.Empty, + Category = evt.AlarmChanged.Category ?? string.Empty, + OperatorUser = evt.AlarmChanged.OperatorUser ?? string.Empty, + OperatorComment = evt.AlarmChanged.OperatorComment ?? string.Empty, + OriginalRaiseTime = evt.AlarmChanged.OriginalRaiseTime?.ToDateTimeOffset(), + CurrentValue = evt.AlarmChanged.CurrentValue ?? string.Empty, + LimitValue = evt.AlarmChanged.LimitValue ?? string.Empty }, _ => null }; + /// Parses the wire "kind" string back to ; defaults to Computed. + internal static AlarmKind ParseAlarmKind(string? kind) => + System.Enum.TryParse(kind, ignoreCase: true, out var k) ? k : AlarmKind.Computed; + /// /// Maps proto Quality enum to domain string. Internal for testability. /// diff --git a/tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/Grpc/SiteStreamGrpcClientTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/Grpc/SiteStreamGrpcClientTests.cs index d7ae9b6f..82d4addb 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/Grpc/SiteStreamGrpcClientTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/Grpc/SiteStreamGrpcClientTests.cs @@ -40,16 +40,31 @@ public class SiteStreamGrpcClientTests public void ConvertToDomainEvent_AlarmChanged_MapsCorrectly() { var ts = DateTimeOffset.UtcNow; + var raiseTime = ts.AddMinutes(-30); var evt = new SiteStreamEvent { CorrelationId = "corr-2", AlarmChanged = new AlarmStateUpdate { InstanceUniqueName = "Site1.Motor01", - AlarmName = "OverTemp", + AlarmName = "T01.Hi", State = AlarmStateEnum.AlarmStateActive, - Priority = 3, - Timestamp = Timestamp.FromDateTimeOffset(ts) + Priority = 850, + Timestamp = Timestamp.FromDateTimeOffset(ts), + Kind = "NativeOpcUa", + Active = true, + Acknowledged = false, + Confirmed = false, + ShelveState = "TimedShelved", + Suppressed = true, + SourceReference = "T01.Hi", + AlarmTypeName = "AnalogLimit.Hi", + Category = "Process", + OperatorUser = "op2", + OperatorComment = "shelved", + OriginalRaiseTime = Timestamp.FromDateTimeOffset(raiseTime), + CurrentValue = "120", + LimitValue = "100" } }; @@ -57,10 +72,26 @@ public class SiteStreamGrpcClientTests var alarm = Assert.IsType(result); Assert.Equal("Site1.Motor01", alarm.InstanceUniqueName); - Assert.Equal("OverTemp", alarm.AlarmName); + Assert.Equal("T01.Hi", alarm.AlarmName); Assert.Equal(AlarmState.Active, alarm.State); - Assert.Equal(3, alarm.Priority); + Assert.Equal(850, alarm.Priority); Assert.Equal(ts, alarm.Timestamp); + + // Native enrichment mapped back. + Assert.Equal(AlarmKind.NativeOpcUa, alarm.Kind); + Assert.True(alarm.Condition.Active); + Assert.False(alarm.Condition.Acknowledged); + Assert.Equal(AlarmShelveState.TimedShelved, alarm.Condition.Shelve); + Assert.True(alarm.Condition.Suppressed); + Assert.Equal(850, alarm.Condition.Severity); + Assert.Equal("T01.Hi", alarm.SourceReference); + Assert.Equal("AnalogLimit.Hi", alarm.AlarmTypeName); + Assert.Equal("Process", alarm.Category); + Assert.Equal("op2", alarm.OperatorUser); + Assert.Equal("shelved", alarm.OperatorComment); + Assert.Equal(raiseTime, alarm.OriginalRaiseTime); + Assert.Equal("120", alarm.CurrentValue); + Assert.Equal("100", alarm.LimitValue); } [Fact] diff --git a/tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/Grpc/StreamRelayActorTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/Grpc/StreamRelayActorTests.cs index 3882684f..416a475b 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/Grpc/StreamRelayActorTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/Grpc/StreamRelayActorTests.cs @@ -3,6 +3,7 @@ using Akka.Actor; using Akka.TestKit.Xunit2; using Google.Protobuf.WellKnownTypes; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming; +using ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms; using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums; using ZB.MOM.WW.ScadaBridge.Communication.Actors; using ZB.MOM.WW.ScadaBridge.Communication.Grpc; @@ -57,8 +58,23 @@ public class StreamRelayActorTests : TestKit new StreamRelayActor(correlationId, channel.Writer))); var timestamp = new DateTimeOffset(2026, 3, 21, 11, 0, 0, TimeSpan.Zero); + var raiseTime = new DateTimeOffset(2026, 3, 21, 10, 0, 0, TimeSpan.Zero); var domainEvent = new AlarmStateChanged( - "Site1.Pump01", "HighPressure", AlarmState.Active, 2, timestamp); + "Site1.Pump01", "T01.Hi", AlarmState.Active, 900, timestamp) + { + Kind = AlarmKind.NativeMxAccess, + SourceReference = "T01.Hi", + AlarmTypeName = "AnalogLimit.Hi", + Category = "Process", + OperatorUser = "op1", + OperatorComment = "ack", + OriginalRaiseTime = raiseTime, + CurrentValue = "92", + LimitValue = "90", + Condition = new AlarmConditionState( + Active: true, Acknowledged: true, Confirmed: null, + Shelve: AlarmShelveState.OneShotShelved, Suppressed: false, Severity: 900) + }; actor.Tell(domainEvent); @@ -76,10 +92,25 @@ public class StreamRelayActorTests : TestKit var alarm = protoEvent.AlarmChanged; Assert.Equal("Site1.Pump01", alarm.InstanceUniqueName); - Assert.Equal("HighPressure", alarm.AlarmName); + Assert.Equal("T01.Hi", alarm.AlarmName); Assert.Equal(AlarmStateEnum.AlarmStateActive, alarm.State); - Assert.Equal(2, alarm.Priority); + Assert.Equal(900, alarm.Priority); Assert.Equal(Timestamp.FromDateTimeOffset(timestamp), alarm.Timestamp); + + // Native enrichment mapped out. + Assert.Equal("NativeMxAccess", alarm.Kind); + Assert.True(alarm.Active); + Assert.True(alarm.Acknowledged); + Assert.Equal("OneShotShelved", alarm.ShelveState); + Assert.False(alarm.Suppressed); + Assert.Equal("T01.Hi", alarm.SourceReference); + Assert.Equal("AnalogLimit.Hi", alarm.AlarmTypeName); + Assert.Equal("Process", alarm.Category); + Assert.Equal("op1", alarm.OperatorUser); + Assert.Equal("ack", alarm.OperatorComment); + Assert.Equal(Timestamp.FromDateTimeOffset(raiseTime), alarm.OriginalRaiseTime); + Assert.Equal("92", alarm.CurrentValue); + Assert.Equal("90", alarm.LimitValue); } [Fact]