using Microsoft.Extensions.Logging; using ZB.MOM.WW.HistorianGateway.Client; using ZB.MOM.WW.HistorianGateway.Contracts.Grpc; using ZB.MOM.WW.OtOpcUa.Runtime.Historian; namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway; /// /// Concrete backed by the published /// package client. Each seam method forwards directly to the /// matching client wrapper — both sides speak the same generated historian_gateway.v1 proto /// types, so no shape translation happens here. The package client's typed exception hierarchy /// (HistorianGatewayUnavailableException et al.) is allowed to surface unchanged; the /// records it as a health failure and the node manager /// turns it into a Bad HistoryRead result. /// /// /// /// Lazy channel. calls , /// which constructs a GrpcChannel over a SocketsHttpHandler without opening a /// connection — the first RPC dials. Constructing the adapter therefore performs no network I/O, /// which the offline seam tests rely on (they build from bogus endpoints and must not connect). /// /// public sealed class HistorianGatewayClientAdapter : IHistorianGatewayClient, IDisposable { private readonly HistorianGatewayClient _inner; private HistorianGatewayClientAdapter(HistorianGatewayClient inner) => _inner = inner; /// /// Builds an adapter over a freshly created package client mapped from the bound /// . No connection is opened (lazy channel). /// /// The bound ServerHistorian configuration (endpoint, key, TLS posture). /// Logger factory threaded into the package client's channel diagnostics. /// A ready-to-use adapter whose underlying channel has not yet dialed the gateway. /// /// is empty or not an absolute http(s) URI. /// Defense in depth (archreview 06/S-11): the Host's ServerHistorianOptionsValidator fails /// these configs at startup, but any caller that bypasses ValidateOnStart (tests, the live /// fixture, future non-Host wiring) still gets a named, config-key-carrying error here instead of /// the raw the bare new Uri(...) ctor throws. /// public static HistorianGatewayClientAdapter Create(ServerHistorianOptions options, ILoggerFactory loggerFactory) { ArgumentNullException.ThrowIfNull(options); ArgumentNullException.ThrowIfNull(loggerFactory); // Fail fast with a named, actionable error rather than letting `new Uri("")` throw a raw // UriFormatException deep in the factory (archreview 06/S-11). The Endpoint is not a secret, // so it is safe to echo; the ApiKey is never surfaced. if (!Uri.TryCreate(options.Endpoint, UriKind.Absolute, out var endpointUri) || (endpointUri.Scheme != Uri.UriSchemeHttp && endpointUri.Scheme != Uri.UriSchemeHttps)) { throw new InvalidOperationException( $"ServerHistorian:Endpoint must be an absolute http(s) URI (e.g. https://host:5222); got '{options.Endpoint}'."); } var clientOptions = new HistorianGatewayClientOptions { Endpoint = endpointUri, ApiKey = options.ApiKey, UseTls = options.UseTls, CaCertificatePath = options.CaCertificatePath, // INVERTED mapping: ServerHistorianOptions.AllowUntrustedServerCertificate (opt-in to accept // a self-signed cert) is the negation of the client's RequireCertificateValidation. Allowing // an untrusted cert == not requiring validation; a pinned CaCertificatePath always verifies. RequireCertificateValidation = !options.AllowUntrustedServerCertificate, DefaultCallTimeout = options.CallTimeout, LoggerFactory = loggerFactory, }; return new HistorianGatewayClientAdapter(HistorianGatewayClient.Create(clientOptions)); } /// public IAsyncEnumerable ReadRawAsync( string tag, DateTime startUtc, DateTime endUtc, int maxValues, CancellationToken ct) => _inner.ReadRawAsync(tag, startUtc, endUtc, maxValues, ct); /// public IAsyncEnumerable ReadAggregateAsync( string tag, DateTime startUtc, DateTime endUtc, RetrievalMode mode, TimeSpan interval, CancellationToken ct) => _inner.ReadAggregateAsync(tag, startUtc, endUtc, mode, interval, ct); /// public Task> ReadAtTimeAsync( string tag, IReadOnlyList timestampsUtc, CancellationToken ct) => _inner.ReadAtTimeAsync(tag, timestampsUtc, ct); /// public IAsyncEnumerable ReadEventsAsync( string? sourceName, DateTime startUtc, DateTime endUtc, int maxEvents, CancellationToken ct) { HistorianEventFilter? filter = sourceName is null ? null : new HistorianEventFilter { PropertyName = "Source_Object", Comparison = HistorianEventComparison.Equal, Value = sourceName, }; return _inner.ReadEventsAsync(startUtc, endUtc, filter, ct); } /// public Task WriteLiveValuesAsync( string tag, IReadOnlyList values, CancellationToken ct) => _inner.WriteLiveValuesAsync(tag, values, ct); /// public Task SendEventAsync(HistorianEvent evt, CancellationToken ct) => _inner.SendEventAsync(evt, ct); /// public Task EnsureTagsAsync( IReadOnlyList definitions, CancellationToken ct) => _inner.EnsureTagsAsync(definitions, ct); /// public Task ProbeAsync(CancellationToken ct) => _inner.ProbeAsync(ct); /// public Task GetConnectionStatusAsync(CancellationToken ct) => _inner.GetConnectionStatusAsync(ct); /// Disposes the underlying package client (and its channel). Prefer . public void Dispose() => _inner.Dispose(); /// Asynchronously disposes the underlying package client (and its channel). /// A task that completes when the client has been disposed. public ValueTask DisposeAsync() => _inner.DisposeAsync(); }