diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/Mapping/EventMapper.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/Mapping/EventMapper.cs new file mode 100644 index 00000000..6aaadca8 --- /dev/null +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/Mapping/EventMapper.cs @@ -0,0 +1,67 @@ +using System.Globalization; +using ZB.MOM.WW.HistorianGateway.Contracts.Grpc; +using ZB.MOM.WW.OtOpcUa.Core.Abstractions; + +namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Mapping; + +/// +/// Maps a gateway wire event () onto the driver-agnostic +/// consumed by the Server's HistoryReadEvents path. +/// +internal static class EventMapper +{ + /// OPC UA severity range (Part 9): 1 (lowest) … 1000 (highest). + private const ushort MinSeverity = 1; + private const ushort MaxSeverity = 1000; + + /// Maps a single gateway event to a historical event. + /// The gateway wire event. + /// The driver-agnostic historical event. + public static HistoricalEvent ToHistoricalEvent(HistorianEvent historianEvent) + { + // Message: prefer the "Message" property, else fall back to the event Type (best-effort + // render); never null-crash on a missing property. + string? message; + if (historianEvent.Properties.TryGetValue("Message", out var m) && !string.IsNullOrEmpty(m)) + message = m; + else + message = string.IsNullOrEmpty(historianEvent.Type) ? null : historianEvent.Type; + + return new HistoricalEvent( + EventId: historianEvent.Id, + SourceName: string.IsNullOrEmpty(historianEvent.SourceName) ? null : historianEvent.SourceName, + EventTimeUtc: historianEvent.EventTime?.ToDateTime() ?? default, // Utc kind + ReceivedTimeUtc: historianEvent.ReceivedTime?.ToDateTime() ?? default, // Utc kind + Message: message, + Severity: ParseSeverity(historianEvent.Properties)); + } + + /// Maps a batch of gateway events to historical events, in order. + /// The gateway wire events. + /// The driver-agnostic historical events. + public static IReadOnlyList ToHistoricalEvents(IEnumerable events) + { + var result = new List(); + foreach (var historianEvent in events) + result.Add(ToHistoricalEvent(historianEvent)); + return result; + } + + /// + /// Parses an OPC UA severity from the "Severity" property (else "Priority"), clamped to + /// [1, 1000]. Missing or unparseable values default to the minimum severity (1). + /// + private static ushort ParseSeverity(IDictionary properties) + { + string? raw = null; + if (properties.TryGetValue("Severity", out var severity)) + raw = severity; + else if (properties.TryGetValue("Priority", out var priority)) + raw = priority; + + if (int.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) + return (ushort)Math.Clamp(value, MinSeverity, MaxSeverity); + + return MinSeverity; + } +} diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Mapping/EventMapperTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Mapping/EventMapperTests.cs new file mode 100644 index 00000000..de0f2cdd --- /dev/null +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Mapping/EventMapperTests.cs @@ -0,0 +1,40 @@ +using Google.Protobuf.WellKnownTypes; +using Xunit; +using ZB.MOM.WW.HistorianGateway.Contracts.Grpc; +using ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Mapping; + +namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests.Mapping; + +public sealed class EventMapperTests +{ + [Fact] + public void Maps_core_fields_and_times() + { + var e = new HistorianEvent { Id = "E1", SourceName = "Pump1", + EventTime = Ts(2026, 1, 1, 0, 0, 0), ReceivedTime = Ts(2026, 1, 1, 0, 0, 5) }; + e.Properties["Message"] = "High temp"; + e.Properties["Severity"] = "700"; + var h = EventMapper.ToHistoricalEvent(e); + Assert.Equal("E1", h.EventId); + Assert.Equal("Pump1", h.SourceName); + Assert.Equal("High temp", h.Message); + Assert.Equal((ushort)700, h.Severity); + Assert.Equal(DateTimeKind.Utc, h.EventTimeUtc.Kind); + } + + [Theory] + [InlineData("Priority", "999", 999)] + [InlineData("Severity", "0", 1)] // clamp to OPC UA min 1 + [InlineData("Severity", "5000", 1000)] // clamp to OPC UA max 1000 + [InlineData(null, null, 1)] // missing → default min severity + public void Severity_parsed_and_clamped(string? key, string? val, int expected) + { + var e = new HistorianEvent { Id = "E", EventTime = Ts(2026, 1, 1, 0, 0, 0), ReceivedTime = Ts(2026, 1, 1, 0, 0, 0) }; + if (key is not null) e.Properties[key] = val!; + Assert.Equal((ushort)expected, EventMapper.ToHistoricalEvent(e).Severity); + } + + // Ts(...) builds a Google.Protobuf.WellKnownTypes.Timestamp from UTC parts. + private static Timestamp Ts(int y, int mo, int d, int h, int mi, int s) + => Timestamp.FromDateTime(new DateTime(y, mo, d, h, mi, s, DateTimeKind.Utc)); +}