diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriver.cs
index 4dec9564..73c3a551 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriver.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriver.cs
@@ -1764,11 +1764,79 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
catch (Exception ex) when (ex is FormatException or InvalidCastException or OverflowException) { return 0; }
}
- // ReadEventsAsync stays at the interface default (throws NotSupportedException) per
- // IHistoryProvider contract -- the OPC UA Client driver CAN forward HistoryReadEvents,
- // but the call-site needs an EventFilter SelectClauses surface which the interface
- // doesn't carry. Landing the event-history passthrough requires extending
- // IHistoryProvider.ReadEventsAsync with a filter-spec parameter; out of scope for this PR.
+ ///
+ /// Forwards OPC UA HistoryReadEvents to the upstream server. Sends the fixed canonical
+ /// and maps the result onto
+ /// (the OtOpcUa server projects only those six BaseEventType fields, so a richer client
+ /// filter would be discarded server-side — the driver supplies the canonical set itself).
+ ///
+ ///
+ /// Upstream event-notifier NodeId to read from (mirrors how fullReference is the
+ /// upstream NodeId for raw reads). Null/empty → the upstream Server object (i=2253),
+ /// the standard server-wide event notifier. An unparseable id short-circuits to an empty
+ /// result, matching the raw path's malformed-NodeId behavior.
+ ///
+ /// Inclusive lower bound on event time (UTC).
+ /// Exclusive upper bound on event time (UTC).
+ /// Upper cap; <= 0 means "no cap" (NumValuesPerNode = 0).
+ /// Request cancellation.
+ /// The historical events plus the upstream continuation point (null when complete).
+ public async Task ReadEventsAsync(
+ string? sourceName, DateTime startUtc, DateTime endUtc, int maxEvents,
+ CancellationToken cancellationToken)
+ {
+ var session = RequireSession();
+
+ NodeId notifierNodeId;
+ if (string.IsNullOrEmpty(sourceName))
+ {
+ notifierNodeId = ObjectIds.Server;
+ }
+ else if (!TryParseNodeId(session, sourceName, out var parsed))
+ {
+ return new Core.Abstractions.HistoricalEventsResult([], null);
+ }
+ else
+ {
+ notifierNodeId = parsed;
+ }
+
+ var details = new ReadEventDetails
+ {
+ StartTime = startUtc,
+ EndTime = endUtc,
+ NumValuesPerNode = maxEvents <= 0 ? 0u : (uint)maxEvents,
+ Filter = BuildBaseEventFilter(),
+ };
+
+ var nodesToRead = new HistoryReadValueIdCollection
+ {
+ new HistoryReadValueId { NodeId = notifierNodeId },
+ };
+
+ await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
+ try
+ {
+ var resp = await session.HistoryReadAsync(
+ requestHeader: null,
+ historyReadDetails: new ExtensionObject(details),
+ timestampsToReturn: TimestampsToReturn.Both,
+ releaseContinuationPoints: false,
+ nodesToRead: nodesToRead,
+ ct: cancellationToken).ConfigureAwait(false);
+
+ if (resp.Results.Count == 0) return new Core.Abstractions.HistoricalEventsResult([], null);
+ var r = resp.Results[0];
+
+ var events = r.HistoryData?.Body is HistoryEvent he
+ ? MapHistoryEvents(he)
+ : [];
+
+ var contPt = r.ContinuationPoint is { Length: > 0 } ? r.ContinuationPoint : null;
+ return new Core.Abstractions.HistoricalEventsResult(events, contPt);
+ }
+ finally { _gate.Release(); }
+ }
// ---- IHostConnectivityProbe ----
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/OpcUaClientHistoryTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/OpcUaClientHistoryTests.cs
index 69eadae4..a515dac5 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/OpcUaClientHistoryTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/OpcUaClientHistoryTests.cs
@@ -80,16 +80,13 @@ public sealed class OpcUaClientHistoryTests
TestContext.Current.CancellationToken));
}
- /// Verifies ReadEventsAsync throws NotSupportedException as documented.
+ /// ReadEventsAsync requires an initialized session like the sibling history reads.
[Fact]
- public async Task ReadEventsAsync_throws_NotSupportedException_as_documented()
+ public async Task ReadEventsAsync_without_initialize_throws_InvalidOperationException()
{
- // The IHistoryProvider default implementation throws; the OPC UA Client driver
- // deliberately inherits that default (see PR 76 commit body) because the OPC UA
- // client call path needs an EventFilter SelectClauses spec the interface doesn't carry.
- using var drv = new OpcUaClientDriver(new OpcUaClientDriverOptions(), "opcua-events-default");
- await Should.ThrowAsync(async () =>
- await ((IHistoryProvider)drv).ReadEventsAsync(
+ using var drv = new OpcUaClientDriver(new OpcUaClientDriverOptions(), "opcua-events-uninit");
+ await Should.ThrowAsync(async () =>
+ await drv.ReadEventsAsync(
sourceName: null,
startUtc: DateTime.UtcNow.AddMinutes(-5),
endUtc: DateTime.UtcNow,