using Google.Protobuf; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Artifacts; using ZB.MOM.WW.ScadaBridge.Commons.Messages.DataConnection; using ZB.MOM.WW.ScadaBridge.Commons.Messages.DebugView; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Deployment; using ZB.MOM.WW.ScadaBridge.Commons.Messages.InboundApi; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Lifecycle; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management; using ZB.MOM.WW.ScadaBridge.Commons.Messages.RemoteQuery; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming; using ZB.MOM.WW.ScadaBridge.Commons.Types; using ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms; using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums; using BoolValue = Google.Protobuf.WellKnownTypes.BoolValue; using Duration = Google.Protobuf.WellKnownTypes.Duration; using Int32Value = Google.Protobuf.WellKnownTypes.Int32Value; using StringValue = Google.Protobuf.WellKnownTypes.StringValue; using Timestamp = Google.Protobuf.WellKnownTypes.Timestamp; namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc; /// /// Canonical bridge between the in-process central→site command/reply records /// (Commons Messages.*) and the wire messages of /// Protos/site_command.proto. Every one of the 28 migrated commands and /// all 22 reply shapes pass through here; nothing else may hand-roll the /// translation, so the contract has exactly one definition. /// /// /// /// Mirrors the sibling / /// and lives in Communication for the same reason: this project owns the /// generated wire types and already references Commons, so both the site /// server (T1B.2) and the central transport (T1B.3) can share one implementation /// without a project-reference cycle. /// /// Null conventions. proto3 has no presence for scalars, so: /// /// /// Nullable strings ride as plain string and the empty string /// represents null — the convention /// already established. It is a normalisation, not a loss: every such member /// here is an optional error message, token, filter or id where "" and /// null carry the same meaning. The one exception is /// , where /// "wait for the empty string" is a real target distinct from "no target"; /// that field uses a StringValue wrapper instead. /// /// /// Nullable ints/bools use the Int32Value/BoolValue /// wrappers, and nullable enums use a one-field message of the same /// shape (proto3 enums have no presence and no stock wrapper exists). /// /// /// Nullable collections are wrapped in a per-collection message, so /// the null/empty distinction becomes a message-presence question. proto3 /// repeated/map alone collapses the two — precisely the silent /// data-loss class the transport round-trip guard caught in the transport round-trip tests. /// /// /// Guid? members ride as a string holding the "D" form, with /// the empty string representing null. /// (a non-nullable struct over a /// ) rides as the same "D" string. /// /// /// /// Time. DTO members declared use /// google.protobuf.Timestamp. That normalises to UTC, which is lossless /// under equality (it compares instants), and every /// timestamp in this contract is documented UTC anyway. Members declared /// (the certificate validity bounds) are coerced to /// first, matching their …Utc names. /// Loosely-typed object? values are the exception and keep their exact /// kind/offset — see . /// /// /// Enums translate through explicit switches in both directions, never by /// ordinal, so reordering a C# enum cannot silently re-map the wire. Every wire /// enum reserves 0 for _UNSPECIFIED; decoding an unspecified or unknown /// value yields the documented safe default for that enum (never a throw — a /// version-skewed peer must not be able to fault a whole command). /// /// public static class SiteCommandDtoMapper { // ───────────────────────────────────────────────────────────────────── // Group classification — the authoritative 28-command inventory in code. // ───────────────────────────────────────────────────────────────────── /// /// Classifies a central→site command onto the RPC group that carries it. /// The central transport (T1B.3) uses this to pick both the RPC and the /// deadline; the coverage tests use it as the command inventory. /// /// The command record to classify. /// The whose RPC carries the command. /// The type is not part of the migrated command set. public static SiteCommandGroup GroupOf(object command) { ArgumentNullException.ThrowIfNull(command); return command switch { RefreshDeploymentCommand or EnableInstanceCommand or DisableInstanceCommand or DeleteInstanceCommand or DeploymentStateQueryRequest or DeployArtifactsCommand => SiteCommandGroup.Lifecycle, BrowseNodeCommand or SearchAddressSpaceCommand or ReadTagValuesCommand or VerifyEndpointCommand or TrustServerCertCommand or ListServerCertsCommand or RemoveServerCertCommand or WriteTagRequest => SiteCommandGroup.OpcUa, EventLogQueryRequest or DebugSnapshotRequest or SubscribeDebugViewRequest or UnsubscribeDebugViewRequest => SiteCommandGroup.Query, ParkedMessageQueryRequest or ParkedMessageRetryRequest or ParkedMessageDiscardRequest or RetryParkedOperation or DiscardParkedOperation => SiteCommandGroup.Parked, RouteToCallRequest or RouteToGetAttributesRequest or RouteToSetAttributesRequest or RouteToWaitForAttributeRequest => SiteCommandGroup.Route, TriggerSiteFailover => SiteCommandGroup.Failover, _ => throw new ArgumentException( $"'{command.GetType().Name}' is not a migrated site command.", nameof(command)) }; } /// /// Classifies a site→central reply onto the RPC group whose reply envelope /// carries it — the mirror of , used by the site /// dispatcher to pack whatever a handler answered into the right envelope. /// /// The reply record to classify. /// The whose reply envelope carries it. /// The type is not a migrated reply shape. public static SiteCommandGroup GroupOfReply(object reply) { ArgumentNullException.ThrowIfNull(reply); return reply switch { DeploymentStatusResponse or InstanceLifecycleResponse or DeploymentStateQueryResponse or ArtifactDeploymentResponse => SiteCommandGroup.Lifecycle, BrowseNodeResult or SearchAddressSpaceResult or ReadTagValuesResult or VerifyEndpointResult or CertTrustResult or WriteTagResponse => SiteCommandGroup.OpcUa, EventLogQueryResponse or DebugViewSnapshot or UnsubscribeDebugViewAck => SiteCommandGroup.Query, ParkedMessageQueryResponse or ParkedMessageRetryResponse or ParkedMessageDiscardResponse or ParkedOperationActionAck => SiteCommandGroup.Parked, RouteToCallResponse or RouteToGetAttributesResponse or RouteToSetAttributesResponse or RouteToWaitForAttributeResponse => SiteCommandGroup.Route, SiteFailoverAck => SiteCommandGroup.Failover, _ => throw new ArgumentException( $"'{reply.GetType().Name}' is not a migrated site reply.", nameof(reply)) }; } // ───────────────────────────────────────────────────────────────────── // RPC 1 — ExecuteLifecycle: envelopes // ───────────────────────────────────────────────────────────────────── /// Packs a lifecycle-group command into its request envelope. /// One of the six lifecycle-group commands. /// The populated request envelope. /// The command does not belong to this group. public static LifecycleRequest ToLifecycleRequest(object command) { ArgumentNullException.ThrowIfNull(command); return command switch { RefreshDeploymentCommand c => new LifecycleRequest { RefreshDeployment = ToProto(c) }, EnableInstanceCommand c => new LifecycleRequest { EnableInstance = ToProto(c) }, DisableInstanceCommand c => new LifecycleRequest { DisableInstance = ToProto(c) }, DeleteInstanceCommand c => new LifecycleRequest { DeleteInstance = ToProto(c) }, DeploymentStateQueryRequest c => new LifecycleRequest { DeploymentStateQuery = ToProto(c) }, DeployArtifactsCommand c => new LifecycleRequest { DeployArtifacts = ToProto(c) }, _ => throw NotInGroup(command, SiteCommandGroup.Lifecycle) }; } /// Unpacks a lifecycle-group request envelope back to its command record. /// The request envelope received on the wire. /// The decoded command record. /// The envelope carries no case this build knows. public static object FromLifecycleRequest(LifecycleRequest request) { ArgumentNullException.ThrowIfNull(request); return request.CommandCase switch { LifecycleRequest.CommandOneofCase.RefreshDeployment => FromProto(request.RefreshDeployment), LifecycleRequest.CommandOneofCase.EnableInstance => FromProto(request.EnableInstance), LifecycleRequest.CommandOneofCase.DisableInstance => FromProto(request.DisableInstance), LifecycleRequest.CommandOneofCase.DeleteInstance => FromProto(request.DeleteInstance), LifecycleRequest.CommandOneofCase.DeploymentStateQuery => FromProto(request.DeploymentStateQuery), LifecycleRequest.CommandOneofCase.DeployArtifacts => FromProto(request.DeployArtifacts), _ => throw UnknownCase(request.CommandCase, nameof(LifecycleRequest)) }; } /// Packs a lifecycle-group reply into its reply envelope. /// One of the four lifecycle-group reply records. /// The populated reply envelope. /// The reply does not belong to this group. public static LifecycleReply ToLifecycleReply(object reply) { ArgumentNullException.ThrowIfNull(reply); return reply switch { DeploymentStatusResponse r => new LifecycleReply { DeploymentStatus = ToProto(r) }, InstanceLifecycleResponse r => new LifecycleReply { InstanceLifecycle = ToProto(r) }, DeploymentStateQueryResponse r => new LifecycleReply { DeploymentStateQuery = ToProto(r) }, ArtifactDeploymentResponse r => new LifecycleReply { ArtifactDeployment = ToProto(r) }, _ => throw NotInGroup(reply, SiteCommandGroup.Lifecycle) }; } /// Unpacks a lifecycle-group reply envelope back to its reply record. /// The reply envelope received on the wire. /// The decoded reply record. /// The envelope carries no case this build knows. public static object FromLifecycleReply(LifecycleReply reply) { ArgumentNullException.ThrowIfNull(reply); return reply.ReplyCase switch { LifecycleReply.ReplyOneofCase.DeploymentStatus => FromProto(reply.DeploymentStatus), LifecycleReply.ReplyOneofCase.InstanceLifecycle => FromProto(reply.InstanceLifecycle), LifecycleReply.ReplyOneofCase.DeploymentStateQuery => FromProto(reply.DeploymentStateQuery), LifecycleReply.ReplyOneofCase.ArtifactDeployment => FromProto(reply.ArtifactDeployment), _ => throw UnknownCase(reply.ReplyCase, nameof(LifecycleReply)) }; } // ── Lifecycle commands ── /// Projects a onto the wire. /// The command to project. /// The wire message. public static RefreshDeploymentCommandDto ToProto(RefreshDeploymentCommand command) { ArgumentNullException.ThrowIfNull(command); return new RefreshDeploymentCommandDto { DeploymentId = command.DeploymentId, InstanceUniqueName = command.InstanceUniqueName, RevisionHash = command.RevisionHash, DeployedBy = command.DeployedBy, Timestamp = Ts(command.Timestamp), CentralFetchBaseUrl = command.CentralFetchBaseUrl, FetchToken = command.FetchToken }; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded command record. public static RefreshDeploymentCommand FromProto(RefreshDeploymentCommandDto dto) { ArgumentNullException.ThrowIfNull(dto); return new RefreshDeploymentCommand( dto.DeploymentId, dto.InstanceUniqueName, dto.RevisionHash, dto.DeployedBy, Dto(dto.Timestamp), dto.CentralFetchBaseUrl, dto.FetchToken); } /// Projects an onto the wire. /// The command to project. /// The wire message. public static EnableInstanceCommandDto ToProto(EnableInstanceCommand command) { ArgumentNullException.ThrowIfNull(command); return new EnableInstanceCommandDto { CommandId = command.CommandId, InstanceUniqueName = command.InstanceUniqueName, Timestamp = Ts(command.Timestamp) }; } /// Rehydrates an from the wire. /// The wire message to decode. /// The decoded command record. public static EnableInstanceCommand FromProto(EnableInstanceCommandDto dto) { ArgumentNullException.ThrowIfNull(dto); return new EnableInstanceCommand(dto.CommandId, dto.InstanceUniqueName, Dto(dto.Timestamp)); } /// Projects a onto the wire. /// The command to project. /// The wire message. public static DisableInstanceCommandDto ToProto(DisableInstanceCommand command) { ArgumentNullException.ThrowIfNull(command); return new DisableInstanceCommandDto { CommandId = command.CommandId, InstanceUniqueName = command.InstanceUniqueName, Timestamp = Ts(command.Timestamp) }; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded command record. public static DisableInstanceCommand FromProto(DisableInstanceCommandDto dto) { ArgumentNullException.ThrowIfNull(dto); return new DisableInstanceCommand(dto.CommandId, dto.InstanceUniqueName, Dto(dto.Timestamp)); } /// Projects a onto the wire. /// The command to project. /// The wire message. public static DeleteInstanceCommandDto ToProto(DeleteInstanceCommand command) { ArgumentNullException.ThrowIfNull(command); return new DeleteInstanceCommandDto { CommandId = command.CommandId, InstanceUniqueName = command.InstanceUniqueName, Timestamp = Ts(command.Timestamp) }; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded command record. public static DeleteInstanceCommand FromProto(DeleteInstanceCommandDto dto) { ArgumentNullException.ThrowIfNull(dto); return new DeleteInstanceCommand(dto.CommandId, dto.InstanceUniqueName, Dto(dto.Timestamp)); } /// Projects a onto the wire. /// The request to project. /// The wire message. public static DeploymentStateQueryRequestDto ToProto(DeploymentStateQueryRequest request) { ArgumentNullException.ThrowIfNull(request); return new DeploymentStateQueryRequestDto { CorrelationId = request.CorrelationId, InstanceUniqueName = request.InstanceUniqueName, Timestamp = Ts(request.Timestamp) }; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded request record. public static DeploymentStateQueryRequest FromProto(DeploymentStateQueryRequestDto dto) { ArgumentNullException.ThrowIfNull(dto); return new DeploymentStateQueryRequest(dto.CorrelationId, dto.InstanceUniqueName, Dto(dto.Timestamp)); } /// Projects a onto the wire. /// Each artifact collection is nullable; a null collection leaves its wrapper message unset. /// The command to project. /// The wire message. public static DeployArtifactsCommandDto ToProto(DeployArtifactsCommand command) { ArgumentNullException.ThrowIfNull(command); var dto = new DeployArtifactsCommandDto { DeploymentId = command.DeploymentId, Timestamp = Ts(command.Timestamp) }; if (command.SharedScripts is not null) { var list = new SharedScriptArtifactListDto(); list.Items.AddRange(command.SharedScripts.Select(ToProto)); dto.SharedScripts = list; } if (command.ExternalSystems is not null) { var list = new ExternalSystemArtifactListDto(); list.Items.AddRange(command.ExternalSystems.Select(ToProto)); dto.ExternalSystems = list; } if (command.DatabaseConnections is not null) { var list = new DatabaseConnectionArtifactListDto(); list.Items.AddRange(command.DatabaseConnections.Select(ToProto)); dto.DatabaseConnections = list; } if (command.NotificationLists is not null) { var list = new NotificationListArtifactListDto(); list.Items.AddRange(command.NotificationLists.Select(ToProto)); dto.NotificationLists = list; } if (command.DataConnections is not null) { var list = new DataConnectionArtifactListDto(); list.Items.AddRange(command.DataConnections.Select(ToProto)); dto.DataConnections = list; } if (command.SmtpConfigurations is not null) { var list = new SmtpConfigurationArtifactListDto(); list.Items.AddRange(command.SmtpConfigurations.Select(ToProto)); dto.SmtpConfigurations = list; } if (command.Secrets is not null) { var list = new SecretArtifactListDto(); list.Items.AddRange(command.Secrets.Select(ToProto)); dto.Secrets = list; } return dto; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded command record; unset collection wrappers rehydrate as null. public static DeployArtifactsCommand FromProto(DeployArtifactsCommandDto dto) { ArgumentNullException.ThrowIfNull(dto); return new DeployArtifactsCommand( dto.DeploymentId, dto.SharedScripts is null ? null : dto.SharedScripts.Items.Select(FromProto).ToList(), dto.ExternalSystems is null ? null : dto.ExternalSystems.Items.Select(FromProto).ToList(), dto.DatabaseConnections is null ? null : dto.DatabaseConnections.Items.Select(FromProto).ToList(), dto.NotificationLists is null ? null : dto.NotificationLists.Items.Select(FromProto).ToList(), dto.DataConnections is null ? null : dto.DataConnections.Items.Select(FromProto).ToList(), dto.SmtpConfigurations is null ? null : dto.SmtpConfigurations.Items.Select(FromProto).ToList(), Dto(dto.Timestamp), dto.Secrets is null ? null : dto.Secrets.Items.Select(FromProto).ToList()); } /// Projects a onto the wire. /// The artifact to project. /// The wire message. public static SecretArtifactDto ToProto(SecretArtifact artifact) { ArgumentNullException.ThrowIfNull(artifact); return new SecretArtifactDto { Name = artifact.Name, ContentType = artifact.ContentType, Ciphertext = ByteString.CopyFrom(artifact.Ciphertext), Nonce = ByteString.CopyFrom(artifact.Nonce), Tag = ByteString.CopyFrom(artifact.Tag), WrappedDek = ByteString.CopyFrom(artifact.WrappedDek), WrapNonce = ByteString.CopyFrom(artifact.WrapNonce), WrapTag = ByteString.CopyFrom(artifact.WrapTag), KekId = artifact.KekId, Revision = artifact.Revision, CreatedUtc = Ts(artifact.CreatedUtc), UpdatedUtc = Ts(artifact.UpdatedUtc), Description = artifact.Description }; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded artifact. public static SecretArtifact FromProto(SecretArtifactDto dto) { ArgumentNullException.ThrowIfNull(dto); return new SecretArtifact( dto.Name, dto.ContentType, dto.Ciphertext.ToByteArray(), dto.Nonce.ToByteArray(), dto.Tag.ToByteArray(), dto.WrappedDek.ToByteArray(), dto.WrapNonce.ToByteArray(), dto.WrapTag.ToByteArray(), dto.KekId, dto.Revision, Dto(dto.CreatedUtc), Dto(dto.UpdatedUtc), dto.Description); } /// Projects a onto the wire. /// The artifact to project. /// The wire message. public static SharedScriptArtifactDto ToProto(SharedScriptArtifact artifact) { ArgumentNullException.ThrowIfNull(artifact); return new SharedScriptArtifactDto { Name = artifact.Name, Code = artifact.Code, ParameterDefinitions = OrEmpty(artifact.ParameterDefinitions), ReturnDefinition = OrEmpty(artifact.ReturnDefinition) }; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded artifact. public static SharedScriptArtifact FromProto(SharedScriptArtifactDto dto) { ArgumentNullException.ThrowIfNull(dto); return new SharedScriptArtifact( dto.Name, dto.Code, NullIfEmpty(dto.ParameterDefinitions), NullIfEmpty(dto.ReturnDefinition)); } /// Projects an onto the wire. /// The artifact to project. /// The wire message. public static ExternalSystemArtifactDto ToProto(ExternalSystemArtifact artifact) { ArgumentNullException.ThrowIfNull(artifact); return new ExternalSystemArtifactDto { Name = artifact.Name, EndpointUrl = artifact.EndpointUrl, AuthType = artifact.AuthType, AuthConfiguration = OrEmpty(artifact.AuthConfiguration), MethodDefinitionsJson = OrEmpty(artifact.MethodDefinitionsJson), TimeoutSeconds = artifact.TimeoutSeconds, MaxRetries = artifact.MaxRetries, RetryDelay = Duration.FromTimeSpan(artifact.RetryDelay) }; } /// Rehydrates an from the wire. /// The wire message to decode. /// The decoded artifact. public static ExternalSystemArtifact FromProto(ExternalSystemArtifactDto dto) { ArgumentNullException.ThrowIfNull(dto); return new ExternalSystemArtifact( dto.Name, dto.EndpointUrl, dto.AuthType, NullIfEmpty(dto.AuthConfiguration), NullIfEmpty(dto.MethodDefinitionsJson), dto.TimeoutSeconds, dto.MaxRetries, dto.RetryDelay?.ToTimeSpan() ?? TimeSpan.Zero); } /// Projects a onto the wire. /// The artifact to project. /// The wire message. public static DatabaseConnectionArtifactDto ToProto(DatabaseConnectionArtifact artifact) { ArgumentNullException.ThrowIfNull(artifact); return new DatabaseConnectionArtifactDto { Name = artifact.Name, ConnectionString = artifact.ConnectionString, MaxRetries = artifact.MaxRetries, RetryDelay = Duration.FromTimeSpan(artifact.RetryDelay) }; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded artifact. public static DatabaseConnectionArtifact FromProto(DatabaseConnectionArtifactDto dto) { ArgumentNullException.ThrowIfNull(dto); return new DatabaseConnectionArtifact( dto.Name, dto.ConnectionString, dto.MaxRetries, dto.RetryDelay?.ToTimeSpan() ?? TimeSpan.Zero); } /// Projects a onto the wire. /// The artifact to project. /// The wire message. public static NotificationListArtifactDto ToProto(NotificationListArtifact artifact) { ArgumentNullException.ThrowIfNull(artifact); var dto = new NotificationListArtifactDto { Name = artifact.Name }; dto.RecipientEmails.AddRange(artifact.RecipientEmails); return dto; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded artifact. public static NotificationListArtifact FromProto(NotificationListArtifactDto dto) { ArgumentNullException.ThrowIfNull(dto); return new NotificationListArtifact(dto.Name, dto.RecipientEmails.ToList()); } /// Projects a onto the wire. /// The artifact to project. /// The wire message. public static DataConnectionArtifactDto ToProto(DataConnectionArtifact artifact) { ArgumentNullException.ThrowIfNull(artifact); return new DataConnectionArtifactDto { Name = artifact.Name, Protocol = artifact.Protocol, PrimaryConfigurationJson = OrEmpty(artifact.PrimaryConfigurationJson), BackupConfigurationJson = OrEmpty(artifact.BackupConfigurationJson), FailoverRetryCount = artifact.FailoverRetryCount }; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded artifact. public static DataConnectionArtifact FromProto(DataConnectionArtifactDto dto) { ArgumentNullException.ThrowIfNull(dto); return new DataConnectionArtifact( dto.Name, dto.Protocol, NullIfEmpty(dto.PrimaryConfigurationJson), NullIfEmpty(dto.BackupConfigurationJson), dto.FailoverRetryCount); } /// Projects an onto the wire. /// The artifact to project. /// The wire message. public static SmtpConfigurationArtifactDto ToProto(SmtpConfigurationArtifact artifact) { ArgumentNullException.ThrowIfNull(artifact); return new SmtpConfigurationArtifactDto { Name = artifact.Name, Server = artifact.Server, Port = artifact.Port, AuthMode = artifact.AuthMode, FromAddress = artifact.FromAddress, Username = OrEmpty(artifact.Username), Password = OrEmpty(artifact.Password), OauthConfig = OrEmpty(artifact.OAuthConfig) }; } /// Rehydrates an from the wire. /// The wire message to decode. /// The decoded artifact. public static SmtpConfigurationArtifact FromProto(SmtpConfigurationArtifactDto dto) { ArgumentNullException.ThrowIfNull(dto); return new SmtpConfigurationArtifact( dto.Name, dto.Server, dto.Port, dto.AuthMode, dto.FromAddress, NullIfEmpty(dto.Username), NullIfEmpty(dto.Password), NullIfEmpty(dto.OauthConfig)); } // ── Lifecycle replies ── /// Projects a onto the wire. /// The reply to project. /// The wire message. public static DeploymentStatusResponseDto ToProto(DeploymentStatusResponse response) { ArgumentNullException.ThrowIfNull(response); return new DeploymentStatusResponseDto { DeploymentId = response.DeploymentId, InstanceUniqueName = response.InstanceUniqueName, Status = ToProto(response.Status), ErrorMessage = OrEmpty(response.ErrorMessage), Timestamp = Ts(response.Timestamp) }; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded reply record. public static DeploymentStatusResponse FromProto(DeploymentStatusResponseDto dto) { ArgumentNullException.ThrowIfNull(dto); return new DeploymentStatusResponse( dto.DeploymentId, dto.InstanceUniqueName, FromProto(dto.Status), NullIfEmpty(dto.ErrorMessage), Dto(dto.Timestamp)); } /// Projects an onto the wire. /// The reply to project. /// The wire message. public static InstanceLifecycleResponseDto ToProto(InstanceLifecycleResponse response) { ArgumentNullException.ThrowIfNull(response); return new InstanceLifecycleResponseDto { CommandId = response.CommandId, InstanceUniqueName = response.InstanceUniqueName, Success = response.Success, ErrorMessage = OrEmpty(response.ErrorMessage), Timestamp = Ts(response.Timestamp) }; } /// Rehydrates an from the wire. /// The wire message to decode. /// The decoded reply record. public static InstanceLifecycleResponse FromProto(InstanceLifecycleResponseDto dto) { ArgumentNullException.ThrowIfNull(dto); return new InstanceLifecycleResponse( dto.CommandId, dto.InstanceUniqueName, dto.Success, NullIfEmpty(dto.ErrorMessage), Dto(dto.Timestamp)); } /// Projects a onto the wire. /// The reply to project. /// The wire message. public static DeploymentStateQueryResponseDto ToProto(DeploymentStateQueryResponse response) { ArgumentNullException.ThrowIfNull(response); return new DeploymentStateQueryResponseDto { CorrelationId = response.CorrelationId, InstanceUniqueName = response.InstanceUniqueName, IsDeployed = response.IsDeployed, AppliedDeploymentId = OrEmpty(response.AppliedDeploymentId), AppliedRevisionHash = OrEmpty(response.AppliedRevisionHash), Timestamp = Ts(response.Timestamp) }; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded reply record. public static DeploymentStateQueryResponse FromProto(DeploymentStateQueryResponseDto dto) { ArgumentNullException.ThrowIfNull(dto); return new DeploymentStateQueryResponse( dto.CorrelationId, dto.InstanceUniqueName, dto.IsDeployed, NullIfEmpty(dto.AppliedDeploymentId), NullIfEmpty(dto.AppliedRevisionHash), Dto(dto.Timestamp)); } /// Projects an onto the wire. /// The reply to project. /// The wire message. public static ArtifactDeploymentResponseDto ToProto(ArtifactDeploymentResponse response) { ArgumentNullException.ThrowIfNull(response); var dto = new ArtifactDeploymentResponseDto { DeploymentId = response.DeploymentId, SiteId = response.SiteId, Success = response.Success, ErrorMessage = OrEmpty(response.ErrorMessage), Timestamp = Ts(response.Timestamp) }; if (response.SecretsApplied.HasValue) { dto.SecretsApplied = response.SecretsApplied.Value; } if (response.SecretsRemoved is { Count: > 0 }) { dto.SecretsRemoved.AddRange(response.SecretsRemoved); } return dto; } /// Rehydrates an from the wire. /// The wire message to decode. /// The decoded reply record. public static ArtifactDeploymentResponse FromProto(ArtifactDeploymentResponseDto dto) { ArgumentNullException.ThrowIfNull(dto); return new ArtifactDeploymentResponse( dto.DeploymentId, dto.SiteId, dto.Success, NullIfEmpty(dto.ErrorMessage), Dto(dto.Timestamp), dto.SecretsApplied, dto.SecretsRemoved.Count > 0 ? dto.SecretsRemoved.ToList() : null); } // ───────────────────────────────────────────────────────────────────── // RPC 2 — ExecuteOpcUa: envelopes // ───────────────────────────────────────────────────────────────────── /// Packs an OPC UA-group command into its request envelope. /// One of the eight OPC UA-group commands. /// The populated request envelope. /// The command does not belong to this group. public static OpcUaRequest ToOpcUaRequest(object command) { ArgumentNullException.ThrowIfNull(command); return command switch { BrowseNodeCommand c => new OpcUaRequest { BrowseNode = ToProto(c) }, SearchAddressSpaceCommand c => new OpcUaRequest { SearchAddressSpace = ToProto(c) }, ReadTagValuesCommand c => new OpcUaRequest { ReadTagValues = ToProto(c) }, VerifyEndpointCommand c => new OpcUaRequest { VerifyEndpoint = ToProto(c) }, TrustServerCertCommand c => new OpcUaRequest { TrustServerCert = ToProto(c) }, ListServerCertsCommand c => new OpcUaRequest { ListServerCerts = ToProto(c) }, RemoveServerCertCommand c => new OpcUaRequest { RemoveServerCert = ToProto(c) }, WriteTagRequest c => new OpcUaRequest { WriteTag = ToProto(c) }, _ => throw NotInGroup(command, SiteCommandGroup.OpcUa) }; } /// Unpacks an OPC UA-group request envelope back to its command record. /// The request envelope received on the wire. /// The decoded command record. /// The envelope carries no case this build knows. public static object FromOpcUaRequest(OpcUaRequest request) { ArgumentNullException.ThrowIfNull(request); return request.CommandCase switch { OpcUaRequest.CommandOneofCase.BrowseNode => FromProto(request.BrowseNode), OpcUaRequest.CommandOneofCase.SearchAddressSpace => FromProto(request.SearchAddressSpace), OpcUaRequest.CommandOneofCase.ReadTagValues => FromProto(request.ReadTagValues), OpcUaRequest.CommandOneofCase.VerifyEndpoint => FromProto(request.VerifyEndpoint), OpcUaRequest.CommandOneofCase.TrustServerCert => FromProto(request.TrustServerCert), OpcUaRequest.CommandOneofCase.ListServerCerts => FromProto(request.ListServerCerts), OpcUaRequest.CommandOneofCase.RemoveServerCert => FromProto(request.RemoveServerCert), OpcUaRequest.CommandOneofCase.WriteTag => FromProto(request.WriteTag), _ => throw UnknownCase(request.CommandCase, nameof(OpcUaRequest)) }; } /// Packs an OPC UA-group reply into its reply envelope. /// One of the six OPC UA-group reply records. /// The populated reply envelope. /// The reply does not belong to this group. public static OpcUaReply ToOpcUaReply(object reply) { ArgumentNullException.ThrowIfNull(reply); return reply switch { BrowseNodeResult r => new OpcUaReply { BrowseNode = ToProto(r) }, SearchAddressSpaceResult r => new OpcUaReply { SearchAddressSpace = ToProto(r) }, ReadTagValuesResult r => new OpcUaReply { ReadTagValues = ToProto(r) }, VerifyEndpointResult r => new OpcUaReply { VerifyEndpoint = ToProto(r) }, CertTrustResult r => new OpcUaReply { CertTrust = ToProto(r) }, WriteTagResponse r => new OpcUaReply { WriteTag = ToProto(r) }, _ => throw NotInGroup(reply, SiteCommandGroup.OpcUa) }; } /// Unpacks an OPC UA-group reply envelope back to its reply record. /// The reply envelope received on the wire. /// The decoded reply record. /// The envelope carries no case this build knows. public static object FromOpcUaReply(OpcUaReply reply) { ArgumentNullException.ThrowIfNull(reply); return reply.ReplyCase switch { OpcUaReply.ReplyOneofCase.BrowseNode => FromProto(reply.BrowseNode), OpcUaReply.ReplyOneofCase.SearchAddressSpace => FromProto(reply.SearchAddressSpace), OpcUaReply.ReplyOneofCase.ReadTagValues => FromProto(reply.ReadTagValues), OpcUaReply.ReplyOneofCase.VerifyEndpoint => FromProto(reply.VerifyEndpoint), OpcUaReply.ReplyOneofCase.CertTrust => FromProto(reply.CertTrust), OpcUaReply.ReplyOneofCase.WriteTag => FromProto(reply.WriteTag), _ => throw UnknownCase(reply.ReplyCase, nameof(OpcUaReply)) }; } // ── OPC UA commands ── /// Projects a onto the wire. /// The command to project. /// The wire message. public static BrowseNodeCommandDto ToProto(BrowseNodeCommand command) { ArgumentNullException.ThrowIfNull(command); return new BrowseNodeCommandDto { ConnectionName = command.ConnectionName, ParentNodeId = OrEmpty(command.ParentNodeId), ContinuationToken = OrEmpty(command.ContinuationToken), SiteIdentifier = OrEmpty(command.SiteIdentifier) }; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded command record. public static BrowseNodeCommand FromProto(BrowseNodeCommandDto dto) { ArgumentNullException.ThrowIfNull(dto); return new BrowseNodeCommand( dto.ConnectionName, NullIfEmpty(dto.ParentNodeId), NullIfEmpty(dto.ContinuationToken), NullIfEmpty(dto.SiteIdentifier)); } /// Projects a onto the wire. /// The command to project. /// The wire message. public static SearchAddressSpaceCommandDto ToProto(SearchAddressSpaceCommand command) { ArgumentNullException.ThrowIfNull(command); return new SearchAddressSpaceCommandDto { ConnectionName = command.ConnectionName, Query = command.Query, MaxDepth = command.MaxDepth, MaxResults = command.MaxResults, SiteIdentifier = OrEmpty(command.SiteIdentifier) }; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded command record. public static SearchAddressSpaceCommand FromProto(SearchAddressSpaceCommandDto dto) { ArgumentNullException.ThrowIfNull(dto); return new SearchAddressSpaceCommand( dto.ConnectionName, dto.Query, dto.MaxDepth, dto.MaxResults, NullIfEmpty(dto.SiteIdentifier)); } /// Projects a onto the wire. /// The command to project. /// The wire message. public static ReadTagValuesCommandDto ToProto(ReadTagValuesCommand command) { ArgumentNullException.ThrowIfNull(command); var dto = new ReadTagValuesCommandDto { ConnectionName = command.ConnectionName }; dto.TagPaths.AddRange(command.TagPaths); return dto; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded command record. public static ReadTagValuesCommand FromProto(ReadTagValuesCommandDto dto) { ArgumentNullException.ThrowIfNull(dto); return new ReadTagValuesCommand(dto.ConnectionName, dto.TagPaths.ToList()); } /// Projects a onto the wire. /// The command to project. /// The wire message. public static VerifyEndpointCommandDto ToProto(VerifyEndpointCommand command) { ArgumentNullException.ThrowIfNull(command); return new VerifyEndpointCommandDto { ConnectionName = command.ConnectionName, Protocol = command.Protocol, ConfigJson = command.ConfigJson, SiteIdentifier = OrEmpty(command.SiteIdentifier) }; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded command record. public static VerifyEndpointCommand FromProto(VerifyEndpointCommandDto dto) { ArgumentNullException.ThrowIfNull(dto); return new VerifyEndpointCommand( dto.ConnectionName, dto.Protocol, dto.ConfigJson, NullIfEmpty(dto.SiteIdentifier)); } /// Projects a onto the wire. /// The command to project. /// The wire message. public static TrustServerCertCommandDto ToProto(TrustServerCertCommand command) { ArgumentNullException.ThrowIfNull(command); return new TrustServerCertCommandDto { ConnectionName = command.ConnectionName, DerBase64 = command.DerBase64, Thumbprint = command.Thumbprint, SiteIdentifier = OrEmpty(command.SiteIdentifier) }; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded command record. public static TrustServerCertCommand FromProto(TrustServerCertCommandDto dto) { ArgumentNullException.ThrowIfNull(dto); return new TrustServerCertCommand( dto.ConnectionName, dto.DerBase64, dto.Thumbprint, NullIfEmpty(dto.SiteIdentifier)); } /// Projects a onto the wire. /// The command to project. /// The wire message. public static ListServerCertsCommandDto ToProto(ListServerCertsCommand command) { ArgumentNullException.ThrowIfNull(command); return new ListServerCertsCommandDto { SiteIdentifier = OrEmpty(command.SiteIdentifier) }; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded command record. public static ListServerCertsCommand FromProto(ListServerCertsCommandDto dto) { ArgumentNullException.ThrowIfNull(dto); return new ListServerCertsCommand(NullIfEmpty(dto.SiteIdentifier)); } /// Projects a onto the wire. /// The command to project. /// The wire message. public static RemoveServerCertCommandDto ToProto(RemoveServerCertCommand command) { ArgumentNullException.ThrowIfNull(command); return new RemoveServerCertCommandDto { Thumbprint = command.Thumbprint, SiteIdentifier = OrEmpty(command.SiteIdentifier) }; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded command record. public static RemoveServerCertCommand FromProto(RemoveServerCertCommandDto dto) { ArgumentNullException.ThrowIfNull(dto); return new RemoveServerCertCommand(dto.Thumbprint, NullIfEmpty(dto.SiteIdentifier)); } /// Projects a onto the wire. /// The request to project. /// The wire message. public static WriteTagRequestDto ToProto(WriteTagRequest request) { ArgumentNullException.ThrowIfNull(request); return new WriteTagRequestDto { CorrelationId = request.CorrelationId, ConnectionName = request.ConnectionName, TagPath = request.TagPath, Value = LooseValueCodec.ToProtoOrNull(request.Value), Timestamp = Ts(request.Timestamp) }; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded request record. public static WriteTagRequest FromProto(WriteTagRequestDto dto) { ArgumentNullException.ThrowIfNull(dto); return new WriteTagRequest( dto.CorrelationId, dto.ConnectionName, dto.TagPath, LooseValueCodec.FromProto(dto.Value), Dto(dto.Timestamp)); } // ── OPC UA replies ── /// Projects a onto the wire. /// The reply to project. /// The wire message. public static BrowseNodeResultDto ToProto(BrowseNodeResult result) { ArgumentNullException.ThrowIfNull(result); var dto = new BrowseNodeResultDto { Truncated = result.Truncated, ContinuationToken = OrEmpty(result.ContinuationToken) }; dto.Children.AddRange(result.Children.Select(ToProto)); if (result.Failure is not null) { dto.Failure = ToProto(result.Failure); } return dto; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded reply record. public static BrowseNodeResult FromProto(BrowseNodeResultDto dto) { ArgumentNullException.ThrowIfNull(dto); return new BrowseNodeResult( dto.Children.Select(FromProto).ToList(), dto.Truncated, dto.Failure is null ? null : FromProto(dto.Failure), NullIfEmpty(dto.ContinuationToken)); } /// Projects a onto the wire. /// The address-space node to project. /// The wire message. public static BrowseNodeDto ToProto(BrowseNode node) { ArgumentNullException.ThrowIfNull(node); var dto = new BrowseNodeDto { NodeId = node.NodeId, DisplayName = node.DisplayName, NodeClass = ToProto(node.NodeClass), HasChildren = node.HasChildren, DataType = OrEmpty(node.DataType) }; if (node.ValueRank.HasValue) { dto.ValueRank = node.ValueRank.Value; } if (node.Writable.HasValue) { dto.Writable = node.Writable.Value; } return dto; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded address-space node. public static BrowseNode FromProto(BrowseNodeDto dto) { ArgumentNullException.ThrowIfNull(dto); return new BrowseNode( dto.NodeId, dto.DisplayName, FromProto(dto.NodeClass), dto.HasChildren, NullIfEmpty(dto.DataType), dto.ValueRank, dto.Writable); } /// Projects a onto the wire. /// The structured browse failure to project. /// The wire message. public static BrowseFailureDto ToProto(BrowseFailure failure) { ArgumentNullException.ThrowIfNull(failure); return new BrowseFailureDto { Kind = ToProto(failure.Kind), Message = failure.Message }; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded structured browse failure. public static BrowseFailure FromProto(BrowseFailureDto dto) { ArgumentNullException.ThrowIfNull(dto); return new BrowseFailure(FromProto(dto.Kind), dto.Message); } /// Projects a onto the wire. /// The reply to project. /// The wire message. public static SearchAddressSpaceResultDto ToProto(SearchAddressSpaceResult result) { ArgumentNullException.ThrowIfNull(result); var dto = new SearchAddressSpaceResultDto { CapReached = result.CapReached }; dto.Matches.AddRange(result.Matches.Select(ToProto)); if (result.Failure is not null) { dto.Failure = ToProto(result.Failure); } return dto; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded reply record. public static SearchAddressSpaceResult FromProto(SearchAddressSpaceResultDto dto) { ArgumentNullException.ThrowIfNull(dto); return new SearchAddressSpaceResult( dto.Matches.Select(FromProto).ToList(), dto.CapReached, dto.Failure is null ? null : FromProto(dto.Failure)); } /// Projects an onto the wire. /// The search match to project. /// The wire message. public static AddressSpaceMatchDto ToProto(AddressSpaceMatch match) { ArgumentNullException.ThrowIfNull(match); return new AddressSpaceMatchDto { Node = ToProto(match.Node), Path = match.Path }; } /// Rehydrates an from the wire. /// The wire message to decode. /// The decoded search match. public static AddressSpaceMatch FromProto(AddressSpaceMatchDto dto) { ArgumentNullException.ThrowIfNull(dto); return new AddressSpaceMatch(FromProto(dto.Node), dto.Path); } /// Projects a onto the wire. /// The reply to project. /// The wire message. public static ReadTagValuesResultDto ToProto(ReadTagValuesResult result) { ArgumentNullException.ThrowIfNull(result); var dto = new ReadTagValuesResultDto(); dto.Outcomes.AddRange(result.Outcomes.Select(ToProto)); if (result.Failure is not null) { dto.Failure = ToProto(result.Failure); } return dto; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded reply record. public static ReadTagValuesResult FromProto(ReadTagValuesResultDto dto) { ArgumentNullException.ThrowIfNull(dto); return new ReadTagValuesResult( dto.Outcomes.Select(FromProto).ToList(), dto.Failure is null ? null : FromProto(dto.Failure)); } /// Projects a onto the wire. /// The per-tag read outcome to project. /// The wire message. public static TagReadOutcomeDto ToProto(TagReadOutcome outcome) { ArgumentNullException.ThrowIfNull(outcome); return new TagReadOutcomeDto { TagPath = outcome.TagPath, Success = outcome.Success, Value = LooseValueCodec.ToProtoOrNull(outcome.Value), Quality = outcome.Quality, Timestamp = Ts(outcome.Timestamp), ErrorMessage = OrEmpty(outcome.ErrorMessage) }; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded per-tag read outcome. public static TagReadOutcome FromProto(TagReadOutcomeDto dto) { ArgumentNullException.ThrowIfNull(dto); return new TagReadOutcome( dto.TagPath, dto.Success, LooseValueCodec.FromProto(dto.Value), dto.Quality, Dto(dto.Timestamp), NullIfEmpty(dto.ErrorMessage)); } /// Projects a onto the wire. /// The connection-level read failure to project. /// The wire message. public static ReadTagValuesFailureDto ToProto(ReadTagValuesFailure failure) { ArgumentNullException.ThrowIfNull(failure); return new ReadTagValuesFailureDto { Kind = ToProto(failure.Kind), Message = failure.Message }; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded connection-level read failure. public static ReadTagValuesFailure FromProto(ReadTagValuesFailureDto dto) { ArgumentNullException.ThrowIfNull(dto); return new ReadTagValuesFailure(FromProto(dto.Kind), dto.Message); } /// Projects a onto the wire. /// The reply to project. /// The wire message. public static VerifyEndpointResultDto ToProto(VerifyEndpointResult result) { ArgumentNullException.ThrowIfNull(result); var dto = new VerifyEndpointResultDto { Success = result.Success, Error = OrEmpty(result.Error) }; if (result.FailureKind.HasValue) { dto.FailureKind = new VerifyFailureKindValue { Value = ToProto(result.FailureKind.Value) }; } if (result.Cert is not null) { dto.Cert = ToProto(result.Cert); } return dto; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded reply record. public static VerifyEndpointResult FromProto(VerifyEndpointResultDto dto) { ArgumentNullException.ThrowIfNull(dto); return new VerifyEndpointResult( dto.Success, dto.FailureKind is null ? null : FromProto(dto.FailureKind.Value), NullIfEmpty(dto.Error), dto.Cert is null ? null : FromProto(dto.Cert)); } /// Projects a onto the wire. /// The captured server certificate to project. /// The wire message. public static ServerCertInfoDto ToProto(ServerCertInfo cert) { ArgumentNullException.ThrowIfNull(cert); return new ServerCertInfoDto { Thumbprint = cert.Thumbprint, Subject = cert.Subject, Issuer = cert.Issuer, NotBeforeUtc = TsUtc(cert.NotBeforeUtc), NotAfterUtc = TsUtc(cert.NotAfterUtc), DerBase64 = cert.DerBase64 }; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded captured server certificate. public static ServerCertInfo FromProto(ServerCertInfoDto dto) { ArgumentNullException.ThrowIfNull(dto); return new ServerCertInfo( dto.Thumbprint, dto.Subject, dto.Issuer, DtUtc(dto.NotBeforeUtc), DtUtc(dto.NotAfterUtc), dto.DerBase64); } /// Projects a onto the wire. /// A null (trust/remove) leaves the list wrapper unset. /// The reply to project. /// The wire message. public static CertTrustResultDto ToProto(CertTrustResult result) { ArgumentNullException.ThrowIfNull(result); var dto = new CertTrustResultDto { Success = result.Success, Error = OrEmpty(result.Error) }; if (result.Certs is not null) { var list = new TrustedCertInfoListDto(); list.Items.AddRange(result.Certs.Select(ToProto)); dto.Certs = list; } return dto; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded reply record. public static CertTrustResult FromProto(CertTrustResultDto dto) { ArgumentNullException.ThrowIfNull(dto); return new CertTrustResult( dto.Success, NullIfEmpty(dto.Error), dto.Certs is null ? null : dto.Certs.Items.Select(FromProto).ToList()); } /// Projects a onto the wire. /// The PKI store entry to project. /// The wire message. public static TrustedCertInfoDto ToProto(TrustedCertInfo cert) { ArgumentNullException.ThrowIfNull(cert); return new TrustedCertInfoDto { Thumbprint = cert.Thumbprint, Subject = cert.Subject, Issuer = cert.Issuer, NotBeforeUtc = TsUtc(cert.NotBeforeUtc), NotAfterUtc = TsUtc(cert.NotAfterUtc), Rejected = cert.Rejected }; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded PKI store entry. public static TrustedCertInfo FromProto(TrustedCertInfoDto dto) { ArgumentNullException.ThrowIfNull(dto); return new TrustedCertInfo( dto.Thumbprint, dto.Subject, dto.Issuer, DtUtc(dto.NotBeforeUtc), DtUtc(dto.NotAfterUtc), dto.Rejected); } /// Projects a onto the wire. /// The reply to project. /// The wire message. public static WriteTagResponseDto ToProto(WriteTagResponse response) { ArgumentNullException.ThrowIfNull(response); return new WriteTagResponseDto { CorrelationId = response.CorrelationId, Success = response.Success, ErrorMessage = OrEmpty(response.ErrorMessage), Timestamp = Ts(response.Timestamp) }; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded reply record. public static WriteTagResponse FromProto(WriteTagResponseDto dto) { ArgumentNullException.ThrowIfNull(dto); return new WriteTagResponse( dto.CorrelationId, dto.Success, NullIfEmpty(dto.ErrorMessage), Dto(dto.Timestamp)); } // ───────────────────────────────────────────────────────────────────── // RPC 3 — ExecuteQuery: envelopes // ───────────────────────────────────────────────────────────────────── /// Packs a query-group command into its request envelope. /// One of the four query-group commands. /// The populated request envelope. /// The command does not belong to this group. public static QueryRequest ToQueryRequest(object command) { ArgumentNullException.ThrowIfNull(command); return command switch { EventLogQueryRequest c => new QueryRequest { EventLogQuery = ToProto(c) }, DebugSnapshotRequest c => new QueryRequest { DebugSnapshot = ToProto(c) }, SubscribeDebugViewRequest c => new QueryRequest { SubscribeDebugView = ToProto(c) }, UnsubscribeDebugViewRequest c => new QueryRequest { UnsubscribeDebugView = ToProto(c) }, _ => throw NotInGroup(command, SiteCommandGroup.Query) }; } /// Unpacks a query-group request envelope back to its command record. /// The request envelope received on the wire. /// The decoded command record. /// The envelope carries no case this build knows. public static object FromQueryRequest(QueryRequest request) { ArgumentNullException.ThrowIfNull(request); return request.CommandCase switch { QueryRequest.CommandOneofCase.EventLogQuery => FromProto(request.EventLogQuery), QueryRequest.CommandOneofCase.DebugSnapshot => FromProto(request.DebugSnapshot), QueryRequest.CommandOneofCase.SubscribeDebugView => FromProto(request.SubscribeDebugView), QueryRequest.CommandOneofCase.UnsubscribeDebugView => FromProto(request.UnsubscribeDebugView), _ => throw UnknownCase(request.CommandCase, nameof(QueryRequest)) }; } /// /// Packs a query-group reply into its reply envelope. Pass /// for the unsubscribe path, which is a /// Tell today and carries no payload. /// /// One of the query-group reply records. /// The populated reply envelope. /// The reply does not belong to this group. public static QueryReply ToQueryReply(object reply) { ArgumentNullException.ThrowIfNull(reply); return reply switch { EventLogQueryResponse r => new QueryReply { EventLogQuery = ToProto(r) }, DebugViewSnapshot r => new QueryReply { DebugViewSnapshot = ToProto(r) }, UnsubscribeDebugViewAck => new QueryReply { UnsubscribeDebugView = new UnsubscribeDebugViewAckDto() }, _ => throw NotInGroup(reply, SiteCommandGroup.Query) }; } /// Unpacks a query-group reply envelope back to its reply record. /// The reply envelope received on the wire. /// The decoded reply record. /// The envelope carries no case this build knows. public static object FromQueryReply(QueryReply reply) { ArgumentNullException.ThrowIfNull(reply); return reply.ReplyCase switch { QueryReply.ReplyOneofCase.EventLogQuery => FromProto(reply.EventLogQuery), QueryReply.ReplyOneofCase.DebugViewSnapshot => FromProto(reply.DebugViewSnapshot), QueryReply.ReplyOneofCase.UnsubscribeDebugView => UnsubscribeDebugViewAck.Instance, _ => throw UnknownCase(reply.ReplyCase, nameof(QueryReply)) }; } // ── Query commands ── /// Projects an onto the wire. /// The request to project. /// The wire message. public static EventLogQueryRequestDto ToProto(EventLogQueryRequest request) { ArgumentNullException.ThrowIfNull(request); var dto = new EventLogQueryRequestDto { CorrelationId = request.CorrelationId, SiteId = request.SiteId, EventType = OrEmpty(request.EventType), Severity = OrEmpty(request.Severity), InstanceId = OrEmpty(request.InstanceId), KeywordFilter = OrEmpty(request.KeywordFilter), ContinuationToken = OrEmpty(request.ContinuationToken), PageSize = request.PageSize, Timestamp = Ts(request.Timestamp) }; if (request.From.HasValue) { dto.From = Ts(request.From.Value); } if (request.To.HasValue) { dto.To = Ts(request.To.Value); } return dto; } /// Rehydrates an from the wire. /// The wire message to decode. /// The decoded request record. public static EventLogQueryRequest FromProto(EventLogQueryRequestDto dto) { ArgumentNullException.ThrowIfNull(dto); return new EventLogQueryRequest( dto.CorrelationId, dto.SiteId, DtoOrNull(dto.From), DtoOrNull(dto.To), NullIfEmpty(dto.EventType), NullIfEmpty(dto.Severity), NullIfEmpty(dto.InstanceId), NullIfEmpty(dto.KeywordFilter), NullIfEmpty(dto.ContinuationToken), dto.PageSize, Dto(dto.Timestamp)); } /// Projects a onto the wire. /// The request to project. /// The wire message. public static DebugSnapshotRequestDto ToProto(DebugSnapshotRequest request) { ArgumentNullException.ThrowIfNull(request); return new DebugSnapshotRequestDto { InstanceUniqueName = request.InstanceUniqueName, CorrelationId = request.CorrelationId, // Additive field 3 — proto3 false default means an older site that does // not know the field simply returns the full snapshot (correct, just fatter). AlarmsOnly = request.AlarmsOnly }; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded request record. public static DebugSnapshotRequest FromProto(DebugSnapshotRequestDto dto) { ArgumentNullException.ThrowIfNull(dto); return new DebugSnapshotRequest(dto.InstanceUniqueName, dto.CorrelationId, dto.AlarmsOnly); } /// Projects a onto the wire. /// The request to project. /// The wire message. public static SubscribeDebugViewRequestDto ToProto(SubscribeDebugViewRequest request) { ArgumentNullException.ThrowIfNull(request); return new SubscribeDebugViewRequestDto { InstanceUniqueName = request.InstanceUniqueName, CorrelationId = request.CorrelationId }; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded request record. public static SubscribeDebugViewRequest FromProto(SubscribeDebugViewRequestDto dto) { ArgumentNullException.ThrowIfNull(dto); return new SubscribeDebugViewRequest(dto.InstanceUniqueName, dto.CorrelationId); } /// Projects an onto the wire. /// The request to project. /// The wire message. public static UnsubscribeDebugViewRequestDto ToProto(UnsubscribeDebugViewRequest request) { ArgumentNullException.ThrowIfNull(request); return new UnsubscribeDebugViewRequestDto { InstanceUniqueName = request.InstanceUniqueName, CorrelationId = request.CorrelationId }; } /// Rehydrates an from the wire. /// The wire message to decode. /// The decoded request record. public static UnsubscribeDebugViewRequest FromProto(UnsubscribeDebugViewRequestDto dto) { ArgumentNullException.ThrowIfNull(dto); return new UnsubscribeDebugViewRequest(dto.InstanceUniqueName, dto.CorrelationId); } // ── Query replies ── /// Projects an onto the wire. /// The reply to project. /// The wire message. public static EventLogQueryResponseDto ToProto(EventLogQueryResponse response) { ArgumentNullException.ThrowIfNull(response); var dto = new EventLogQueryResponseDto { CorrelationId = response.CorrelationId, SiteId = response.SiteId, ContinuationToken = OrEmpty(response.ContinuationToken), HasMore = response.HasMore, Success = response.Success, ErrorMessage = OrEmpty(response.ErrorMessage), Timestamp = Ts(response.Timestamp) }; dto.Entries.AddRange(response.Entries.Select(ToProto)); return dto; } /// Rehydrates an from the wire. /// The wire message to decode. /// The decoded reply record. public static EventLogQueryResponse FromProto(EventLogQueryResponseDto dto) { ArgumentNullException.ThrowIfNull(dto); return new EventLogQueryResponse( dto.CorrelationId, dto.SiteId, dto.Entries.Select(FromProto).ToList(), NullIfEmpty(dto.ContinuationToken), dto.HasMore, dto.Success, NullIfEmpty(dto.ErrorMessage), Dto(dto.Timestamp)); } /// Projects an onto the wire. /// The event-log row to project. /// The wire message. public static EventLogEntryDto ToProto(EventLogEntry entry) { ArgumentNullException.ThrowIfNull(entry); return new EventLogEntryDto { Id = entry.Id, Timestamp = Ts(entry.Timestamp), EventType = entry.EventType, Severity = entry.Severity, InstanceId = OrEmpty(entry.InstanceId), Source = entry.Source, Message = entry.Message, Details = OrEmpty(entry.Details) }; } /// Rehydrates an from the wire. /// The wire message to decode. /// The decoded event-log row. public static EventLogEntry FromProto(EventLogEntryDto dto) { ArgumentNullException.ThrowIfNull(dto); return new EventLogEntry( dto.Id, Dto(dto.Timestamp), dto.EventType, dto.Severity, NullIfEmpty(dto.InstanceId), dto.Source, dto.Message, NullIfEmpty(dto.Details)); } /// Projects a onto the wire. /// The reply to project. /// The wire message. public static DebugViewSnapshotDto ToProto(DebugViewSnapshot snapshot) { ArgumentNullException.ThrowIfNull(snapshot); var dto = new DebugViewSnapshotDto { InstanceUniqueName = snapshot.InstanceUniqueName, SnapshotTimestamp = Ts(snapshot.SnapshotTimestamp), InstanceNotFound = snapshot.InstanceNotFound }; dto.AttributeValues.AddRange(snapshot.AttributeValues.Select(ToProto)); dto.AlarmStates.AddRange(snapshot.AlarmStates.Select(ToProto)); return dto; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded reply record. public static DebugViewSnapshot FromProto(DebugViewSnapshotDto dto) { ArgumentNullException.ThrowIfNull(dto); return new DebugViewSnapshot( dto.InstanceUniqueName, dto.AttributeValues.Select(FromProto).ToList(), dto.AlarmStates.Select(FromProto).ToList(), Dto(dto.SnapshotTimestamp), dto.InstanceNotFound); } /// Projects an snapshot row onto the wire. /// The attribute snapshot row to project. /// The wire message. public static DebugAttributeValueDto ToProto(AttributeValueChanged value) { ArgumentNullException.ThrowIfNull(value); return new DebugAttributeValueDto { InstanceUniqueName = value.InstanceUniqueName, AttributePath = value.AttributePath, AttributeName = value.AttributeName, Value = LooseValueCodec.ToProtoOrNull(value.Value), Quality = value.Quality, Timestamp = Ts(value.Timestamp) }; } /// Rehydrates an snapshot row from the wire. /// The wire message to decode. /// The decoded attribute snapshot row. public static AttributeValueChanged FromProto(DebugAttributeValueDto dto) { ArgumentNullException.ThrowIfNull(dto); return new AttributeValueChanged( dto.InstanceUniqueName, dto.AttributePath, dto.AttributeName, LooseValueCodec.FromProto(dto.Value), dto.Quality, Dto(dto.Timestamp)); } /// Projects an snapshot row onto the wire. /// /// is only written when it differs /// from the value the record itself derives from State + Priority. /// The record stores the condition in a nullable backing field and synthesises /// the computed default on read, so writing it unconditionally would turn an /// implicit condition into an explicit one — indistinguishable through the /// property, but a difference under record equality. Omitting the redundant /// value keeps the round trip exact for computed alarms, which are the common /// case, and is information-preserving for the rest. /// /// The alarm snapshot row to project. /// The wire message. public static DebugAlarmStateDto ToProto(AlarmStateChanged alarm) { ArgumentNullException.ThrowIfNull(alarm); var dto = new DebugAlarmStateDto { InstanceUniqueName = alarm.InstanceUniqueName, AlarmName = alarm.AlarmName, State = ToProto(alarm.State), Priority = alarm.Priority, Timestamp = Ts(alarm.Timestamp), Level = ToProto(alarm.Level), Message = alarm.Message, Kind = ToProto(alarm.Kind), SourceReference = alarm.SourceReference, AlarmTypeName = alarm.AlarmTypeName, Category = alarm.Category, OperatorUser = alarm.OperatorUser, OperatorComment = alarm.OperatorComment, CurrentValue = alarm.CurrentValue, LimitValue = alarm.LimitValue, NativeSourceCanonicalName = alarm.NativeSourceCanonicalName, IsConfiguredPlaceholder = alarm.IsConfiguredPlaceholder, IsMesReportable = alarm.IsMesReportable }; if (alarm.Condition != AlarmConditionStateFactory.ForComputed(alarm.State, alarm.Priority)) { dto.Condition = ToProto(alarm.Condition); } if (alarm.OriginalRaiseTime.HasValue) { dto.OriginalRaiseTime = Ts(alarm.OriginalRaiseTime.Value); } if (alarm.AckTime.HasValue) { dto.AckTime = Ts(alarm.AckTime.Value); } return dto; } /// Rehydrates an snapshot row from the wire. /// The wire message to decode. /// The decoded alarm snapshot row. public static AlarmStateChanged FromProto(DebugAlarmStateDto dto) { ArgumentNullException.ThrowIfNull(dto); var alarm = new AlarmStateChanged( dto.InstanceUniqueName, dto.AlarmName, FromProto(dto.State), dto.Priority, Dto(dto.Timestamp)) { Level = FromProto(dto.Level), Message = dto.Message, Kind = FromProto(dto.Kind), SourceReference = dto.SourceReference, AlarmTypeName = dto.AlarmTypeName, Category = dto.Category, OperatorUser = dto.OperatorUser, OperatorComment = dto.OperatorComment, OriginalRaiseTime = DtoOrNull(dto.OriginalRaiseTime), CurrentValue = dto.CurrentValue, LimitValue = dto.LimitValue, NativeSourceCanonicalName = dto.NativeSourceCanonicalName, IsConfiguredPlaceholder = dto.IsConfiguredPlaceholder, IsMesReportable = dto.IsMesReportable, AckTime = DtoOrNull(dto.AckTime) }; // AlarmConditionStateDto carries Severity only; the raw priority (Gitea #129) travels as the // row's own Priority, which the site always sets equal to Condition.Priority, so restore it here. return dto.Condition is null ? alarm : alarm with { Condition = FromProto(dto.Condition) with { Priority = dto.Priority } }; } /// Projects an onto the wire. /// The unified alarm condition to project. /// The wire message. public static AlarmConditionStateDto ToProto(AlarmConditionState condition) { ArgumentNullException.ThrowIfNull(condition); var dto = new AlarmConditionStateDto { Active = condition.Active, Acknowledged = condition.Acknowledged, Shelve = ToProto(condition.Shelve), Suppressed = condition.Suppressed, Severity = condition.Severity }; if (condition.Confirmed.HasValue) { dto.Confirmed = condition.Confirmed.Value; } return dto; } /// Rehydrates an from the wire. /// The wire message to decode. /// The decoded unified alarm condition. public static AlarmConditionState FromProto(AlarmConditionStateDto dto) { ArgumentNullException.ThrowIfNull(dto); return new AlarmConditionState( dto.Active, dto.Acknowledged, dto.Confirmed, FromProto(dto.Shelve), dto.Suppressed, dto.Severity); } // ───────────────────────────────────────────────────────────────────── // RPC 4 — ExecuteParked: envelopes // ───────────────────────────────────────────────────────────────────── /// Packs a parked-group command into its request envelope. /// One of the five parked-group commands. /// The populated request envelope. /// The command does not belong to this group. public static ParkedRequest ToParkedRequest(object command) { ArgumentNullException.ThrowIfNull(command); return command switch { ParkedMessageQueryRequest c => new ParkedRequest { ParkedMessageQuery = ToProto(c) }, ParkedMessageRetryRequest c => new ParkedRequest { ParkedMessageRetry = ToProto(c) }, ParkedMessageDiscardRequest c => new ParkedRequest { ParkedMessageDiscard = ToProto(c) }, RetryParkedOperation c => new ParkedRequest { RetryParkedOperation = ToProto(c) }, DiscardParkedOperation c => new ParkedRequest { DiscardParkedOperation = ToProto(c) }, _ => throw NotInGroup(command, SiteCommandGroup.Parked) }; } /// Unpacks a parked-group request envelope back to its command record. /// The request envelope received on the wire. /// The decoded command record. /// The envelope carries no case this build knows. public static object FromParkedRequest(ParkedRequest request) { ArgumentNullException.ThrowIfNull(request); return request.CommandCase switch { ParkedRequest.CommandOneofCase.ParkedMessageQuery => FromProto(request.ParkedMessageQuery), ParkedRequest.CommandOneofCase.ParkedMessageRetry => FromProto(request.ParkedMessageRetry), ParkedRequest.CommandOneofCase.ParkedMessageDiscard => FromProto(request.ParkedMessageDiscard), ParkedRequest.CommandOneofCase.RetryParkedOperation => FromProto(request.RetryParkedOperation), ParkedRequest.CommandOneofCase.DiscardParkedOperation => FromProto(request.DiscardParkedOperation), _ => throw UnknownCase(request.CommandCase, nameof(ParkedRequest)) }; } /// Packs a parked-group reply into its reply envelope. /// One of the four parked-group reply records. /// The populated reply envelope. /// The reply does not belong to this group. public static ParkedReply ToParkedReply(object reply) { ArgumentNullException.ThrowIfNull(reply); return reply switch { ParkedMessageQueryResponse r => new ParkedReply { ParkedMessageQuery = ToProto(r) }, ParkedMessageRetryResponse r => new ParkedReply { ParkedMessageRetry = ToProto(r) }, ParkedMessageDiscardResponse r => new ParkedReply { ParkedMessageDiscard = ToProto(r) }, ParkedOperationActionAck r => new ParkedReply { ParkedOperationAction = ToProto(r) }, _ => throw NotInGroup(reply, SiteCommandGroup.Parked) }; } /// Unpacks a parked-group reply envelope back to its reply record. /// The reply envelope received on the wire. /// The decoded reply record. /// The envelope carries no case this build knows. public static object FromParkedReply(ParkedReply reply) { ArgumentNullException.ThrowIfNull(reply); return reply.ReplyCase switch { ParkedReply.ReplyOneofCase.ParkedMessageQuery => FromProto(reply.ParkedMessageQuery), ParkedReply.ReplyOneofCase.ParkedMessageRetry => FromProto(reply.ParkedMessageRetry), ParkedReply.ReplyOneofCase.ParkedMessageDiscard => FromProto(reply.ParkedMessageDiscard), ParkedReply.ReplyOneofCase.ParkedOperationAction => FromProto(reply.ParkedOperationAction), _ => throw UnknownCase(reply.ReplyCase, nameof(ParkedReply)) }; } // ── Parked commands ── /// Projects a onto the wire. /// The request to project. /// The wire message. public static ParkedMessageQueryRequestDto ToProto(ParkedMessageQueryRequest request) { ArgumentNullException.ThrowIfNull(request); return new ParkedMessageQueryRequestDto { CorrelationId = request.CorrelationId, SiteId = request.SiteId, PageNumber = request.PageNumber, PageSize = request.PageSize, Timestamp = Ts(request.Timestamp) }; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded request record. public static ParkedMessageQueryRequest FromProto(ParkedMessageQueryRequestDto dto) { ArgumentNullException.ThrowIfNull(dto); return new ParkedMessageQueryRequest( dto.CorrelationId, dto.SiteId, dto.PageNumber, dto.PageSize, Dto(dto.Timestamp)); } /// Projects a onto the wire. /// The request to project. /// The wire message. public static ParkedMessageRetryRequestDto ToProto(ParkedMessageRetryRequest request) { ArgumentNullException.ThrowIfNull(request); return new ParkedMessageRetryRequestDto { CorrelationId = request.CorrelationId, SiteId = request.SiteId, MessageId = request.MessageId, Timestamp = Ts(request.Timestamp) }; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded request record. public static ParkedMessageRetryRequest FromProto(ParkedMessageRetryRequestDto dto) { ArgumentNullException.ThrowIfNull(dto); return new ParkedMessageRetryRequest( dto.CorrelationId, dto.SiteId, dto.MessageId, Dto(dto.Timestamp)); } /// Projects a onto the wire. /// The request to project. /// The wire message. public static ParkedMessageDiscardRequestDto ToProto(ParkedMessageDiscardRequest request) { ArgumentNullException.ThrowIfNull(request); return new ParkedMessageDiscardRequestDto { CorrelationId = request.CorrelationId, SiteId = request.SiteId, MessageId = request.MessageId, Timestamp = Ts(request.Timestamp) }; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded request record. public static ParkedMessageDiscardRequest FromProto(ParkedMessageDiscardRequestDto dto) { ArgumentNullException.ThrowIfNull(dto); return new ParkedMessageDiscardRequest( dto.CorrelationId, dto.SiteId, dto.MessageId, Dto(dto.Timestamp)); } /// Projects a relay onto the wire. /// The relay command to project. /// The wire message. public static RetryParkedOperationDto ToProto(RetryParkedOperation command) { ArgumentNullException.ThrowIfNull(command); return new RetryParkedOperationDto { CorrelationId = command.CorrelationId, TrackedOperationId = command.TrackedOperationId.ToString() }; } /// Rehydrates a relay from the wire. /// The wire message to decode. /// The decoded relay command. public static RetryParkedOperation FromProto(RetryParkedOperationDto dto) { ArgumentNullException.ThrowIfNull(dto); return new RetryParkedOperation(dto.CorrelationId, TrackedId(dto.TrackedOperationId)); } /// Projects a relay onto the wire. /// The relay command to project. /// The wire message. public static DiscardParkedOperationDto ToProto(DiscardParkedOperation command) { ArgumentNullException.ThrowIfNull(command); return new DiscardParkedOperationDto { CorrelationId = command.CorrelationId, TrackedOperationId = command.TrackedOperationId.ToString() }; } /// Rehydrates a relay from the wire. /// The wire message to decode. /// The decoded relay command. public static DiscardParkedOperation FromProto(DiscardParkedOperationDto dto) { ArgumentNullException.ThrowIfNull(dto); return new DiscardParkedOperation(dto.CorrelationId, TrackedId(dto.TrackedOperationId)); } // ── Parked replies ── /// Projects a onto the wire. /// The reply to project. /// The wire message. public static ParkedMessageQueryResponseDto ToProto(ParkedMessageQueryResponse response) { ArgumentNullException.ThrowIfNull(response); var dto = new ParkedMessageQueryResponseDto { CorrelationId = response.CorrelationId, SiteId = response.SiteId, TotalCount = response.TotalCount, PageNumber = response.PageNumber, PageSize = response.PageSize, Success = response.Success, ErrorMessage = OrEmpty(response.ErrorMessage), Timestamp = Ts(response.Timestamp) }; dto.Messages.AddRange(response.Messages.Select(ToProto)); return dto; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded reply record. public static ParkedMessageQueryResponse FromProto(ParkedMessageQueryResponseDto dto) { ArgumentNullException.ThrowIfNull(dto); return new ParkedMessageQueryResponse( dto.CorrelationId, dto.SiteId, dto.Messages.Select(FromProto).ToList(), dto.TotalCount, dto.PageNumber, dto.PageSize, dto.Success, NullIfEmpty(dto.ErrorMessage), Dto(dto.Timestamp)); } /// Projects a onto the wire. /// The parked store-and-forward row to project. /// The wire message. public static ParkedMessageEntryDto ToProto(ParkedMessageEntry entry) { ArgumentNullException.ThrowIfNull(entry); return new ParkedMessageEntryDto { MessageId = entry.MessageId, TargetSystem = entry.TargetSystem, MethodName = entry.MethodName, ErrorMessage = entry.ErrorMessage, AttemptCount = entry.AttemptCount, OriginalTimestamp = Ts(entry.OriginalTimestamp), LastAttemptTimestamp = Ts(entry.LastAttemptTimestamp), MaxAttempts = entry.MaxAttempts, Category = ToProto(entry.Category), OriginInstance = OrEmpty(entry.OriginInstance) }; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded parked store-and-forward row. public static ParkedMessageEntry FromProto(ParkedMessageEntryDto dto) { ArgumentNullException.ThrowIfNull(dto); return new ParkedMessageEntry( dto.MessageId, dto.TargetSystem, dto.MethodName, dto.ErrorMessage, dto.AttemptCount, Dto(dto.OriginalTimestamp), Dto(dto.LastAttemptTimestamp), dto.MaxAttempts, FromProto(dto.Category), NullIfEmpty(dto.OriginInstance)); } /// Projects a onto the wire. /// The reply to project. /// The wire message. public static ParkedMessageRetryResponseDto ToProto(ParkedMessageRetryResponse response) { ArgumentNullException.ThrowIfNull(response); return new ParkedMessageRetryResponseDto { CorrelationId = response.CorrelationId, Success = response.Success, ErrorMessage = OrEmpty(response.ErrorMessage) }; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded reply record. public static ParkedMessageRetryResponse FromProto(ParkedMessageRetryResponseDto dto) { ArgumentNullException.ThrowIfNull(dto); return new ParkedMessageRetryResponse(dto.CorrelationId, dto.Success, NullIfEmpty(dto.ErrorMessage)); } /// Projects a onto the wire. /// The reply to project. /// The wire message. public static ParkedMessageDiscardResponseDto ToProto(ParkedMessageDiscardResponse response) { ArgumentNullException.ThrowIfNull(response); return new ParkedMessageDiscardResponseDto { CorrelationId = response.CorrelationId, Success = response.Success, ErrorMessage = OrEmpty(response.ErrorMessage) }; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded reply record. public static ParkedMessageDiscardResponse FromProto(ParkedMessageDiscardResponseDto dto) { ArgumentNullException.ThrowIfNull(dto); return new ParkedMessageDiscardResponse(dto.CorrelationId, dto.Success, NullIfEmpty(dto.ErrorMessage)); } /// Projects a onto the wire. /// The relay ack to project. /// The wire message. public static ParkedOperationActionAckDto ToProto(ParkedOperationActionAck ack) { ArgumentNullException.ThrowIfNull(ack); return new ParkedOperationActionAckDto { CorrelationId = ack.CorrelationId, Applied = ack.Applied, ErrorMessage = OrEmpty(ack.ErrorMessage) }; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded relay ack. public static ParkedOperationActionAck FromProto(ParkedOperationActionAckDto dto) { ArgumentNullException.ThrowIfNull(dto); return new ParkedOperationActionAck(dto.CorrelationId, dto.Applied, NullIfEmpty(dto.ErrorMessage)); } // ───────────────────────────────────────────────────────────────────── // RPC 5 — ExecuteRoute: envelopes // ───────────────────────────────────────────────────────────────────── /// Packs a route-group command into its request envelope. /// One of the four route-group commands. /// The populated request envelope. /// The command does not belong to this group. public static RouteRequest ToRouteRequest(object command) { ArgumentNullException.ThrowIfNull(command); return command switch { RouteToCallRequest c => new RouteRequest { RouteToCall = ToProto(c) }, RouteToGetAttributesRequest c => new RouteRequest { RouteToGetAttributes = ToProto(c) }, RouteToSetAttributesRequest c => new RouteRequest { RouteToSetAttributes = ToProto(c) }, RouteToWaitForAttributeRequest c => new RouteRequest { RouteToWaitForAttribute = ToProto(c) }, _ => throw NotInGroup(command, SiteCommandGroup.Route) }; } /// Unpacks a route-group request envelope back to its command record. /// The request envelope received on the wire. /// The decoded command record. /// The envelope carries no case this build knows. public static object FromRouteRequest(RouteRequest request) { ArgumentNullException.ThrowIfNull(request); return request.CommandCase switch { RouteRequest.CommandOneofCase.RouteToCall => FromProto(request.RouteToCall), RouteRequest.CommandOneofCase.RouteToGetAttributes => FromProto(request.RouteToGetAttributes), RouteRequest.CommandOneofCase.RouteToSetAttributes => FromProto(request.RouteToSetAttributes), RouteRequest.CommandOneofCase.RouteToWaitForAttribute => FromProto(request.RouteToWaitForAttribute), _ => throw UnknownCase(request.CommandCase, nameof(RouteRequest)) }; } /// Packs a route-group reply into its reply envelope. /// One of the four route-group reply records. /// The populated reply envelope. /// The reply does not belong to this group. public static RouteReply ToRouteReply(object reply) { ArgumentNullException.ThrowIfNull(reply); return reply switch { RouteToCallResponse r => new RouteReply { RouteToCall = ToProto(r) }, RouteToGetAttributesResponse r => new RouteReply { RouteToGetAttributes = ToProto(r) }, RouteToSetAttributesResponse r => new RouteReply { RouteToSetAttributes = ToProto(r) }, RouteToWaitForAttributeResponse r => new RouteReply { RouteToWaitForAttribute = ToProto(r) }, _ => throw NotInGroup(reply, SiteCommandGroup.Route) }; } /// Unpacks a route-group reply envelope back to its reply record. /// The reply envelope received on the wire. /// The decoded reply record. /// The envelope carries no case this build knows. public static object FromRouteReply(RouteReply reply) { ArgumentNullException.ThrowIfNull(reply); return reply.ReplyCase switch { RouteReply.ReplyOneofCase.RouteToCall => FromProto(reply.RouteToCall), RouteReply.ReplyOneofCase.RouteToGetAttributes => FromProto(reply.RouteToGetAttributes), RouteReply.ReplyOneofCase.RouteToSetAttributes => FromProto(reply.RouteToSetAttributes), RouteReply.ReplyOneofCase.RouteToWaitForAttribute => FromProto(reply.RouteToWaitForAttribute), _ => throw UnknownCase(reply.ReplyCase, nameof(RouteReply)) }; } // ── Route commands ── /// Projects a onto the wire. /// The request to project. /// The wire message. public static RouteToCallRequestDto ToProto(RouteToCallRequest request) { ArgumentNullException.ThrowIfNull(request); return new RouteToCallRequestDto { CorrelationId = request.CorrelationId, InstanceUniqueName = request.InstanceUniqueName, ScriptName = request.ScriptName, Parameters = LooseValueCodec.ToProtoMapOrNull(request.Parameters), Timestamp = Ts(request.Timestamp), ParentExecutionId = GuidToWire(request.ParentExecutionId), Trace = request.Trace }; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded request record. public static RouteToCallRequest FromProto(RouteToCallRequestDto dto) { ArgumentNullException.ThrowIfNull(dto); return new RouteToCallRequest( dto.CorrelationId, dto.InstanceUniqueName, dto.ScriptName, LooseValueCodec.FromProtoMapOrNull(dto.Parameters), Dto(dto.Timestamp), GuidFromWire(dto.ParentExecutionId), dto.Trace); } /// Projects a onto the wire. /// The request to project. /// The wire message. public static RouteToGetAttributesRequestDto ToProto(RouteToGetAttributesRequest request) { ArgumentNullException.ThrowIfNull(request); var dto = new RouteToGetAttributesRequestDto { CorrelationId = request.CorrelationId, InstanceUniqueName = request.InstanceUniqueName, Timestamp = Ts(request.Timestamp), ParentExecutionId = GuidToWire(request.ParentExecutionId) }; dto.AttributeNames.AddRange(request.AttributeNames); return dto; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded request record. public static RouteToGetAttributesRequest FromProto(RouteToGetAttributesRequestDto dto) { ArgumentNullException.ThrowIfNull(dto); return new RouteToGetAttributesRequest( dto.CorrelationId, dto.InstanceUniqueName, dto.AttributeNames.ToList(), Dto(dto.Timestamp), GuidFromWire(dto.ParentExecutionId)); } /// Projects a onto the wire. /// The request to project. /// The wire message. public static RouteToSetAttributesRequestDto ToProto(RouteToSetAttributesRequest request) { ArgumentNullException.ThrowIfNull(request); var dto = new RouteToSetAttributesRequestDto { CorrelationId = request.CorrelationId, InstanceUniqueName = request.InstanceUniqueName, Timestamp = Ts(request.Timestamp), ParentExecutionId = GuidToWire(request.ParentExecutionId), Trace = request.Trace }; foreach (var (key, value) in request.AttributeValues) { dto.AttributeValues[key] = value; } return dto; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded request record. public static RouteToSetAttributesRequest FromProto(RouteToSetAttributesRequestDto dto) { ArgumentNullException.ThrowIfNull(dto); return new RouteToSetAttributesRequest( dto.CorrelationId, dto.InstanceUniqueName, dto.AttributeValues.ToDictionary(kv => kv.Key, kv => kv.Value), Dto(dto.Timestamp), GuidFromWire(dto.ParentExecutionId), dto.Trace); } /// Projects a onto the wire. /// The request to project. /// The wire message. public static RouteToWaitForAttributeRequestDto ToProto(RouteToWaitForAttributeRequest request) { ArgumentNullException.ThrowIfNull(request); var dto = new RouteToWaitForAttributeRequestDto { CorrelationId = request.CorrelationId, InstanceUniqueName = request.InstanceUniqueName, AttributeName = request.AttributeName, Timeout = Duration.FromTimeSpan(request.Timeout), Timestamp = Ts(request.Timestamp), ParentExecutionId = GuidToWire(request.ParentExecutionId), RequireGoodQuality = request.RequireGoodQuality }; if (request.TargetValueEncoded is not null) { dto.TargetValueEncoded = request.TargetValueEncoded; } return dto; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded request record. public static RouteToWaitForAttributeRequest FromProto(RouteToWaitForAttributeRequestDto dto) { ArgumentNullException.ThrowIfNull(dto); return new RouteToWaitForAttributeRequest( dto.CorrelationId, dto.InstanceUniqueName, dto.AttributeName, dto.TargetValueEncoded, dto.Timeout?.ToTimeSpan() ?? TimeSpan.Zero, Dto(dto.Timestamp), GuidFromWire(dto.ParentExecutionId), dto.RequireGoodQuality); } // ── Route replies ── /// Projects a onto the wire. /// The reply to project. /// The wire message. public static RouteToCallResponseDto ToProto(RouteToCallResponse response) { ArgumentNullException.ThrowIfNull(response); var dto = new RouteToCallResponseDto { CorrelationId = response.CorrelationId, Success = response.Success, ReturnValue = LooseValueCodec.ToProtoOrNull(response.ReturnValue), ErrorMessage = OrEmpty(response.ErrorMessage), Timestamp = Ts(response.Timestamp), // Flow Debugger (design 2026-09-05 §5.4): additive fields 6/7. ChildExecutionId = GuidToWire(response.ChildExecutionId), // Gitea #54: additive fields 8/9 — the site collector's truncation flags. Truncated = response.Truncated, LogsTruncated = response.LogsTruncated }; if (response.TraceEvents is { Count: > 0 } traceEvents) { foreach (var evt in traceEvents) { dto.TraceEvents.Add(AuditEventDtoMapper.ToDto(evt)); } } return dto; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded reply record. public static RouteToCallResponse FromProto(RouteToCallResponseDto dto) { ArgumentNullException.ThrowIfNull(dto); return new RouteToCallResponse( dto.CorrelationId, dto.Success, LooseValueCodec.FromProto(dto.ReturnValue), NullIfEmpty(dto.ErrorMessage), Dto(dto.Timestamp), GuidFromWire(dto.ChildExecutionId), // An untraced run — or an older site that never sets the field — sends no // events, which maps to null so the UI can label the lane "site not traced". dto.TraceEvents.Count == 0 ? null : dto.TraceEvents.Select(AuditEventDtoMapper.FromDto).ToList(), dto.Truncated, dto.LogsTruncated); } /// Projects a onto the wire. /// The reply to project. /// The wire message. public static RouteToGetAttributesResponseDto ToProto(RouteToGetAttributesResponse response) { ArgumentNullException.ThrowIfNull(response); return new RouteToGetAttributesResponseDto { CorrelationId = response.CorrelationId, Values = LooseValueCodec.ToProtoMap(response.Values), Success = response.Success, ErrorMessage = OrEmpty(response.ErrorMessage), Timestamp = Ts(response.Timestamp) }; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded reply record. public static RouteToGetAttributesResponse FromProto(RouteToGetAttributesResponseDto dto) { ArgumentNullException.ThrowIfNull(dto); return new RouteToGetAttributesResponse( dto.CorrelationId, LooseValueCodec.FromProtoMap(dto.Values), dto.Success, NullIfEmpty(dto.ErrorMessage), Dto(dto.Timestamp)); } /// Projects a onto the wire. /// The reply to project. /// The wire message. public static RouteToSetAttributesResponseDto ToProto(RouteToSetAttributesResponse response) { ArgumentNullException.ThrowIfNull(response); return new RouteToSetAttributesResponseDto { CorrelationId = response.CorrelationId, Success = response.Success, ErrorMessage = OrEmpty(response.ErrorMessage), Timestamp = Ts(response.Timestamp) }; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded reply record. public static RouteToSetAttributesResponse FromProto(RouteToSetAttributesResponseDto dto) { ArgumentNullException.ThrowIfNull(dto); return new RouteToSetAttributesResponse( dto.CorrelationId, dto.Success, NullIfEmpty(dto.ErrorMessage), Dto(dto.Timestamp)); } /// Projects a onto the wire. /// The reply to project. /// The wire message. public static RouteToWaitForAttributeResponseDto ToProto(RouteToWaitForAttributeResponse response) { ArgumentNullException.ThrowIfNull(response); return new RouteToWaitForAttributeResponseDto { CorrelationId = response.CorrelationId, Matched = response.Matched, Value = LooseValueCodec.ToProtoOrNull(response.Value), Quality = OrEmpty(response.Quality), TimedOut = response.TimedOut, Success = response.Success, ErrorMessage = OrEmpty(response.ErrorMessage), Timestamp = Ts(response.Timestamp) }; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded reply record. public static RouteToWaitForAttributeResponse FromProto(RouteToWaitForAttributeResponseDto dto) { ArgumentNullException.ThrowIfNull(dto); return new RouteToWaitForAttributeResponse( dto.CorrelationId, dto.Matched, LooseValueCodec.FromProto(dto.Value), NullIfEmpty(dto.Quality), dto.TimedOut, dto.Success, NullIfEmpty(dto.ErrorMessage), Dto(dto.Timestamp)); } // ───────────────────────────────────────────────────────────────────── // RPC 6 — TriggerFailover // ───────────────────────────────────────────────────────────────────── /// Projects a onto the wire. /// The relay command to project. /// The wire message. public static TriggerSiteFailoverDto ToProto(TriggerSiteFailover command) { ArgumentNullException.ThrowIfNull(command); return new TriggerSiteFailoverDto { CorrelationId = command.CorrelationId, SiteId = command.SiteId }; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded relay command. public static TriggerSiteFailover FromProto(TriggerSiteFailoverDto dto) { ArgumentNullException.ThrowIfNull(dto); return new TriggerSiteFailover(dto.CorrelationId, dto.SiteId); } /// Projects a onto the wire. /// The relay ack to project. /// The wire message. public static SiteFailoverAckDto ToProto(SiteFailoverAck ack) { ArgumentNullException.ThrowIfNull(ack); return new SiteFailoverAckDto { CorrelationId = ack.CorrelationId, Accepted = ack.Accepted, TargetAddress = OrEmpty(ack.TargetAddress), ErrorMessage = OrEmpty(ack.ErrorMessage) }; } /// Rehydrates a from the wire. /// The wire message to decode. /// The decoded relay ack. public static SiteFailoverAck FromProto(SiteFailoverAckDto dto) { ArgumentNullException.ThrowIfNull(dto); return new SiteFailoverAck( dto.CorrelationId, dto.Accepted, NullIfEmpty(dto.TargetAddress), NullIfEmpty(dto.ErrorMessage)); } // ───────────────────────────────────────────────────────────────────── // Enum translation — explicit both ways, never by ordinal. // ───────────────────────────────────────────────────────────────────── /// Projects a onto its wire enum. /// The status to project. /// The wire enum value. public static DeploymentStatusDto ToProto(DeploymentStatus status) => status switch { DeploymentStatus.Pending => DeploymentStatusDto.Pending, DeploymentStatus.InProgress => DeploymentStatusDto.InProgress, DeploymentStatus.Success => DeploymentStatusDto.Success, DeploymentStatus.Failed => DeploymentStatusDto.Failed, _ => DeploymentStatusDto.Unspecified }; /// Rehydrates a ; unknown decodes to Failed (never claim success). /// The wire enum value. /// The decoded status. public static DeploymentStatus FromProto(DeploymentStatusDto status) => status switch { DeploymentStatusDto.Pending => DeploymentStatus.Pending, DeploymentStatusDto.InProgress => DeploymentStatus.InProgress, DeploymentStatusDto.Success => DeploymentStatus.Success, _ => DeploymentStatus.Failed }; /// Projects a onto its wire enum. /// The node class to project. /// The wire enum value. public static BrowseNodeClassDto ToProto(BrowseNodeClass nodeClass) => nodeClass switch { BrowseNodeClass.Object => BrowseNodeClassDto.Object, BrowseNodeClass.Variable => BrowseNodeClassDto.Variable, BrowseNodeClass.Method => BrowseNodeClassDto.Method, BrowseNodeClass.Other => BrowseNodeClassDto.Other, _ => BrowseNodeClassDto.Unspecified }; /// Rehydrates a ; unknown decodes to Other. /// The wire enum value. /// The decoded node class. public static BrowseNodeClass FromProto(BrowseNodeClassDto nodeClass) => nodeClass switch { BrowseNodeClassDto.Object => BrowseNodeClass.Object, BrowseNodeClassDto.Variable => BrowseNodeClass.Variable, BrowseNodeClassDto.Method => BrowseNodeClass.Method, _ => BrowseNodeClass.Other }; /// Projects a onto its wire enum. /// The failure kind to project. /// The wire enum value. public static BrowseFailureKindDto ToProto(BrowseFailureKind kind) => kind switch { BrowseFailureKind.ConnectionNotFound => BrowseFailureKindDto.ConnectionNotFound, BrowseFailureKind.ConnectionNotConnected => BrowseFailureKindDto.ConnectionNotConnected, BrowseFailureKind.NotBrowsable => BrowseFailureKindDto.NotBrowsable, BrowseFailureKind.Timeout => BrowseFailureKindDto.Timeout, BrowseFailureKind.ServerError => BrowseFailureKindDto.ServerError, _ => BrowseFailureKindDto.Unspecified }; /// Rehydrates a ; unknown decodes to ServerError. /// The wire enum value. /// The decoded failure kind. public static BrowseFailureKind FromProto(BrowseFailureKindDto kind) => kind switch { BrowseFailureKindDto.ConnectionNotFound => BrowseFailureKind.ConnectionNotFound, BrowseFailureKindDto.ConnectionNotConnected => BrowseFailureKind.ConnectionNotConnected, BrowseFailureKindDto.NotBrowsable => BrowseFailureKind.NotBrowsable, BrowseFailureKindDto.Timeout => BrowseFailureKind.Timeout, _ => BrowseFailureKind.ServerError }; /// Projects a onto its wire enum. /// The failure kind to project. /// The wire enum value. public static ReadTagValuesFailureKindDto ToProto(ReadTagValuesFailureKind kind) => kind switch { ReadTagValuesFailureKind.ConnectionNotFound => ReadTagValuesFailureKindDto.ConnectionNotFound, ReadTagValuesFailureKind.ConnectionNotConnected => ReadTagValuesFailureKindDto.ConnectionNotConnected, ReadTagValuesFailureKind.Timeout => ReadTagValuesFailureKindDto.Timeout, ReadTagValuesFailureKind.ServerError => ReadTagValuesFailureKindDto.ServerError, _ => ReadTagValuesFailureKindDto.Unspecified }; /// Rehydrates a ; unknown decodes to ServerError. /// The wire enum value. /// The decoded failure kind. public static ReadTagValuesFailureKind FromProto(ReadTagValuesFailureKindDto kind) => kind switch { ReadTagValuesFailureKindDto.ConnectionNotFound => ReadTagValuesFailureKind.ConnectionNotFound, ReadTagValuesFailureKindDto.ConnectionNotConnected => ReadTagValuesFailureKind.ConnectionNotConnected, ReadTagValuesFailureKindDto.Timeout => ReadTagValuesFailureKind.Timeout, _ => ReadTagValuesFailureKind.ServerError }; /// Projects a onto its wire enum. /// The failure kind to project. /// The wire enum value. public static VerifyFailureKindDto ToProto(VerifyFailureKind kind) => kind switch { VerifyFailureKind.Unreachable => VerifyFailureKindDto.Unreachable, VerifyFailureKind.AuthFailed => VerifyFailureKindDto.AuthFailed, VerifyFailureKind.UntrustedCertificate => VerifyFailureKindDto.UntrustedCertificate, VerifyFailureKind.Timeout => VerifyFailureKindDto.Timeout, VerifyFailureKind.ServerError => VerifyFailureKindDto.ServerError, _ => VerifyFailureKindDto.Unspecified }; /// Rehydrates a ; unknown decodes to ServerError. /// The wire enum value. /// The decoded failure kind. public static VerifyFailureKind FromProto(VerifyFailureKindDto kind) => kind switch { VerifyFailureKindDto.Unreachable => VerifyFailureKind.Unreachable, VerifyFailureKindDto.AuthFailed => VerifyFailureKind.AuthFailed, VerifyFailureKindDto.UntrustedCertificate => VerifyFailureKind.UntrustedCertificate, VerifyFailureKindDto.Timeout => VerifyFailureKind.Timeout, _ => VerifyFailureKind.ServerError }; /// Projects a onto its wire enum. /// The category to project. /// The wire enum value. public static StoreAndForwardCategoryDto ToProto(StoreAndForwardCategory category) => category switch { StoreAndForwardCategory.ExternalSystem => StoreAndForwardCategoryDto.ExternalSystem, StoreAndForwardCategory.Notification => StoreAndForwardCategoryDto.Notification, StoreAndForwardCategory.CachedDbWrite => StoreAndForwardCategoryDto.CachedDbWrite, _ => StoreAndForwardCategoryDto.Unspecified }; /// Rehydrates a ; unknown decodes to ExternalSystem (the DTO's own default). /// The wire enum value. /// The decoded category. public static StoreAndForwardCategory FromProto(StoreAndForwardCategoryDto category) => category switch { StoreAndForwardCategoryDto.Notification => StoreAndForwardCategory.Notification, StoreAndForwardCategoryDto.CachedDbWrite => StoreAndForwardCategory.CachedDbWrite, _ => StoreAndForwardCategory.ExternalSystem }; /// Projects an onto its wire enum. /// The alarm state to project. /// The wire enum value. public static AlarmStateDto ToProto(AlarmState state) => state switch { AlarmState.Active => AlarmStateDto.Active, AlarmState.Normal => AlarmStateDto.Normal, _ => AlarmStateDto.Unspecified }; /// Rehydrates an ; unknown decodes to Normal (never invent an alarm). /// The wire enum value. /// The decoded alarm state. public static AlarmState FromProto(AlarmStateDto state) => state == AlarmStateDto.Active ? AlarmState.Active : AlarmState.Normal; /// Projects an onto its wire enum. /// The alarm level to project. /// The wire enum value. public static AlarmLevelDto ToProto(AlarmLevel level) => level switch { AlarmLevel.None => AlarmLevelDto.None, AlarmLevel.Low => AlarmLevelDto.Low, AlarmLevel.LowLow => AlarmLevelDto.LowLow, AlarmLevel.High => AlarmLevelDto.High, AlarmLevel.HighHigh => AlarmLevelDto.HighHigh, _ => AlarmLevelDto.Unspecified }; /// Rehydrates an ; unknown decodes to None. /// The wire enum value. /// The decoded alarm level. public static AlarmLevel FromProto(AlarmLevelDto level) => level switch { AlarmLevelDto.Low => AlarmLevel.Low, AlarmLevelDto.LowLow => AlarmLevel.LowLow, AlarmLevelDto.High => AlarmLevel.High, AlarmLevelDto.HighHigh => AlarmLevel.HighHigh, _ => AlarmLevel.None }; /// Projects an onto its wire enum. /// The alarm kind to project. /// The wire enum value. public static AlarmKindDto ToProto(AlarmKind kind) => kind switch { AlarmKind.Computed => AlarmKindDto.Computed, AlarmKind.NativeOpcUa => AlarmKindDto.NativeOpcUa, AlarmKind.NativeMxAccess => AlarmKindDto.NativeMxAccess, _ => AlarmKindDto.Unspecified }; /// Rehydrates an ; unknown decodes to Computed (the record's own default). /// The wire enum value. /// The decoded alarm kind. public static AlarmKind FromProto(AlarmKindDto kind) => kind switch { AlarmKindDto.NativeOpcUa => AlarmKind.NativeOpcUa, AlarmKindDto.NativeMxAccess => AlarmKind.NativeMxAccess, _ => AlarmKind.Computed }; /// Projects an onto its wire enum. /// The shelve state to project. /// The wire enum value. public static AlarmShelveStateDto ToProto(AlarmShelveState shelve) => shelve switch { AlarmShelveState.Unshelved => AlarmShelveStateDto.Unshelved, AlarmShelveState.OneShotShelved => AlarmShelveStateDto.OneShotShelved, AlarmShelveState.TimedShelved => AlarmShelveStateDto.TimedShelved, AlarmShelveState.PermanentShelved => AlarmShelveStateDto.PermanentShelved, _ => AlarmShelveStateDto.Unspecified }; /// /// Rehydrates an ; unknown decodes to /// Unshelved — the same safe default /// applies on the streaming path. /// /// The wire enum value. /// The decoded shelve state. public static AlarmShelveState FromProto(AlarmShelveStateDto shelve) => shelve switch { AlarmShelveStateDto.OneShotShelved => AlarmShelveState.OneShotShelved, AlarmShelveStateDto.TimedShelved => AlarmShelveState.TimedShelved, AlarmShelveStateDto.PermanentShelved => AlarmShelveState.PermanentShelved, _ => AlarmShelveState.Unshelved }; // ───────────────────────────────────────────────────────────────────── // Primitive helpers // ───────────────────────────────────────────────────────────────────── private static Timestamp Ts(DateTimeOffset value) => Timestamp.FromDateTimeOffset(value); private static DateTimeOffset Dto(Timestamp? value) => value?.ToDateTimeOffset() ?? default; private static DateTimeOffset? DtoOrNull(Timestamp? value) => value?.ToDateTimeOffset(); private static Timestamp TsUtc(DateTime value) => Timestamp.FromDateTime(EnsureUtc(value)); private static DateTime DtUtc(Timestamp? value) => value?.ToDateTime() ?? default; private static DateTime EnsureUtc(DateTime value) => value.Kind == DateTimeKind.Utc ? value : DateTime.SpecifyKind(value.ToUniversalTime(), DateTimeKind.Utc); private static string OrEmpty(string? value) => value ?? string.Empty; private static string? NullIfEmpty(string value) => string.IsNullOrEmpty(value) ? null : value; private static string GuidToWire(Guid? value) => value?.ToString("D") ?? string.Empty; private static Guid? GuidFromWire(string value) => Guid.TryParse(value, out var parsed) ? parsed : null; private static TrackedOperationId TrackedId(string value) => TrackedOperationId.TryParse(value, out var parsed) ? parsed : default; private static ArgumentException NotInGroup(object message, SiteCommandGroup group) => new($"'{message.GetType().Name}' is not part of the {group} RPC group.", nameof(message)); private static NotSupportedException UnknownCase(TCase oneofCase, string envelope) where TCase : struct, Enum => new($"{envelope} carries oneof case '{oneofCase}', which this build does not understand. " + "The peer is running a newer contract (message evolution is additive-only)."); } /// /// Marker reply for the fire-and-forget UnsubscribeDebugView command. /// /// /// The Akka path Tells the unsubscribe and expects nothing back, but a unary RPC /// must answer something. This singleton is what the dispatcher returns and what /// produces, so the two /// transports present the same shape; the central transport discards it and keeps /// the caller-visible fire-and-forget semantics. /// public sealed record UnsubscribeDebugViewAck { private UnsubscribeDebugViewAck() { } /// The single instance. public static UnsubscribeDebugViewAck Instance { get; } = new(); }