Files
lmxopcua/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/Mapping/AlarmEventMapper.cs
T
Joseph Doherty 55b12dc217 fix(historian-gateway): map alarm severity numerically; dispose provisioner's gRPC channel
Driver.Historian.Gateway-001 (High): AlarmEventMapper wrote the alarm's
Severity wire property as the .NET enum name (e.g. "High"), but
EventMapper's read side int.TryParse()s that same property and silently
falls back to the minimum severity on any unparseable value - so every
alarm event's severity round-tripped through this module's own mappers
collapsed to 1 (Low), contradicting AlarmHistorianEvent.Severity's own
documented contract ("mapped to Historian's numeric priority"). Now
mapped through a numeric priority mirroring the existing
NativeAlarmProjector.MapSeverity convention (200/500/700/900).

Driver.Historian.Gateway-002 (Low): GatewayTagProvisioner never disposed
its IHistorianGatewayClient, unlike its GatewayHistorianDataSource /
GatewayAlarmHistorianWriter siblings. Now implements IAsyncDisposable so
the DI-owned singleton's gRPC channel closes gracefully at shutdown.

Both TDD: new/corrected tests confirmed RED against the pre-fix code.
2026-08-10 06:53:41 -04:00

68 lines
3.4 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)
{
// 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
{
// Deliberately DO NOT set HistorianEvent.Id: the gateway's SendEvent path rejects a
// client-supplied event id (the server handler throws and the call fails permanently —
// confirmed live). The historian assigns event identity server-side; the alarm's own id
// is preserved below as a property for read-back correlation/traceability.
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;
}
}