diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/Mapping/AlarmEventMapper.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/Mapping/AlarmEventMapper.cs new file mode 100644 index 00000000..c49f7c7c --- /dev/null +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/Mapping/AlarmEventMapper.cs @@ -0,0 +1,42 @@ +using Google.Protobuf.WellKnownTypes; +using ZB.MOM.WW.HistorianGateway.Contracts.Grpc; +using ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian; + +namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Mapping; + +/// +/// Maps a driver-agnostic onto a gateway wire +/// for the SendEvent write path. +/// +internal static class AlarmEventMapper +{ + /// Maps an alarm historian event to a gateway event. + /// The driver-agnostic alarm event. + /// The gateway wire event ready for SendEvent. + public static HistorianEvent ToHistorianEvent(AlarmHistorianEvent alarm) + { + // Timestamp.FromDateTime requires a Utc-kind DateTime; coerce defensively (TimestampUtc is + // already Utc by contract, but a caller could pass Unspecified). + var eventTime = Timestamp.FromDateTime(DateTime.SpecifyKind(alarm.TimestampUtc, DateTimeKind.Utc)); + + var historianEvent = new HistorianEvent + { + Id = string.IsNullOrWhiteSpace(alarm.AlarmId) ? Guid.NewGuid().ToString("N") : alarm.AlarmId, + SourceName = alarm.EquipmentPath, + Type = alarm.AlarmTypeName, + EventTime = eventTime, + ReceivedTime = eventTime, // the server re-stamps the received time on the SQL path + }; + + // Proto map values must be non-null — only insert non-null properties. + historianEvent.Properties["AlarmName"] = alarm.AlarmName; + historianEvent.Properties["EventKind"] = alarm.EventKind; + historianEvent.Properties["Severity"] = alarm.Severity.ToString(); + historianEvent.Properties["User"] = alarm.User; + historianEvent.Properties["Message"] = alarm.Message; + if (alarm.Comment is not null) + historianEvent.Properties["Comment"] = alarm.Comment; + + return historianEvent; + } +} diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Mapping/AlarmEventMapperTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Mapping/AlarmEventMapperTests.cs new file mode 100644 index 00000000..2ac0c003 --- /dev/null +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Mapping/AlarmEventMapperTests.cs @@ -0,0 +1,35 @@ +using Xunit; +using ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian; // AlarmHistorianEvent +using ZB.MOM.WW.OtOpcUa.Core.Abstractions; // AlarmSeverity +using ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Mapping; + +namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests.Mapping; + +public sealed class AlarmEventMapperTests +{ + [Fact] + public void Maps_source_time_type_and_rich_properties() + { + var a = new AlarmHistorianEvent("A1", "Area/Line/Pump1", "HiHi", "LimitAlarm", + AlarmSeverity.High, "Activated", "Temp high", "operator1", "ack note", + new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc)); + var e = AlarmEventMapper.ToHistorianEvent(a); + Assert.Equal("Area/Line/Pump1", e.SourceName); + Assert.Equal("LimitAlarm", e.Type); + Assert.Equal(new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc), e.EventTime.ToDateTime()); + Assert.Equal("HiHi", e.Properties["AlarmName"]); + Assert.Equal("Activated", e.Properties["EventKind"]); + Assert.Equal("High", e.Properties["Severity"]); + Assert.Equal("operator1", e.Properties["User"]); + Assert.Equal("ack note", e.Properties["Comment"]); + Assert.Equal("Temp high", e.Properties["Message"]); + } + + [Fact] + public void Null_comment_is_omitted_not_null() + { + var a = new AlarmHistorianEvent("A", "S", "N", "DiscreteAlarm", AlarmSeverity.Low, "Cleared", "m", "system", null, + new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc)); + Assert.False(AlarmEventMapper.ToHistorianEvent(a).Properties.ContainsKey("Comment")); + } +}