diff --git a/Directory.Packages.props b/Directory.Packages.props
index 460f35ac..3c258375 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -120,5 +120,7 @@
+
+
\ No newline at end of file
diff --git a/NuGet.config b/NuGet.config
index d656667c..2d7a0dcd 100644
--- a/NuGet.config
+++ b/NuGet.config
@@ -23,6 +23,8 @@
+
+
diff --git a/ZB.MOM.WW.OtOpcUa.slnx b/ZB.MOM.WW.OtOpcUa.slnx
index 6b7bc0bc..1c4b6908 100644
--- a/ZB.MOM.WW.OtOpcUa.slnx
+++ b/ZB.MOM.WW.OtOpcUa.slnx
@@ -26,6 +26,7 @@
+
@@ -86,6 +87,7 @@
+
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/IHistorianGatewayClient.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/IHistorianGatewayClient.cs
new file mode 100644
index 00000000..efb969a4
--- /dev/null
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/IHistorianGatewayClient.cs
@@ -0,0 +1,64 @@
+using ZB.MOM.WW.HistorianGateway.Contracts.Grpc;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway;
+
+///
+/// Abstraction over the HistorianGateway gRPC client surface consumed by the OtOpcUa historian
+/// backend driver. Proto-typed (the wire contract lives in
+/// ZB.MOM.WW.HistorianGateway.Contracts.Grpc); the concrete adapter wrapping
+/// HistorianGatewayClient is supplied by a later task. The seam exists so the driver and
+/// its tests can depend on a fake without a live gateway.
+///
+public interface IHistorianGatewayClient : IAsyncDisposable
+{
+ /// Streams raw historian samples for a tag over a time window.
+ IAsyncEnumerable ReadRawAsync(
+ string tag,
+ DateTime startUtc,
+ DateTime endUtc,
+ int maxValues,
+ CancellationToken ct);
+
+ /// Streams aggregate samples for a tag using the given retrieval mode and interval.
+ IAsyncEnumerable ReadAggregateAsync(
+ string tag,
+ DateTime startUtc,
+ DateTime endUtc,
+ RetrievalMode mode,
+ TimeSpan interval,
+ CancellationToken ct);
+
+ /// Reads the samples nearest to each of the requested timestamps (unary).
+ Task> ReadAtTimeAsync(
+ string tag,
+ IReadOnlyList timestampsUtc,
+ CancellationToken ct);
+
+ /// Streams historian events over a window, optionally filtered to a single source name.
+ IAsyncEnumerable ReadEventsAsync(
+ string? sourceName,
+ DateTime startUtc,
+ DateTime endUtc,
+ int maxEvents,
+ CancellationToken ct);
+
+ /// Writes live values for a tag through the gateway's SQL live-write path.
+ Task WriteLiveValuesAsync(
+ string tag,
+ IReadOnlyList values,
+ CancellationToken ct);
+
+ /// Sends a single historian event.
+ Task SendEventAsync(HistorianEvent evt, CancellationToken ct);
+
+ /// Ensures the supplied tag definitions exist (create-or-update).
+ Task EnsureTagsAsync(
+ IReadOnlyList definitions,
+ CancellationToken ct);
+
+ /// Probes gateway/historian reachability.
+ Task ProbeAsync(CancellationToken ct);
+
+ /// Reads the gateway's current historian connection status.
+ Task GetConnectionStatusAsync(CancellationToken ct);
+}
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
new file mode 100644
index 00000000..aad1040c
--- /dev/null
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.csproj
@@ -0,0 +1,24 @@
+
+
+
+ true
+ true
+ $(NoWarn);CS1591
+ ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/FakeHistorianGatewayClient.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/FakeHistorianGatewayClient.cs
new file mode 100644
index 00000000..1eda81d0
--- /dev/null
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/FakeHistorianGatewayClient.cs
@@ -0,0 +1,214 @@
+using System.Runtime.CompilerServices;
+using ZB.MOM.WW.HistorianGateway.Contracts.Grpc;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests;
+
+///
+/// Reusable in-memory test double for . Every method returns
+/// from a public settable result field and records its call arguments into public fields, so the
+/// later driver tasks (T7/T8/T11/T12/T14) can drive behaviour and assert on what the driver sent
+/// without a live gateway. Throw fields let a test simulate transport faults (e.g. an
+/// RpcException) per operation; reads share .
+///
+public sealed class FakeHistorianGatewayClient : IHistorianGatewayClient
+{
+ // ---- ReadRaw -------------------------------------------------------------------------------
+ public IReadOnlyList RawSamples = Array.Empty();
+ public string? LastReadRawTag;
+ public DateTime LastReadRawStartUtc;
+ public DateTime LastReadRawEndUtc;
+ public int LastReadRawMaxValues;
+ public int ReadRawCallCount;
+
+ // ---- ReadAggregate -------------------------------------------------------------------------
+ public IReadOnlyList AggregateSamples = Array.Empty();
+ public string? LastAggregateTag;
+ public DateTime LastAggregateStartUtc;
+ public DateTime LastAggregateEndUtc;
+ public RetrievalMode LastAggregateMode;
+ public TimeSpan LastAggregateInterval;
+ public int ReadAggregateCallCount;
+
+ // ---- ReadAtTime ----------------------------------------------------------------------------
+ public IReadOnlyList AtTimeSamples = Array.Empty();
+ public string? LastReadAtTimeTag;
+ public IReadOnlyList? LastReadAtTimeTimestamps;
+ public int ReadAtTimeCallCount;
+
+ // ---- ReadEvents ----------------------------------------------------------------------------
+ public IReadOnlyList Events = Array.Empty();
+ public string? LastReadEventsSourceName;
+ public DateTime LastReadEventsStartUtc;
+ public DateTime LastReadEventsEndUtc;
+ public int LastReadEventsMaxEvents;
+ public int ReadEventsCallCount;
+
+ /// Thrown (deferred to first enumeration) by every read method when set.
+ public Exception? ThrowOnRead;
+
+ // ---- WriteLiveValues -----------------------------------------------------------------------
+ public WriteAck WriteLiveValuesResult = new() { Success = true };
+ public string? LastWriteLiveTag;
+ public IReadOnlyList? LastWriteLiveValues;
+ public int WriteLiveValuesCallCount;
+ public Exception? WriteLiveValuesThrows;
+
+ // ---- SendEvent -----------------------------------------------------------------------------
+ public WriteAck SendEventResult = new() { Success = true };
+ public HistorianEvent? LastSendEvent;
+ public int SendEventCallCount;
+ public Exception? SendEventThrows;
+
+ // ---- EnsureTags ----------------------------------------------------------------------------
+ public TagOperationResults EnsureTagsResult = new();
+ public IReadOnlyList? LastEnsureDefinitions;
+ public int EnsureTagsCallCount;
+ public Exception? EnsureTagsThrows;
+
+ // ---- Probe ---------------------------------------------------------------------------------
+ public bool ProbeResult = true;
+ public int ProbeCallCount;
+ public Exception? ProbeThrows;
+
+ // ---- GetConnectionStatus -------------------------------------------------------------------
+ public ConnectionStatus ConnectionStatus = new();
+ public int GetConnectionStatusCallCount;
+ public Exception? GetConnectionStatusThrows;
+
+ // ---- Dispose -------------------------------------------------------------------------------
+ public int DisposeCallCount;
+
+ public IAsyncEnumerable ReadRawAsync(
+ string tag,
+ DateTime startUtc,
+ DateTime endUtc,
+ int maxValues,
+ CancellationToken ct)
+ {
+ LastReadRawTag = tag;
+ LastReadRawStartUtc = startUtc;
+ LastReadRawEndUtc = endUtc;
+ LastReadRawMaxValues = maxValues;
+ ReadRawCallCount++;
+ return ToAsyncStream(RawSamples, ThrowOnRead, ct);
+ }
+
+ public IAsyncEnumerable ReadAggregateAsync(
+ string tag,
+ DateTime startUtc,
+ DateTime endUtc,
+ RetrievalMode mode,
+ TimeSpan interval,
+ CancellationToken ct)
+ {
+ LastAggregateTag = tag;
+ LastAggregateStartUtc = startUtc;
+ LastAggregateEndUtc = endUtc;
+ LastAggregateMode = mode;
+ LastAggregateInterval = interval;
+ ReadAggregateCallCount++;
+ return ToAsyncStream(AggregateSamples, ThrowOnRead, ct);
+ }
+
+ public Task> ReadAtTimeAsync(
+ string tag,
+ IReadOnlyList timestampsUtc,
+ CancellationToken ct)
+ {
+ LastReadAtTimeTag = tag;
+ LastReadAtTimeTimestamps = timestampsUtc;
+ ReadAtTimeCallCount++;
+ return ThrowOnRead is not null
+ ? Task.FromException>(ThrowOnRead)
+ : Task.FromResult(AtTimeSamples);
+ }
+
+ public IAsyncEnumerable ReadEventsAsync(
+ string? sourceName,
+ DateTime startUtc,
+ DateTime endUtc,
+ int maxEvents,
+ CancellationToken ct)
+ {
+ LastReadEventsSourceName = sourceName;
+ LastReadEventsStartUtc = startUtc;
+ LastReadEventsEndUtc = endUtc;
+ LastReadEventsMaxEvents = maxEvents;
+ ReadEventsCallCount++;
+ return ToAsyncStream(Events, ThrowOnRead, ct);
+ }
+
+ public Task WriteLiveValuesAsync(
+ string tag,
+ IReadOnlyList values,
+ CancellationToken ct)
+ {
+ LastWriteLiveTag = tag;
+ LastWriteLiveValues = values;
+ WriteLiveValuesCallCount++;
+ return WriteLiveValuesThrows is not null
+ ? Task.FromException(WriteLiveValuesThrows)
+ : Task.FromResult(WriteLiveValuesResult);
+ }
+
+ public Task SendEventAsync(HistorianEvent evt, CancellationToken ct)
+ {
+ LastSendEvent = evt;
+ SendEventCallCount++;
+ return SendEventThrows is not null
+ ? Task.FromException(SendEventThrows)
+ : Task.FromResult(SendEventResult);
+ }
+
+ public Task EnsureTagsAsync(
+ IReadOnlyList definitions,
+ CancellationToken ct)
+ {
+ LastEnsureDefinitions = definitions;
+ EnsureTagsCallCount++;
+ return EnsureTagsThrows is not null
+ ? Task.FromException(EnsureTagsThrows)
+ : Task.FromResult(EnsureTagsResult);
+ }
+
+ public Task ProbeAsync(CancellationToken ct)
+ {
+ ProbeCallCount++;
+ return ProbeThrows is not null
+ ? Task.FromException(ProbeThrows)
+ : Task.FromResult(ProbeResult);
+ }
+
+ public Task GetConnectionStatusAsync(CancellationToken ct)
+ {
+ GetConnectionStatusCallCount++;
+ return GetConnectionStatusThrows is not null
+ ? Task.FromException(GetConnectionStatusThrows)
+ : Task.FromResult(ConnectionStatus);
+ }
+
+ public ValueTask DisposeAsync()
+ {
+ DisposeCallCount++;
+ return ValueTask.CompletedTask;
+ }
+
+ private static async IAsyncEnumerable ToAsyncStream(
+ IReadOnlyList items,
+ Exception? error,
+ [EnumeratorCancellation] CancellationToken ct)
+ {
+ if (error is not null)
+ {
+ throw error;
+ }
+
+ foreach (var item in items)
+ {
+ ct.ThrowIfCancellationRequested();
+ yield return item;
+ }
+
+ await Task.CompletedTask.ConfigureAwait(false);
+ }
+}
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/ProjectSmokeTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/ProjectSmokeTests.cs
new file mode 100644
index 00000000..f4c3e324
--- /dev/null
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/ProjectSmokeTests.cs
@@ -0,0 +1,13 @@
+using Xunit;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests;
+
+public sealed class ProjectSmokeTests
+{
+ [Fact]
+ public void GatewayClientSeam_IsReferenceable()
+ {
+ var t = typeof(IHistorianGatewayClient);
+ Assert.Equal("IHistorianGatewayClient", t.Name);
+ }
+}
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
new file mode 100644
index 00000000..f4ddf47d
--- /dev/null
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests.csproj
@@ -0,0 +1,26 @@
+
+
+
+ net10.0
+ enable
+ enable
+ false
+ true
+ ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+