Files
lmxopcua/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/Mapping/AlarmEventMapper.cs
T
Joseph Doherty 109163621c
v2-ci / build (pull_request) Successful in 5m52s
v2-ci / unit-tests (pull_request) Failing after 1h23m55s
fix(historian): preserve stable alarm delivery identities
2026-10-13 07:10:58 -04:00

68 lines
3.3 KiB
C#

using Google.Protobuf.WellKnownTypes;
using ZB.MOM.WW.HistorianGateway.Contracts.Grpc;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian;
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Mapping;
/// <summary>
/// Maps a driver-agnostic <see cref="AlarmHistorianEvent"/> onto a gateway wire
/// <see cref="HistorianEvent"/> for the <c>SendEvent</c> write path.
/// </summary>
internal static class AlarmEventMapper
{
/// <summary>
/// Maps <see cref="AlarmSeverity"/> to the numeric OPC UA Part 9 priority (1-1000) carried in the
/// wire event's <c>Severity</c> property, per the <see cref="AlarmHistorianEvent.Severity"/>
/// contract ("Mapped to Historian's numeric priority on the sink side"). The read side
/// (<see cref="EventMapper.ToHistoricalEvent"/>) <c>int.TryParse</c>s this same property, so it
/// must stay a numeric string — the enum's <c>.ToString()</c> name (e.g. "High") is NOT
/// parseable and used to silently collapse every read-back severity to the minimum (1). The
/// values mirror the existing native-alarm convention
/// (<c>ZB.MOM.WW.OtOpcUa.Runtime.Drivers.NativeAlarmProjector.MapSeverity</c>).
/// </summary>
private static string ToNumericSeverity(AlarmSeverity severity) => severity switch
{
AlarmSeverity.Low => "200",
AlarmSeverity.Medium => "500",
AlarmSeverity.High => "700",
AlarmSeverity.Critical => "900",
_ => "500",
};
/// <summary>Maps an alarm historian event to a gateway event.</summary>
/// <param name="alarm">The driver-agnostic alarm event.</param>
/// <returns>The gateway wire event ready for <c>SendEvent</c>.</returns>
public static HistorianEvent ToHistorianEvent(AlarmHistorianEvent alarm)
{
if (alarm.DeliveryId == Guid.Empty)
throw new ArgumentException("Alarm delivery identity must be nonzero.", nameof(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
{
// Identity belongs to the event/durable queue, never to an individual RPC attempt.
Id = alarm.DeliveryId.ToString("D"),
SourceName = alarm.EquipmentPath,
Type = alarm.AlarmTypeName,
EventTime = eventTime,
ReceivedTime = eventTime, // the server re-stamps the received time on the SQL path
};
// Proto map<string,string> values must be non-null — only insert non-null properties.
if (!string.IsNullOrWhiteSpace(alarm.AlarmId))
historianEvent.Properties["AlarmId"] = alarm.AlarmId;
historianEvent.Properties["AlarmName"] = alarm.AlarmName;
historianEvent.Properties["EventKind"] = alarm.EventKind;
historianEvent.Properties["Severity"] = ToNumericSeverity(alarm.Severity);
historianEvent.Properties["User"] = alarm.User;
historianEvent.Properties["Message"] = alarm.Message;
if (alarm.Comment is not null)
historianEvent.Properties["Comment"] = alarm.Comment;
return historianEvent;
}
}