diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/GatewayHistorianServiceCollectionExtensions.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/GatewayHistorianServiceCollectionExtensions.cs
new file mode 100644
index 00000000..b52fa217
--- /dev/null
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/GatewayHistorianServiceCollectionExtensions.cs
@@ -0,0 +1,42 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
+using ZB.MOM.WW.OtOpcUa.Runtime.Historian;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway;
+
+///
+/// Host-callable factory that builds the gateway-backed server-side HistoryRead data source. The
+/// Host's AddServerHistorian wiring supplies as its
+/// Func<ServerHistorianOptions, IServiceProvider, IHistorianDataSource>, keeping the
+/// concrete package-client dependency inside this driver project (the Host references only the
+/// driver, not the package client directly).
+///
+public static class GatewayHistorian
+{
+ ///
+ /// Builds a over a lazily connected
+ /// mapped from the bound
+ /// . Resolves an and the data
+ /// source's from , falling back to
+ /// the null implementations when absent (e.g. minimal test providers). Performs no network I/O —
+ /// the underlying channel dials on first use.
+ ///
+ /// The bound ServerHistorian configuration.
+ /// The resolving service provider (used only to locate logging services).
+ /// The gateway-backed .
+ public static IHistorianDataSource CreateDataSource(ServerHistorianOptions options, IServiceProvider services)
+ {
+ ArgumentNullException.ThrowIfNull(options);
+ ArgumentNullException.ThrowIfNull(services);
+
+ var loggerFactory = services.GetService() ?? NullLoggerFactory.Instance;
+ var logger = services.GetService>()
+ ?? NullLogger.Instance;
+
+ return new GatewayHistorianDataSource(
+ HistorianGatewayClientAdapter.Create(options, loggerFactory),
+ logger);
+ }
+}
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/HistorianGatewayClientAdapter.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/HistorianGatewayClientAdapter.cs
new file mode 100644
index 00000000..f69426e8
--- /dev/null
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/HistorianGatewayClientAdapter.cs
@@ -0,0 +1,126 @@
+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.
+ public static HistorianGatewayClientAdapter Create(ServerHistorianOptions options, ILoggerFactory loggerFactory)
+ {
+ ArgumentNullException.ThrowIfNull(options);
+ ArgumentNullException.ThrowIfNull(loggerFactory);
+
+ var clientOptions = new HistorianGatewayClientOptions
+ {
+ Endpoint = new Uri(options.Endpoint),
+ 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);
+
+ ///
+ ///
+ /// is rendered into the gateway's one server-filterable predicate —
+ /// a Source_Object filter the SQL ReadEvents
+ /// path binds as WHERE Source_Object = @source. A null source passes a null filter
+ /// (full window). is intentionally ignored here: the gateway wire
+ /// contract carries no per-call cap, so the cap is enforced upstream by
+ /// via early stream termination.
+ ///
+ 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();
+}
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.csproj b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.csproj
index 63c34238..8660ec4f 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.csproj
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.csproj
@@ -11,6 +11,11 @@
+
+
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs
index 59a824a2..478dd454 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs
@@ -22,6 +22,7 @@ using ZB.MOM.WW.OtOpcUa.Host.Health;
using ZB.MOM.WW.OtOpcUa.Host.Logging;
using ZB.MOM.WW.OtOpcUa.Host.Observability;
using ZB.MOM.WW.OtOpcUa.Host.OpcUa;
+using ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway;
using ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client;
using ZB.MOM.WW.OtOpcUa.OpcUaServer;
using ZB.MOM.WW.OtOpcUa.Runtime.Historian;
@@ -109,17 +110,12 @@ if (hasDriver)
// Config-gated server-side HistoryRead backend. When the ServerHistorian section is enabled this
// overrides the NullHistorianDataSource default from AddOtOpcUaRuntime (last registration wins) with
- // a read-only WonderwareHistorianClient the node manager's HistoryRead overrides block-bridge to.
- // The client is supplied here because the Host is the only project that references the Wonderware
- // client — Runtime owns the gating, the Host supplies the concrete read downstream.
+ // a read-only HistorianGateway-backed data source the node manager's HistoryRead overrides
+ // block-bridge to. The factory lives in the Gateway driver (which owns the package-client adapter
+ // and the ServerHistorianOptions -> client-options mapping); Runtime owns the gating.
builder.Services.AddServerHistorian(
builder.Configuration,
- (opts, sp) => new WonderwareHistorianClient(
- new WonderwareHistorianClientOptions(opts.Host, opts.Port, opts.SharedSecret)
- {
- UseTls = opts.UseTls, ServerCertThumbprint = opts.ServerCertThumbprint,
- },
- sp.GetService>()));
+ (opts, sp) => GatewayHistorian.CreateDataSource(opts, sp));
// Bind every cross-platform driver factory before AddAkka resolves IDriverFactory — replaces
// the F7-default NullDriverFactory with a real DriverFactoryRegistryAdapter so DriverHostActor
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Host/ZB.MOM.WW.OtOpcUa.Host.csproj b/src/Server/ZB.MOM.WW.OtOpcUa.Host/ZB.MOM.WW.OtOpcUa.Host.csproj
index a46c3693..1c6275ec 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.Host/ZB.MOM.WW.OtOpcUa.Host.csproj
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.Host/ZB.MOM.WW.OtOpcUa.Host.csproj
@@ -61,6 +61,7 @@
+
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/HistorianGatewayClientAdapterTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/HistorianGatewayClientAdapterTests.cs
new file mode 100644
index 00000000..08dcc002
--- /dev/null
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/HistorianGatewayClientAdapterTests.cs
@@ -0,0 +1,32 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging.Abstractions;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.Runtime.Historian;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests;
+
+///
+/// Read-cutover seam tests (T10). Both assert offline construction only — the package client builds
+/// its GrpcChannel lazily, so neither the adapter ctor nor the factory dials the gateway. A
+/// bogus/unreachable endpoint must therefore construct without throwing or performing network I/O.
+///
+public sealed class HistorianGatewayClientAdapterTests
+{
+ [Fact]
+ public void Adapter_constructs_from_options_without_dialing()
+ {
+ // Constructing the channel must not perform network I/O (lazy connect).
+ var opts = new ServerHistorianOptions { Enabled = true, Endpoint = "https://localhost:5222", ApiKey = "histgw_x_y" };
+ using var adapter = HistorianGatewayClientAdapter.Create(opts, NullLoggerFactory.Instance);
+ Assert.NotNull(adapter);
+ }
+
+ [Fact]
+ public void Factory_builds_GatewayHistorianDataSource()
+ {
+ var opts = new ServerHistorianOptions { Enabled = true, Endpoint = "https://localhost:5222", ApiKey = "histgw_x_y" };
+ using var services = new ServiceCollection().BuildServiceProvider();
+ var dataSource = GatewayHistorian.CreateDataSource(opts, services);
+ Assert.IsType(dataSource);
+ }
+}
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests.csproj b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests.csproj
index f4ddf47d..16e7fc33 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests.csproj
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests.csproj
@@ -13,6 +13,8 @@
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive