feat(mesh-phase5): central telemetry dialer client + proto->domain converters
Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Alerts;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Drivers;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Logging;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Protos.Telemetry.V1;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.ControlPlane.Telemetry;
|
||||
|
||||
/// <summary>
|
||||
/// Central-side projection of a wire <see cref="TelemetryEvent"/> envelope back onto its domain
|
||||
/// record (per-cluster mesh Phase 5). The exact reverse of <c>TelemetryProtoMapNode</c>: it
|
||||
/// transcribes every proto field onto the matching domain field and selects the domain record from
|
||||
/// the envelope's <c>oneof</c> arm.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <b>Timestamp presence is honoured.</b> A proto <c>google.protobuf.Timestamp</c> field is a
|
||||
/// message: when the node left it unset (a null nullable-<c>DateTime</c> on the domain side) the
|
||||
/// generated property is <see langword="null"/>, so the nullable domain fields map via
|
||||
/// <c>?.ToDateTime()</c> and an absent Timestamp becomes <see langword="null"/>. The
|
||||
/// non-nullable domain fields (published / sampled / transition timestamps) are always set by
|
||||
/// the node mapper, so they map via a direct <c>.ToDateTime()</c> (which yields a
|
||||
/// <see cref="DateTimeKind.Utc"/> value).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>optional-string presence is honoured.</b> A proto3 <c>optional string</c> that the node
|
||||
/// left unset reads back as <see langword="null"/> (via the generated <c>Has…</c> presence),
|
||||
/// distinguishing null from the empty string; likewise the <c>optional bool</c>
|
||||
/// <c>historize_to_aveva</c> round-trips its three states (null / true / false).
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static class TelemetryProtoMapCentral
|
||||
{
|
||||
/// <summary>
|
||||
/// Projects a wire <see cref="TelemetryEvent"/> onto its domain record, selecting the type from
|
||||
/// the populated <c>oneof</c> arm. This switch is the coverage guard: every
|
||||
/// <see cref="TelemetryProtoContract.HandledCases"/> value has an arm here, and an unset (or a
|
||||
/// future unmapped) case throws <see cref="NotSupportedException"/> so adding a fifth oneof case
|
||||
/// without a converter fails the coverage test.
|
||||
/// </summary>
|
||||
/// <param name="evt">The wire envelope to project.</param>
|
||||
/// <returns>The domain record for the populated arm.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="evt"/> is null.</exception>
|
||||
/// <exception cref="NotSupportedException">The envelope carries no handled <c>oneof</c> arm.</exception>
|
||||
public static object MapEvent(TelemetryEvent evt)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(evt);
|
||||
|
||||
return evt.EventCase switch
|
||||
{
|
||||
TelemetryEvent.EventOneofCase.AlarmTransition => ToAlarm(evt.AlarmTransition),
|
||||
TelemetryEvent.EventOneofCase.ScriptLog => ToScript(evt.ScriptLog),
|
||||
TelemetryEvent.EventOneofCase.DriverHealth => ToHealth(evt.DriverHealth),
|
||||
TelemetryEvent.EventOneofCase.DriverResilience => ToResilience(evt.DriverResilience),
|
||||
_ => throw new NotSupportedException(
|
||||
$"TelemetryEvent carries no handled oneof arm (EventCase = {evt.EventCase})."),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Projects an <see cref="AlarmTransition"/> onto an <see cref="AlarmTransitionEvent"/>.</summary>
|
||||
/// <param name="msg">The proto sub-message.</param>
|
||||
/// <returns>The domain record.</returns>
|
||||
public static AlarmTransitionEvent ToAlarm(AlarmTransition msg)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(msg);
|
||||
|
||||
return new AlarmTransitionEvent(
|
||||
AlarmId: msg.AlarmId,
|
||||
EquipmentPath: msg.EquipmentPath,
|
||||
AlarmName: msg.AlarmName,
|
||||
TransitionKind: msg.TransitionKind,
|
||||
Severity: msg.Severity,
|
||||
Message: msg.Message,
|
||||
User: msg.User,
|
||||
TimestampUtc: msg.TimestampUtc.ToDateTime(),
|
||||
AlarmTypeName: msg.AlarmTypeName,
|
||||
Comment: msg.HasComment ? msg.Comment : null,
|
||||
HistorizeToAveva: msg.HasHistorizeToAveva ? msg.HistorizeToAveva : null,
|
||||
ReferencingEquipmentPaths: msg.ReferencingEquipmentPaths.ToList());
|
||||
}
|
||||
|
||||
/// <summary>Projects a <see cref="ScriptLog"/> onto a <see cref="ScriptLogEntry"/>.</summary>
|
||||
/// <param name="msg">The proto sub-message.</param>
|
||||
/// <returns>The domain record.</returns>
|
||||
public static ScriptLogEntry ToScript(ScriptLog msg)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(msg);
|
||||
|
||||
return new ScriptLogEntry(
|
||||
ScriptId: msg.ScriptId,
|
||||
Level: msg.Level,
|
||||
Message: msg.Message,
|
||||
TimestampUtc: msg.TimestampUtc.ToDateTime(),
|
||||
VirtualTagId: msg.HasVirtualTagId ? msg.VirtualTagId : null,
|
||||
AlarmId: msg.HasAlarmId ? msg.AlarmId : null,
|
||||
EquipmentId: msg.HasEquipmentId ? msg.EquipmentId : null);
|
||||
}
|
||||
|
||||
/// <summary>Projects a <see cref="DriverHealth"/> onto a <see cref="DriverHealthChanged"/>.</summary>
|
||||
/// <param name="msg">The proto sub-message.</param>
|
||||
/// <returns>The domain record.</returns>
|
||||
public static DriverHealthChanged ToHealth(DriverHealth msg)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(msg);
|
||||
|
||||
return new DriverHealthChanged(
|
||||
ClusterId: msg.ClusterId,
|
||||
DriverInstanceId: msg.DriverInstanceId,
|
||||
State: msg.State,
|
||||
LastSuccessfulReadUtc: msg.LastSuccessfulReadUtc?.ToDateTime(),
|
||||
LastError: msg.HasLastError ? msg.LastError : null,
|
||||
ErrorCount5Min: msg.ErrorCount5Min,
|
||||
PublishedUtc: msg.PublishedUtc.ToDateTime());
|
||||
}
|
||||
|
||||
/// <summary>Projects a <see cref="DriverResilienceStatus"/> onto a <see cref="DriverResilienceStatusChanged"/>.</summary>
|
||||
/// <param name="msg">The proto sub-message.</param>
|
||||
/// <returns>The domain record.</returns>
|
||||
public static DriverResilienceStatusChanged ToResilience(DriverResilienceStatus msg)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(msg);
|
||||
|
||||
return new DriverResilienceStatusChanged(
|
||||
DriverInstanceId: msg.DriverInstanceId,
|
||||
HostName: msg.HostName,
|
||||
BreakerOpen: msg.BreakerOpen,
|
||||
ConsecutiveFailures: msg.ConsecutiveFailures,
|
||||
CurrentInFlight: msg.CurrentInFlight,
|
||||
LastBreakerOpenUtc: msg.LastBreakerOpenUtc?.ToDateTime(),
|
||||
LastSampledUtc: msg.LastSampledUtc.ToDateTime(),
|
||||
PublishedUtc: msg.PublishedUtc.ToDateTime());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user