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"));
+ }
+}