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; namespace ZB.MOM.WW.OtOpcUa.ControlPlane.Telemetry; /// /// Central-side projection of a wire envelope back onto its domain /// record (per-cluster mesh Phase 5). The exact reverse of TelemetryProtoMapNode: it /// transcribes every proto field onto the matching domain field and selects the domain record from /// the envelope's oneof arm. /// /// /// /// Timestamp presence is honoured. A proto google.protobuf.Timestamp field is a /// message: when the node left it unset (a null nullable-DateTime on the domain side) the /// generated property is , so the nullable domain fields map via /// ?.ToDateTime() and an absent Timestamp becomes . The /// non-nullable domain fields (published / sampled / transition timestamps) are always set by /// the node mapper, so they map via (which yields a /// value). /// /// /// A required Timestamp is defended, not assumed. Across a mixed-version fleet an /// out-of-contract sender could leave a required Timestamp unset. throws /// a clear naming the kind + field instead of letting a /// bare escape — the client's per-event isolation then logs /// it as a dropped malformed event and continues, because it is a code/version fault, never a /// transport fault. /// /// /// optional-string presence is honoured. A proto3 optional string that the node /// left unset reads back as (via the generated Has… presence), /// distinguishing null from the empty string; likewise the optional bool /// historize_to_aveva round-trips its three states (null / true / false). /// /// public static class TelemetryProtoMapCentral { /// /// Projects a wire onto its domain record, selecting the type from /// the populated oneof arm. This switch is the coverage guard: every /// value has an arm here, and an unset (or a /// future unmapped) case throws so adding a fifth oneof case /// without a converter fails the coverage test. /// /// The wire envelope to project. /// The domain record for the populated arm. /// is null. /// The envelope carries no handled oneof arm. 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})."), }; } /// Projects an onto an . /// The proto sub-message. /// The domain record. 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: Required(msg.TimestampUtc, "AlarmTransition", "timestamp_utc"), AlarmTypeName: msg.AlarmTypeName, Comment: msg.HasComment ? msg.Comment : null, HistorizeToAveva: msg.HasHistorizeToAveva ? msg.HistorizeToAveva : null, ReferencingEquipmentPaths: msg.ReferencingEquipmentPaths.ToList()); } /// Projects a onto a . /// The proto sub-message. /// The domain record. public static ScriptLogEntry ToScript(ScriptLog msg) { ArgumentNullException.ThrowIfNull(msg); return new ScriptLogEntry( ScriptId: msg.ScriptId, Level: msg.Level, Message: msg.Message, TimestampUtc: Required(msg.TimestampUtc, "ScriptLog", "timestamp_utc"), VirtualTagId: msg.HasVirtualTagId ? msg.VirtualTagId : null, AlarmId: msg.HasAlarmId ? msg.AlarmId : null, EquipmentId: msg.HasEquipmentId ? msg.EquipmentId : null); } /// Projects a onto a . /// The proto sub-message. /// The domain record. 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: Required(msg.PublishedUtc, "DriverHealth", "published_utc"), RediscoveryNeededUtc: msg.RediscoveryNeededUtc?.ToDateTime(), RediscoveryReason: msg.HasRediscoveryReason ? msg.RediscoveryReason : null); } /// Projects a onto a . /// The proto sub-message. /// The domain record. 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: Required(msg.LastSampledUtc, "DriverResilienceStatus", "last_sampled_utc"), PublishedUtc: Required(msg.PublishedUtc, "DriverResilienceStatus", "published_utc")); } /// /// Converts a required proto to a UTC , /// throwing a clear naming the telemetry kind + proto /// field if an out-of-contract sender left it unset — rather than letting a bare /// escape. /// /// The required proto Timestamp ( when the sender omitted it). /// The telemetry sub-message name, for the diagnostic. /// The proto field name, for the diagnostic. /// The Timestamp as a value. /// is unset. private static DateTime Required(Timestamp? ts, string kind, string field) => ts is null ? throw new InvalidOperationException($"telemetry {kind} missing required timestamp {field}") : ts.ToDateTime(); }