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; }
|
catch (Exception ex) when (ex is FormatException or InvalidCastException or OverflowException) { return 0; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadEventsAsync stays at the interface default (throws NotSupportedException) per
|
/// <summary>
|
||||||
// IHistoryProvider contract -- the OPC UA Client driver CAN forward HistoryReadEvents,
|
/// Forwards OPC UA HistoryReadEvents to the upstream server. Sends the fixed canonical
|
||||||
// but the call-site needs an EventFilter SelectClauses surface which the interface
|
/// <see cref="BuildBaseEventFilter"/> and maps the result onto <see cref="HistoricalEvent"/>
|
||||||
// doesn't carry. Landing the event-history passthrough requires extending
|
/// (the OtOpcUa server projects only those six BaseEventType fields, so a richer client
|
||||||
// IHistoryProvider.ReadEventsAsync with a filter-spec parameter; out of scope for this PR.
|
/// 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 ----
|
// ---- IHostConnectivityProbe ----
|
||||||
|
|
||||||
|
|||||||
@@ -80,16 +80,13 @@ public sealed class OpcUaClientHistoryTests
|
|||||||
TestContext.Current.CancellationToken));
|
TestContext.Current.CancellationToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Verifies ReadEventsAsync throws NotSupportedException as documented.</summary>
|
/// <summary>ReadEventsAsync requires an initialized session like the sibling history reads.</summary>
|
||||||
[Fact]
|
[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
|
using var drv = new OpcUaClientDriver(new OpcUaClientDriverOptions(), "opcua-events-uninit");
|
||||||
// deliberately inherits that default (see PR 76 commit body) because the OPC UA
|
await Should.ThrowAsync<InvalidOperationException>(async () =>
|
||||||
// client call path needs an EventFilter SelectClauses spec the interface doesn't carry.
|
await drv.ReadEventsAsync(
|
||||||
using var drv = new OpcUaClientDriver(new OpcUaClientDriverOptions(), "opcua-events-default");
|
|
||||||
await Should.ThrowAsync<NotSupportedException>(async () =>
|
|
||||||
await ((IHistoryProvider)drv).ReadEventsAsync(
|
|
||||||
sourceName: null,
|
sourceName: null,
|
||||||
startUtc: DateTime.UtcNow.AddMinutes(-5),
|
startUtc: DateTime.UtcNow.AddMinutes(-5),
|
||||||
endUtc: DateTime.UtcNow,
|
endUtc: DateTime.UtcNow,
|
||||||
|
|||||||
Reference in New Issue
Block a user