feat(opcuaclient): implement IHistoryProvider.ReadEventsAsync passthrough
This commit is contained in:
@@ -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.
|
||||
/// <summary>
|
||||
/// Forwards OPC UA HistoryReadEvents to the upstream server. Sends the fixed canonical
|
||||
/// <see cref="BuildBaseEventFilter"/> and maps the result onto <see cref="HistoricalEvent"/>
|
||||
/// (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).
|
||||
/// </summary>
|
||||
/// <param name="sourceName">
|
||||
/// Upstream event-notifier NodeId to read from (mirrors how <c>fullReference</c> is the
|
||||
/// upstream NodeId for raw reads). Null/empty → the upstream Server object (<c>i=2253</c>),
|
||||
/// the standard server-wide event notifier. An unparseable id short-circuits to an empty
|
||||
/// result, matching the raw path's malformed-NodeId behavior.
|
||||
/// </param>
|
||||
/// <param name="startUtc">Inclusive lower bound on event time (UTC).</param>
|
||||
/// <param name="endUtc">Exclusive upper bound on event time (UTC).</param>
|
||||
/// <param name="maxEvents">Upper cap; <c><= 0</c> means "no cap" (NumValuesPerNode = 0).</param>
|
||||
/// <param name="cancellationToken">Request cancellation.</param>
|
||||
/// <returns>The historical events plus the upstream continuation point (null when complete).</returns>
|
||||
public async Task<Core.Abstractions.HistoricalEventsResult> 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 ----
|
||||
|
||||
|
||||
@@ -80,16 +80,13 @@ public sealed class OpcUaClientHistoryTests
|
||||
TestContext.Current.CancellationToken));
|
||||
}
|
||||
|
||||
/// <summary>Verifies ReadEventsAsync throws NotSupportedException as documented.</summary>
|
||||
/// <summary>ReadEventsAsync requires an initialized session like the sibling history reads.</summary>
|
||||
[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<NotSupportedException>(async () =>
|
||||
await ((IHistoryProvider)drv).ReadEventsAsync(
|
||||
using var drv = new OpcUaClientDriver(new OpcUaClientDriverOptions(), "opcua-events-uninit");
|
||||
await Should.ThrowAsync<InvalidOperationException>(async () =>
|
||||
await drv.ReadEventsAsync(
|
||||
sourceName: null,
|
||||
startUtc: DateTime.UtcNow.AddMinutes(-5),
|
||||
endUtc: DateTime.UtcNow,
|
||||
|
||||
Reference in New Issue
Block a user