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
index b52fa217..4acd5b88 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/GatewayHistorianServiceCollectionExtensions.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/GatewayHistorianServiceCollectionExtensions.cs
@@ -2,16 +2,17 @@ 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.Core.AlarmHistorian;
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).
+/// Host-callable factories that build the gateway-backed historian seams against the single
+/// ServerHistorian gateway: for the read path (the Host's
+/// AddServerHistorian wiring) and for the alarm-write path
+/// (the Host's AddAlarmHistorian wiring). Both keep the concrete package-client dependency
+/// inside this driver project — the Host references only the driver, not the package client directly.
///
public static class GatewayHistorian
{
@@ -39,4 +40,42 @@ public static class GatewayHistorian
HistorianGatewayClientAdapter.Create(options, loggerFactory),
logger);
}
+
+ ///
+ /// Builds a over a lazily connected
+ /// mapped from the bound
+ /// — the same single gateway the read path
+ /// () targets. The Host's AddAlarmHistorian wiring supplies
+ /// this as the concrete the durable
+ /// SqliteStoreAndForwardSink drain worker delegates to, sourcing the connection from the
+ /// ServerHistorian section (endpoint/key/TLS) rather than the legacy Wonderware-shaped
+ /// AlarmHistorian host/port. Resolves an and the writer's
+ /// from , falling back to the null
+ /// implementations when absent. Performs no network I/O — the underlying channel dials on first send.
+ ///
+ ///
+ /// This deliberately constructs its own — a
+ /// second gRPC channel to the same gateway as the read path. Collapsing the two onto one shared
+ /// channel would require the container to own a singleton and
+ /// the read-side to stop owning + disposing its client,
+ /// regressing the read cutover's dispose ownership (and its tests). A second channel to a co-located
+ /// sidecar is cheap — the gateway pools and amortizes the underlying historian sessions server-side —
+ /// so each path keeps its own channel with a clean, independent lifetime.
+ ///
+ /// The bound ServerHistorian configuration (endpoint, key, TLS posture).
+ /// The resolving service provider (used only to locate logging services).
+ /// The gateway-backed .
+ public static IAlarmHistorianWriter CreateAlarmWriter(ServerHistorianOptions options, IServiceProvider services)
+ {
+ ArgumentNullException.ThrowIfNull(options);
+ ArgumentNullException.ThrowIfNull(services);
+
+ var loggerFactory = services.GetService() ?? NullLoggerFactory.Instance;
+ var logger = services.GetService>()
+ ?? NullLogger.Instance;
+
+ return new GatewayAlarmHistorianWriter(
+ HistorianGatewayClientAdapter.Create(options, loggerFactory),
+ logger);
+ }
}
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs
index 478dd454..14dd4575 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs
@@ -23,7 +23,6 @@ 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;
using ZB.MOM.WW.OtOpcUa.Runtime.Scripting;
@@ -96,17 +95,23 @@ if (hasDriver)
// Config-gated durable alarm-historian sink. When the AlarmHistorian section is enabled this
// overrides the NullAlarmHistorianSink default from AddOtOpcUaRuntime (last registration wins)
- // with a SqliteStoreAndForwardSink draining to the Wonderware TCP writer. The writer is
- // injected here because the Host is the only project that references the Wonderware client —
- // Runtime owns the gating + Sqlite construction, the Host supplies the concrete downstream.
+ // with a SqliteStoreAndForwardSink draining to the gateway SendEvent writer. The alarm-write path
+ // targets the SAME single gateway as the read path, so its connection (endpoint/key/TLS) is sourced
+ // from the ServerHistorian section — NOT the legacy Wonderware-shaped AlarmHistorian host/port.
+ // AlarmHistorianOptions still supplies the Enabled gate + the SQLite store-and-forward knobs
+ // (consumed inside AddAlarmHistorian), so its Wonderware connection fields are intentionally unused.
+ // Runtime owns the gating + Sqlite construction; the Host supplies the concrete gateway downstream
+ // via the driver factory (which owns the package-client adapter). The writer builds its OWN gateway
+ // channel — a second channel to the same sidecar: sharing one channel with the read path would force
+ // the read-side GatewayHistorianDataSource to stop owning + disposing its client (regressing the read
+ // cutover), and a second channel to a co-located sidecar is cheap (the gateway pools the historian
+ // sessions server-side).
+ var serverHistorianOptions = builder.Configuration
+ .GetSection(ServerHistorianOptions.SectionName).Get()
+ ?? new ServerHistorianOptions();
builder.Services.AddAlarmHistorian(
builder.Configuration,
- (opts, sp) => new WonderwareHistorianClient(
- new WonderwareHistorianClientOptions(opts.Host, opts.Port, opts.SharedSecret)
- {
- UseTls = opts.UseTls, ServerCertThumbprint = opts.ServerCertThumbprint,
- },
- sp.GetService>()));
+ (_, sp) => GatewayHistorian.CreateAlarmWriter(serverHistorianOptions, sp));
// Config-gated server-side HistoryRead backend. When the ServerHistorian section is enabled this
// overrides the NullHistorianDataSource default from AddOtOpcUaRuntime (last registration wins) with
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/GatewayAlarmWriterFactoryTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/GatewayAlarmWriterFactoryTests.cs
new file mode 100644
index 00000000..ddbfc80a
--- /dev/null
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/GatewayAlarmWriterFactoryTests.cs
@@ -0,0 +1,28 @@
+using Microsoft.Extensions.DependencyInjection;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian;
+using ZB.MOM.WW.OtOpcUa.Runtime.Historian;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests;
+
+///
+/// Alarm-write cutover seam test (T13). The Host's AddAlarmHistorian wiring drains the durable
+/// SqliteStoreAndForwardSink through this factory, so it must yield the gateway-backed writer —
+/// sourcing the single gateway's connection from (endpoint/key/TLS),
+/// not the legacy Wonderware-shaped AlarmHistorian host/port. Built offline: the underlying
+/// channel dials lazily, so both the factory and the writer ctor perform no network I/O (a bogus,
+/// unreachable endpoint must construct without throwing or connecting).
+///
+public sealed class GatewayAlarmWriterFactoryTests
+{
+ [Fact]
+ public void Factory_builds_GatewayAlarmHistorianWriter()
+ {
+ var opts = new ServerHistorianOptions { Enabled = true, Endpoint = "https://localhost:5222", ApiKey = "histgw_x_y" };
+ using var services = new ServiceCollection().BuildServiceProvider();
+
+ IAlarmHistorianWriter writer = GatewayHistorian.CreateAlarmWriter(opts, services);
+
+ Assert.IsType(writer);
+ }
+}