feat(historian-gateway): HistorianEvent->HistoricalEvent mapper (+ clamped severity)
Claude-Session: https://claude.ai/code/session_012SDSQ3AcaXqPcBtDESBRii
This commit is contained in:
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps a gateway wire event (<see cref="HistorianEvent"/>) onto the driver-agnostic
|
||||||
|
/// <see cref="HistoricalEvent"/> consumed by the Server's HistoryReadEvents path.
|
||||||
|
/// </summary>
|
||||||
|
internal static class EventMapper
|
||||||
|
{
|
||||||
|
/// <summary>OPC UA severity range (Part 9): 1 (lowest) … 1000 (highest).</summary>
|
||||||
|
private const ushort MinSeverity = 1;
|
||||||
|
private const ushort MaxSeverity = 1000;
|
||||||
|
|
||||||
|
/// <summary>Maps a single gateway event to a historical event.</summary>
|
||||||
|
/// <param name="historianEvent">The gateway wire event.</param>
|
||||||
|
/// <returns>The driver-agnostic historical event.</returns>
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Maps a batch of gateway events to historical events, in order.</summary>
|
||||||
|
/// <param name="events">The gateway wire events.</param>
|
||||||
|
/// <returns>The driver-agnostic historical events.</returns>
|
||||||
|
public static IReadOnlyList<HistoricalEvent> ToHistoricalEvents(IEnumerable<HistorianEvent> events)
|
||||||
|
{
|
||||||
|
var result = new List<HistoricalEvent>();
|
||||||
|
foreach (var historianEvent in events)
|
||||||
|
result.Add(ToHistoricalEvent(historianEvent));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses an OPC UA severity from the "Severity" property (else "Priority"), clamped to
|
||||||
|
/// <c>[1, 1000]</c>. Missing or unparseable values default to the minimum severity (1).
|
||||||
|
/// </summary>
|
||||||
|
private static ushort ParseSeverity(IDictionary<string, string> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+40
@@ -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));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user