using Google.Protobuf.WellKnownTypes;
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;
using ZB.MOM.WW.OtOpcUa.Runtime.Telemetry;
namespace ZB.MOM.WW.OtOpcUa.Host.Grpc;
///
/// Node-side projection of a domain onto its wire
/// envelope (per-cluster mesh Phase 5). The exact reverse of
/// telemetry.proto: it transcribes every domain field onto the matching proto field and
/// selects the envelope's oneof arm from the subtype.
///
///
///
/// DateTime → Timestamp is Kind-defensive. throws
/// unless the source is . Producers
/// use today, but a stray or
/// value must never crash the stream — so a Local value
/// is converted and an Unspecified value is assumed already-UTC (matching the producer intent
/// and preserving the instant on any machine).
///
///
/// Nullable presence is preserved. A null nullable-DateTime leaves the proto
/// Timestamp unset (absent == null per the contract); a null optional string leaves the
/// proto field unset so the generated Has… presence distinguishes null from "".
///
///
public static class TelemetryProtoMapNode
{
/// Projects onto a wire .
/// The domain telemetry item to project.
/// Stream correlation id echoed on every envelope.
/// The wire envelope with the matching oneof arm populated.
public static TelemetryEvent ToProto(TelemetryItem item, string correlationId)
{
ArgumentNullException.ThrowIfNull(item);
return item switch
{
TelemetryItem.Alarm a => new TelemetryEvent
{
CorrelationId = correlationId,
AlarmTransition = MapAlarm(a.E),
},
TelemetryItem.Script s => new TelemetryEvent
{
CorrelationId = correlationId,
ScriptLog = MapScript(s.E),
},
TelemetryItem.Health h => new TelemetryEvent
{
CorrelationId = correlationId,
DriverHealth = MapHealth(h.E),
},
TelemetryItem.Resilience r => new TelemetryEvent
{
CorrelationId = correlationId,
DriverResilience = MapResilience(r.E),
},
_ => throw new ArgumentOutOfRangeException(
nameof(item), item.GetType().Name, "Unknown TelemetryItem subtype"),
};
}
private static AlarmTransition MapAlarm(AlarmTransitionEvent e)
{
// Required (non-optional) proto string setters throw ArgumentNullException on null, and the
// domain records carry no runtime null guard (nullable-ref annotations are compile-time only).
// Coalesce every required string to "" so a stray null can never throw out of the pump and kill
// the stream. The `optional` fields below keep their null-vs-"" presence guards.
var msg = new AlarmTransition
{
AlarmId = e.AlarmId ?? "",
EquipmentPath = e.EquipmentPath ?? "",
AlarmName = e.AlarmName ?? "",
TransitionKind = e.TransitionKind ?? "",
Severity = e.Severity,
Message = e.Message ?? "",
User = e.User ?? "",
TimestampUtc = ToUtcTimestamp(e.TimestampUtc),
AlarmTypeName = e.AlarmTypeName ?? "",
};
if (e.Comment is not null)
msg.Comment = e.Comment;
if (e.HistorizeToAveva is not null)
msg.HistorizeToAveva = e.HistorizeToAveva.Value;
msg.ReferencingEquipmentPaths.AddRange(e.ReferencingEquipmentPaths ?? Enumerable.Empty());
return msg;
}
private static ScriptLog MapScript(ScriptLogEntry e)
{
var msg = new ScriptLog
{
ScriptId = e.ScriptId ?? "",
Level = e.Level ?? "",
Message = e.Message ?? "",
TimestampUtc = ToUtcTimestamp(e.TimestampUtc),
};
if (e.VirtualTagId is not null)
msg.VirtualTagId = e.VirtualTagId;
if (e.AlarmId is not null)
msg.AlarmId = e.AlarmId;
if (e.EquipmentId is not null)
msg.EquipmentId = e.EquipmentId;
return msg;
}
private static DriverHealth MapHealth(DriverHealthChanged e)
{
var msg = new DriverHealth
{
ClusterId = e.ClusterId ?? "",
DriverInstanceId = e.DriverInstanceId ?? "",
State = e.State ?? "",
ErrorCount5Min = e.ErrorCount5Min,
PublishedUtc = ToUtcTimestamp(e.PublishedUtc),
};
if (e.LastSuccessfulReadUtc is not null)
msg.LastSuccessfulReadUtc = ToUtcTimestamp(e.LastSuccessfulReadUtc.Value);
if (e.LastError is not null)
msg.LastError = e.LastError;
if (e.RediscoveryNeededUtc is not null)
msg.RediscoveryNeededUtc = ToUtcTimestamp(e.RediscoveryNeededUtc.Value);
if (e.RediscoveryReason is not null)
msg.RediscoveryReason = e.RediscoveryReason;
return msg;
}
private static DriverResilienceStatus MapResilience(DriverResilienceStatusChanged e)
{
var msg = new DriverResilienceStatus
{
DriverInstanceId = e.DriverInstanceId ?? "",
HostName = e.HostName ?? "",
BreakerOpen = e.BreakerOpen,
ConsecutiveFailures = e.ConsecutiveFailures,
CurrentInFlight = e.CurrentInFlight,
LastSampledUtc = ToUtcTimestamp(e.LastSampledUtc),
PublishedUtc = ToUtcTimestamp(e.PublishedUtc),
};
if (e.LastBreakerOpenUtc is not null)
msg.LastBreakerOpenUtc = ToUtcTimestamp(e.LastBreakerOpenUtc.Value);
return msg;
}
///
/// Converts a domain to a proto without ever
/// throwing on Kind: Utc is used as-is, Local is converted, and Unspecified is assumed
/// already-UTC (the producer contract — — with the wall-clock
/// ticks preserved so the instant round-trips).
///
private static Timestamp ToUtcTimestamp(DateTime dt)
{
var utc = dt.Kind switch
{
DateTimeKind.Utc => dt,
DateTimeKind.Local => dt.ToUniversalTime(),
_ => DateTime.SpecifyKind(dt, DateTimeKind.Utc),
};
return Timestamp.FromDateTime(utc);
}
}