From 4da2d62b9a63086a9a842101eb0ac4593f2a8c44 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 13 Jul 2026 19:29:49 -0400 Subject: [PATCH] fix(historian): stamp a positive StorageRateMs on provisioned tags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GatewayTagProvisioner built HistorianTagDefinition without a StorageRateMs, so the proto uint32 defaulted to 0. The gateway's EnsureTags maps StorageRateMs straight to the AVEVA SDK's storageRateMs, which requires a positive value — 0 makes the SDK throw ArgumentOutOfRangeException ('Storage rate must be > 0 ms'), so EVERY historized-tag auto-provision (the deploy-time IHistorianProvisioning hook) silently failed at the gateway with failed=N. Stamp DefaultStorageRateMs=1000 (matches the gateway's own live provisioner convention; quantized 1000/5000/10000). Add a regression unit test asserting the definition carries a positive rate, and set StorageRateMs on the live-suite's inline definitions so the retype probe reaches a real EnsureTags. Found via the R2-06 live gate against a real 0.2.0 gateway + AVEVA historian: with this fixed, Float/Int2/UInt2/Int4/UInt4/Double all provision and the live suite goes 3/6 -> 5/6. (Int1/Boolean remains blocked by the gateway histsdk's ProtocolEvidenceMissingException for Int1 — a gateway-side type-support gap, not this bug.) Gateway driver unit 103/103. --- .../GatewayTagProvisioner.cs | 12 ++++++++++++ .../GatewayTagProvisionerTests.cs | 19 +++++++++++++++++++ .../Live/GatewayLiveIntegrationTests.cs | 3 +++ 3 files changed, 34 insertions(+) diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/GatewayTagProvisioner.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/GatewayTagProvisioner.cs index 08e87f28..986f82ab 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/GatewayTagProvisioner.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/GatewayTagProvisioner.cs @@ -19,6 +19,16 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway; /// public sealed class GatewayTagProvisioner : IHistorianProvisioning { + /// + /// Storage rate (ms) stamped on every provisioned analog tag. The gateway's EnsureTags path + /// maps this straight to the AVEVA SDK's storageRateMs, which requires a positive, quantized + /// value (1000/5000/10000/…); the proto's uint32 default of 0 makes the SDK throw + /// ("Storage rate must be > 0 ms"), failing the whole + /// provision. 1000 ms (1 s) matches the gateway's own live provisioner default and suits + /// continuous-historization analog tags. + /// + private const uint DefaultStorageRateMs = 1000; + private readonly IHistorianGatewayClient _client; private readonly ILogger _logger; @@ -58,6 +68,8 @@ public sealed class GatewayTagProvisioner : IHistorianProvisioning // Proto string fields are non-nullable — coalesce absent metadata to empty. EngineeringUnit = request.EngineeringUnit ?? string.Empty, Description = request.Description ?? string.Empty, + // Required: a 0 storage rate makes the gateway's EnsureTags throw (see the constant). + StorageRateMs = DefaultStorageRateMs, }); } diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/GatewayTagProvisionerTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/GatewayTagProvisionerTests.cs index a98dad05..698d766c 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/GatewayTagProvisionerTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/GatewayTagProvisionerTests.cs @@ -60,6 +60,25 @@ public sealed class GatewayTagProvisionerTests Assert.True(defs[0].HasDataType); } + [Fact] + public async Task Definition_carries_a_positive_storage_rate() + { + // Regression guard (R2-06 live gate): the gateway maps StorageRateMs straight to the AVEVA SDK's + // storageRateMs, which requires a positive value — the proto uint32 default of 0 makes EnsureTags + // throw ArgumentOutOfRangeException ("Storage rate must be > 0 ms"), silently failing EVERY + // historized-tag provision. A non-zero rate must always be stamped. + var fake = new FakeHistorianGatewayClient { EnsureTagsResult = new TagOperationResults() }; + var p = Provisioner(fake); + + await p.EnsureTagsAsync( + new[] { new HistorianTagProvisionRequest("Pump1.Temp", DriverDataType.Float32, null, null) }, + TestContext.Current.CancellationToken); + + var defs = fake.LastEnsureDefinitions!; + Assert.Single(defs); + Assert.True(defs[0].StorageRateMs > 0, "provisioned tags must carry a positive storage rate (0 fails EnsureTags)."); + } + [Fact] public async Task Maps_metadata_and_coalesces_null_metadata_to_empty() { diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Live/GatewayLiveIntegrationTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Live/GatewayLiveIntegrationTests.cs index 1453d34a..484687d2 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Live/GatewayLiveIntegrationTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Live/GatewayLiveIntegrationTests.cs @@ -103,6 +103,7 @@ public sealed class GatewayLiveIntegrationTests(GatewayLiveFixture fixture) : IC DataType = HistorianDataType.Float, EngineeringUnit = string.Empty, Description = "OtOpcUa live validation sandbox", + StorageRateMs = 1000, // 0 makes the gateway's EnsureTags throw (storageRateMs must be > 0). }, }, ct); @@ -345,6 +346,7 @@ public sealed class GatewayLiveIntegrationTests(GatewayLiveFixture fixture) : IC DataType = HistorianDataType.Float, EngineeringUnit = string.Empty, Description = "OtOpcUa live validation retype probe (stage 1: Float)", + StorageRateMs = 1000, // 0 makes the gateway's EnsureTags throw (storageRateMs must be > 0). }, }, ct); @@ -358,6 +360,7 @@ public sealed class GatewayLiveIntegrationTests(GatewayLiveFixture fixture) : IC DataType = HistorianDataType.Int1, EngineeringUnit = string.Empty, Description = "OtOpcUa live validation retype probe (stage 2: Int1)", + StorageRateMs = 1000, // 0 makes the gateway's EnsureTags throw (storageRateMs must be > 0). }, }, ct);