diff --git a/Directory.Packages.props b/Directory.Packages.props
index eafd04611..4e0ca941f 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -32,9 +32,6 @@
-
-
@@ -245,7 +242,7 @@
-
-
+
+
\ No newline at end of file
diff --git a/docs/Historian.md b/docs/Historian.md
index dca6b314d..98896009e 100644
--- a/docs/Historian.md
+++ b/docs/Historian.md
@@ -345,7 +345,7 @@ and is registered the same way for every backend.
> **Cross-repo follow-up — a multi-tag batch `WriteLiveValues` on the gateway contract.** The drain's
> unit of work is one gateway round-trip **per historian tag**, because
-> `ZB.MOM.WW.HistorianGateway.Client` **1.0.0** exposes no RPC that writes values for more than one tag
+> `ZB.MOM.WW.HistorianGateway.Client` **1.1.0** exposes no RPC that writes values for more than one tag
> in a call. Bounded parallelism (`DrainConcurrency`) is therefore the ceiling: it overlaps the
> round-trips but cannot remove them, so a wide tag set still pays `⌈tags / concurrency⌉` network
> hops per drain pass. Collapsing that to a single hop needs a **batch/multi-tag `WriteLiveValues`** on
@@ -353,8 +353,9 @@ and is registered the same way for every backend.
> lands, raising `DrainBatchSize` widens each hop's payload but not the hop count — which is why the
> concurrency window, not the batch width, is what governs drain throughput today.
-The HistorianGateway **Client and Contracts packages are pinned together at 1.0.0**
-([historiangw#131](https://gitea.dohertylan.com/dohertj2/historiangw/issues/131)). The adapter maps
+The HistorianGateway **Client and Contracts packages are pinned together at 1.1.0**
+([historiangw#135](https://gitea.dohertylan.com/dohertj2/historiangw/issues/135): additive — typed error reasons on
+`HistorianGatewayException`; 1.0.0 before it, [historiangw#131](https://gitea.dohertylan.com/dohertj2/historiangw/issues/131)). The adapter maps
`AllowUntrustedServerCertificate` directly and only for TLS: the default verifies the server, while an
explicit opt-out applies only when `UseTls=true`. Stale certificate options are ignored on plaintext h2c.
This follows the earlier [0.4.0 update](https://gitea.dohertylan.com/dohertj2/historiangw/issues/119). The removed
@@ -1598,8 +1599,9 @@ after historiangw#135 (`a220ec0`) answers it `FailedPrecondition` with a `google
to **`BadAggregateNotSupported`** for the node and counts **no** backend failure (#900, live-verified read-only against
wonder-sql-vd03 `SysPulse`). Only that reason is matched: any other `FailedPrecondition` and every `Internal` keep the old
handling, so an older gateway (which answers this case `Internal`) still gives `BadHistoryOperationUnsupported`, counted as a
-failure. The reason is read in one place, `GatewayErrorReasons` — from the trailer via `Grpc.StatusProto` while the
-HistorianGateway Client pin is 1.0.0; with Client 1.1.0 it becomes `HistorianGatewayException.ErrorReason`.
+failure. The reason is read in one place, `GatewayErrorReasons`, from Client 1.1.0's typed
+`HistorianGatewayException.StatusCode` / `ErrorReason` / `ErrorDomain` (the client parses the trailer; OtOpcUa no longer
+references `Grpc.StatusProto` itself).
⚠️ **`Server/ServerCapabilities/AggregateFunctions` (`i=2997`) is deliberately NOT trimmed** — it keeps the SDK's full 37 in
every configuration, including with no historian. Part 5 defines it as every aggregate the *server* supports, and the server
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/GatewayErrorReasons.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/GatewayErrorReasons.cs
index 2f268fbda..d07975f00 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/GatewayErrorReasons.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/GatewayErrorReasons.cs
@@ -1,72 +1,48 @@
using Grpc.Core;
+using ZB.MOM.WW.HistorianGateway.Client;
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway;
///
/// The HistorianGateway's machine-readable failure reasons (historiangw#135): a google.rpc.ErrorInfo the gateway
-/// attaches in the standard grpc-status-details-bin trailer, domain . The ONE place OtOpcUa
-/// reads them (lmxopcua#900).
+/// attaches in the standard grpc-status-details-bin trailer, domain .
+/// The ONE place OtOpcUa reads them (lmxopcua#900).
///
///
-/// The Client pin is 1.0.0, which does not surface the reason, but keeps the gateway's —
-/// trailers included — as the of the HistorianGatewayException it throws, so
-/// parses the trailer with Grpc.StatusProto. When the pin moves to Client 1.1.0 (not yet on the
-/// feed), becomes the one line
-/// (ex as ZB.MOM.WW.HistorianGateway.Client.HistorianGatewayException)?.ErrorReason (and the constants can point
-/// at HistorianGatewayErrorReasons); the Grpc.StatusProto reference can then go.
+/// Client 1.1.0 parses the trailer itself and exposes it on every it throws
+/// (, ,
+/// ), so this reads those typed properties. The 1.0.0-era trailer
+/// parsing (Grpc.StatusProto over the inner ) is gone: the published client maps every
+/// of every call to a (or an
+/// for a caller cancellation), so no raw one reaches the data source.
///
internal static class GatewayErrorReasons
{
- /// The ErrorInfo.domain of every reason the gateway sends (its proto package).
- public const string Domain = "historian_gateway.v1";
-
///
- /// AVEVA refused the aggregate retrieval mode for this tag (TimeWeightedAverage / Integral / … on a discrete or string
- /// tag; native CustomError 85 at StartQuery). Sent with .
- ///
- public const string AggregateNotSupportedForTag = "AGGREGATE_NOT_SUPPORTED_FOR_TAG";
-
- ///
- /// The gateway's ErrorInfo.reason on (or an inner exception — the client package wraps the
- /// ), or when it sent none, sent one from another domain, or sent an
- /// unparseable trailer.
+ /// The gateway's ErrorInfo.reason on (or an inner exception), or
+ /// when it sent none or sent one from another domain.
///
/// A gateway call's exception.
/// The reason string, or .
- public static string? ReasonOf(Exception ex)
- {
- for (var e = ex; e is not null; e = e.InnerException)
- {
- if (e is not RpcException rpc) continue;
- try
- {
- var info = rpc.GetRpcStatus()?.GetDetail();
- return info is not null && string.Equals(info.Domain, Domain, StringComparison.Ordinal) ? info.Reason : null;
- }
- catch (Google.Protobuf.InvalidProtocolBufferException)
- {
- return null; // a malformed trailer is no reason: the failure keeps its ordinary handling
- }
- }
- return null;
- }
+ public static string? ReasonOf(Exception ex) =>
+ GatewayExceptionOf(ex) is { ErrorDomain: HistorianGatewayErrorReasons.Domain } gw ? gw.ErrorReason : null;
///
/// True when is the gateway's refusal of an aggregate for the tag: status
- /// AND reason . FailedPrecondition alone
- /// is not enough — the gateway also uses it for disabled live writes and passes historian-side ones through — and an
- /// older gateway answers this case Internal with no reason (unchanged handling).
+ /// AND reason .
+ /// FailedPrecondition alone is not enough — the gateway also uses it for disabled live writes and passes historian-side
+ /// ones through — and an older gateway answers this case Internal with no reason (unchanged handling).
///
/// A gateway call's exception.
/// Whether it is the aggregate refusal.
public static bool IsAggregateNotSupportedForTag(Exception ex) =>
- StatusOf(ex) == StatusCode.FailedPrecondition
- && string.Equals(ReasonOf(ex), AggregateNotSupportedForTag, StringComparison.Ordinal);
+ GatewayExceptionOf(ex)?.StatusCode == StatusCode.FailedPrecondition
+ && string.Equals(ReasonOf(ex), HistorianGatewayErrorReasons.AggregateNotSupportedForTag, StringComparison.Ordinal);
- private static StatusCode? StatusOf(Exception ex)
+ private static HistorianGatewayException? GatewayExceptionOf(Exception ex)
{
for (var e = ex; e is not null; e = e.InnerException)
- if (e is RpcException rpc) return rpc.StatusCode;
+ if (e is HistorianGatewayException gw) return gw;
return null;
}
}
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 e8b575bec..c1a1d9410 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
@@ -25,8 +25,6 @@
-
-
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/GatewayAggregateNotSupportedWireTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/GatewayAggregateNotSupportedWireTests.cs
index 8e897c76d..aa502dd27 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/GatewayAggregateNotSupportedWireTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/GatewayAggregateNotSupportedWireTests.cs
@@ -22,8 +22,9 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests;
/// FailedPrecondition, detail HistorianAggregateNotSupportedException, and a google.rpc.Status holding a
/// google.rpc.ErrorInfo (reason AGGREGATE_NOT_SUPPORTED_FOR_TAG, domain historian_gateway.v1, metadata
/// retrieval_mode + native_error_code) in grpc-status-details-bin (shape copied from the gateway's
-/// HistorianRpcFaults.ToRpcException). Read through the PUBLISHED Client 1.0.0 (via the production adapter), so this
-/// also pins that 1.0.0 keeps the trailer on the exception it throws. Controls: every other shape keeps today's handling
+/// HistorianRpcFaults.ToRpcException). Read through the PUBLISHED Client 1.1.0 (via the production adapter), so this
+/// also pins that 1.1.0 surfaces the trailer as the typed HistorianGatewayException.ErrorReason /
+/// ErrorDomain / StatusCode that GatewayErrorReasons reads. Controls: every other shape keeps today's handling
/// (rethrown, counted as a backend failure).
///
public sealed class GatewayAggregateNotSupportedWireTests
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/GatewayErrorReasonsTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/GatewayErrorReasonsTests.cs
new file mode 100644
index 000000000..05e9871ba
--- /dev/null
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/GatewayErrorReasonsTests.cs
@@ -0,0 +1,63 @@
+using Grpc.Core;
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.HistorianGateway.Client;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests;
+
+///
+/// lmxopcua#900 on Client 1.1.0: classifies by the client's typed
+/// / /
+/// . These exceptions carry the reason ONLY on the typed properties
+/// (no inner , no trailer), so they pin that the typed accessor is what is read. The on-the-wire
+/// path through the published client is .
+///
+public sealed class GatewayErrorReasonsTests
+{
+ private static HistorianGatewayException Gw(StatusCode? code, string? reason, string? domain) =>
+ new("x") { StatusCode = code, ErrorReason = reason, ErrorDomain = domain };
+
+ [Fact]
+ public void FailedPrecondition_with_the_gateways_aggregate_reason_is_the_refusal()
+ {
+ var ex = Gw(StatusCode.FailedPrecondition, HistorianGatewayErrorReasons.AggregateNotSupportedForTag, HistorianGatewayErrorReasons.Domain);
+
+ GatewayErrorReasons.IsAggregateNotSupportedForTag(ex).ShouldBeTrue();
+ GatewayErrorReasons.ReasonOf(ex).ShouldBe("AGGREGATE_NOT_SUPPORTED_FOR_TAG");
+ }
+
+ [Fact]
+ public void The_refusal_is_found_when_wrapped()
+ {
+ var ex = new InvalidOperationException("outer",
+ Gw(StatusCode.FailedPrecondition, HistorianGatewayErrorReasons.AggregateNotSupportedForTag, HistorianGatewayErrorReasons.Domain));
+
+ GatewayErrorReasons.IsAggregateNotSupportedForTag(ex).ShouldBeTrue();
+ }
+
+ public static TheoryData NotTheRefusal() => new()
+ {
+ { StatusCode.Internal, HistorianGatewayErrorReasons.AggregateNotSupportedForTag, HistorianGatewayErrorReasons.Domain },
+ { null, HistorianGatewayErrorReasons.AggregateNotSupportedForTag, HistorianGatewayErrorReasons.Domain },
+ { StatusCode.FailedPrecondition, null, null },
+ { StatusCode.FailedPrecondition, "LIVE_WRITES_DISABLED", HistorianGatewayErrorReasons.Domain },
+ { StatusCode.FailedPrecondition, HistorianGatewayErrorReasons.AggregateNotSupportedForTag, "example.com" },
+ { StatusCode.FailedPrecondition, HistorianGatewayErrorReasons.AggregateNotSupportedForTag, null },
+ };
+
+ [Theory]
+ [MemberData(nameof(NotTheRefusal))]
+ public void Every_other_shape_is_not_the_refusal(StatusCode? code, string? reason, string? domain) =>
+ GatewayErrorReasons.IsAggregateNotSupportedForTag(Gw(code, reason, domain)).ShouldBeFalse();
+
+ [Fact]
+ public void A_foreign_domains_reason_is_no_reason() =>
+ GatewayErrorReasons.ReasonOf(Gw(StatusCode.FailedPrecondition, "AGGREGATE_NOT_SUPPORTED_FOR_TAG", "example.com")).ShouldBeNull();
+
+ [Fact]
+ public void A_non_gateway_exception_is_not_the_refusal()
+ {
+ GatewayErrorReasons.IsAggregateNotSupportedForTag(new IOException("x")).ShouldBeFalse();
+ GatewayErrorReasons.ReasonOf(new IOException("x")).ShouldBeNull();
+ }
+}
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/DiscreteTagTimeWeightedWireTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/DiscreteTagTimeWeightedWireTests.cs
index 0a68347cc..2b0e6377d 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/DiscreteTagTimeWeightedWireTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/DiscreteTagTimeWeightedWireTests.cs
@@ -70,8 +70,8 @@ public sealed class DiscreteTagTimeWeightedWireTests
///
/// lmxopcua#900: the residual #897 case — a node declared numeric whose AVEVA tag is discrete still reaches AVEVA. A
/// gateway at or after historiangw#135 answers FailedPrecondition + ErrorInfo AGGREGATE_NOT_SUPPORTED_FOR_TAG
- /// (surfaced as Client 1.0.0 surfaces it: a HistorianGatewayException wrapping the and its
- /// trailers), which is BadAggregateNotSupported for the node and no health failure. Control: an older gateway's
+ /// (surfaced as Client 1.1.0 surfaces it: a HistorianGatewayException built from the ,
+ /// carrying the typed reason), which is BadAggregateNotSupported for the node and no health failure. Control: an older gateway's
/// Internal answer for the same read keeps today's handling (not BadAggregateNotSupported, counted).
///
[Theory]
@@ -140,7 +140,7 @@ public sealed class DiscreteTagTimeWeightedWireTests
///
/// true: answer the discrete tag's refused modes as a gateway at or after historiangw#135 does (FailedPrecondition +
- /// ErrorInfo), wrapped the way Client 1.0.0's MapRpcException wraps it; false: the older Internal answer.
+ /// ErrorInfo), built the way Client 1.1.0's MapRpcException builds it; false: the older Internal answer.
///
public volatile bool SendsAggregateRefusal;