Files
lmxopcua/src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IHistoryProvider.cs
T
2026-04-26 09:46:33 -04:00

355 lines
19 KiB
C#

namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
/// <summary>
/// Driver capability for historical-data reads (OPC UA HistoryRead). Optional —
/// only drivers whose backends carry historian data implement this. Currently:
/// Galaxy (Wonderware Historian via the optional plugin), OPC UA Client (forward
/// to upstream server).
/// </summary>
public interface IHistoryProvider
{
/// <summary>
/// Read raw historical samples for a single attribute over a time range.
/// The Core wraps this with continuation-point handling.
/// </summary>
Task<HistoryReadResult> ReadRawAsync(
string fullReference,
DateTime startUtc,
DateTime endUtc,
uint maxValuesPerNode,
CancellationToken cancellationToken);
/// <summary>
/// Read processed (aggregated) samples — interval-bucketed average / min / max / etc.
/// Optional — drivers that only support raw history can throw <see cref="NotSupportedException"/>.
/// </summary>
Task<HistoryReadResult> ReadProcessedAsync(
string fullReference,
DateTime startUtc,
DateTime endUtc,
TimeSpan interval,
HistoryAggregateType aggregate,
CancellationToken cancellationToken);
/// <summary>
/// Read one sample per requested timestamp — OPC UA HistoryReadAtTime service. The
/// driver interpolates (or returns the prior-boundary sample) when no exact match
/// exists. Optional; drivers that can't interpolate throw <see cref="NotSupportedException"/>.
/// </summary>
/// <remarks>
/// Default implementation throws. Drivers opt in by overriding; keeps existing
/// <c>IHistoryProvider</c> implementations compiling without forcing a ReadAtTime path
/// they may not have a backend for.
/// </remarks>
Task<HistoryReadResult> ReadAtTimeAsync(
string fullReference,
IReadOnlyList<DateTime> timestampsUtc,
CancellationToken cancellationToken)
=> throw new NotSupportedException(
$"{GetType().Name} does not implement ReadAtTimeAsync. " +
"Drivers whose backends support at-time reads override this method.");
/// <summary>
/// Read historical alarm/event records — OPC UA HistoryReadEvents service. Distinct
/// from the live event stream — historical rows come from an event historian (Galaxy's
/// Alarm Provider history log, etc.) rather than the driver's active subscription.
/// </summary>
/// <param name="sourceName">
/// Optional filter: null means "all sources", otherwise restrict to events from that
/// source-object name. Drivers may ignore the filter if the backend doesn't support it.
/// </param>
/// <param name="startUtc">Inclusive lower bound on <c>EventTimeUtc</c>.</param>
/// <param name="endUtc">Exclusive upper bound on <c>EventTimeUtc</c>.</param>
/// <param name="maxEvents">Upper cap on returned events — the driver's backend enforces this.</param>
/// <param name="cancellationToken">Request cancellation.</param>
/// <remarks>
/// Default implementation throws. Only drivers with an event historian (Galaxy via the
/// Wonderware Alarm &amp; Events log) override. Modbus / the OPC UA Client driver stay
/// with the default and let callers see <c>BadHistoryOperationUnsupported</c>.
/// </remarks>
Task<HistoricalEventsResult> ReadEventsAsync(
string? sourceName,
DateTime startUtc,
DateTime endUtc,
int maxEvents,
CancellationToken cancellationToken)
=> throw new NotSupportedException(
$"{GetType().Name} does not implement ReadEventsAsync. " +
"Drivers whose backends have an event historian override this method.");
/// <summary>
/// Filter-aware historical event read — OPC UA HistoryReadEvents service with full
/// <c>EventFilter</c> support (SelectClauses + WhereClause). Distinct from the simpler
/// <see cref="ReadEventsAsync(string?, DateTime, DateTime, int, CancellationToken)"/>
/// overload which is sufficient for "give me the standard BaseEventType fields"
/// queries; this overload is for clients that send a custom <c>EventFilter</c> on the
/// wire (per-select-clause Variant population, where-filter evaluation).
/// </summary>
/// <param name="fullReference">
/// Driver-specific node identifier. May be a notifier object (e.g. the driver-root
/// folder) — drivers that support cluster-wide queries treat it as
/// "all sources in the namespace".
/// </param>
/// <param name="request">Filter spec — time range + select clauses + optional where clause.</param>
/// <param name="cancellationToken">Request cancellation.</param>
/// <remarks>
/// <para>
/// Default implementation throws — drivers opt in by overriding. Existing drivers
/// that only handle the parameterless overload stay green; new drivers that need
/// filter-aware event history (OPC UA Client passthrough, future event-historian
/// backends) override this method.
/// </para>
/// <para>
/// The OPC UA Client driver implements this by translating <see cref="EventHistoryRequest"/>
/// into <c>ReadEventDetails</c> and calling <c>Session.HistoryReadAsync</c> against
/// the upstream server.
/// </para>
/// </remarks>
Task<HistoricalEventBatch> ReadEventsAsync(
string fullReference,
EventHistoryRequest request,
CancellationToken cancellationToken)
=> throw new NotSupportedException(
$"{GetType().Name} does not implement filter-aware ReadEventsAsync(EventHistoryRequest). " +
"Drivers whose backends carry historical events with EventFilter support override this method.");
}
/// <summary>
/// Filter spec for the filter-aware <see cref="IHistoryProvider.ReadEventsAsync(string, EventHistoryRequest, CancellationToken)"/>
/// overload. Mirrors the OPC UA <c>ReadEventDetails</c> wire shape (StartTime, EndTime,
/// NumValuesPerNode, EventFilter) but transport-neutral so non-UA drivers can implement it
/// without taking a dependency on the UA SDK type.
/// </summary>
/// <param name="StartTime">Inclusive lower bound on event time.</param>
/// <param name="EndTime">Exclusive upper bound on event time.</param>
/// <param name="NumValuesPerNode">Maximum events per node (0 = no driver-side cap, server may still apply one).</param>
/// <param name="SelectClauses">
/// Per-field projection. Each entry names a BaseEventType-rooted field (or a
/// typed-path field via <see cref="SimpleAttributeSpec.TypeDefinitionId"/>) the caller
/// wants returned. <c>null</c> means "use the driver's default field set" — typically
/// EventId, SourceName, Time, Message, Severity, ReceiveTime.
/// </param>
/// <param name="WhereClause">
/// Optional content-filter restriction (e.g. <c>EventType OfType AlarmConditionType</c>).
/// Drivers may ignore the where clause if their backend doesn't support it; that's a
/// best-effort projection rather than a hard error.
/// </param>
public sealed record EventHistoryRequest(
DateTime StartTime,
DateTime EndTime,
uint NumValuesPerNode,
IReadOnlyList<SimpleAttributeSpec>? SelectClauses,
ContentFilterSpec? WhereClause);
/// <summary>
/// Transport-neutral mirror of OPC UA's <c>SimpleAttributeOperand</c> — picks one field
/// from a node by typed browse path. <see cref="TypeDefinitionId"/> is the OPC UA NodeId
/// of the type that the path is rooted at (e.g. <c>BaseEventType</c>); <see cref="BrowsePath"/>
/// is a sequence of QualifiedName-style segments (<c>"ns:Name"</c> or just <c>"Name"</c>
/// when ns=0). An empty <see cref="BrowsePath"/> means "the node itself".
/// </summary>
/// <param name="TypeDefinitionId">
/// Type the path is rooted at. <c>null</c> defaults to the OPC UA <c>BaseEventType</c>
/// when the driver has a UA mapping. Format is driver-specific NodeId text (e.g.
/// <c>"i=2041"</c> for BaseEventType).
/// </param>
/// <param name="BrowsePath">Browse-path segments. Empty list = the typed node itself.</param>
/// <param name="FieldName">
/// Stable key the driver uses when populating <see cref="HistoricalEventRow.Fields"/>. The
/// server-side dispatcher uses this to align the returned values with the wire-side
/// SelectClause order, even when a driver doesn't honour the BrowsePath verbatim.
/// </param>
public sealed record SimpleAttributeSpec(
string? TypeDefinitionId,
IReadOnlyList<string> BrowsePath,
string FieldName);
/// <summary>
/// Transport-neutral mirror of OPC UA's <c>ContentFilter</c>. The current shape carries the
/// raw filter operands as opaque OPC UA <c>ExtensionObject</c> bytes — drivers that need to
/// evaluate the filter (Galaxy historian) parse it themselves; the OPC UA Client driver
/// forwards it untouched. A future PR may replace this with a structured AST when more
/// than one driver needs to evaluate where-clauses locally.
/// </summary>
/// <param name="EncodedOperands">
/// Optional binary-encoded <c>ContentFilter</c> from the wire. <c>null</c> when no
/// where-clause was supplied.
/// </param>
public sealed record ContentFilterSpec(byte[]? EncodedOperands);
/// <summary>Result of a HistoryRead call.</summary>
/// <param name="Samples">Returned samples in chronological order.</param>
/// <param name="ContinuationPoint">Opaque token for the next call when more samples are available; null when complete.</param>
public sealed record HistoryReadResult(
IReadOnlyList<DataValueSnapshot> Samples,
byte[]? ContinuationPoint);
/// <summary>
/// Aggregate function for processed history reads. Mirrors the OPC UA Part 13 §5
/// standard aggregate catalog. Each value maps 1:1 onto an
/// <c>Opc.Ua.ObjectIds.AggregateFunction_*</c> NodeId — the OPC UA Client driver does the
/// translation in <c>OpcUaClientDriver.MapAggregateToNodeId</c>; other drivers either
/// evaluate the aggregate locally (Galaxy historian) or surface
/// <c>BadAggregateNotSupported</c> for the values their backend can't honour.
/// </summary>
/// <remarks>
/// <para>
/// <b>Stable ordinals.</b> The first 5 values (<see cref="Average"/>..<see cref="Count"/>)
/// carry ordinals 0-4 from the original PR — additions are appended to keep prior
/// persisted enums (config files, Admin UI dropdowns) compatible.
/// </para>
/// <para>
/// <b>Server-side support.</b> Not every upstream OPC UA server implements every
/// Part 13 aggregate. Implementations advertise their support through
/// <c>AggregateConfiguration</c> on the Server object; clients can probe it at runtime.
/// Aggregates that the upstream rejects come back with
/// <c>StatusCode=BadAggregateNotSupported</c> on the per-row HistoryRead result —
/// the driver passes that through verbatim (cascading-quality rule, Part 11 §8).
/// </para>
/// </remarks>
public enum HistoryAggregateType
{
// ---- Original 5 (ordinals 0-4 — keep stable) ----
/// <summary>Average of all values in the interval. Part 13 §5.4.</summary>
Average,
/// <summary>Minimum value in the interval. Part 13 §5.5.</summary>
Minimum,
/// <summary>Maximum value in the interval. Part 13 §5.6.</summary>
Maximum,
/// <summary>Sum of values in the interval (numeric only). Part 13 §5.10.</summary>
Total,
/// <summary>Count of Good-quality samples in the interval. Part 13 §5.18.</summary>
Count,
// ---- Time-weighted averages (Part 13 §5.4) ----
/// <summary>Time-weighted average — values held until next sample. Part 13 §5.4.2.</summary>
TimeAverage,
/// <summary>Time-weighted average using simple-bounds extrapolation. Part 13 §5.4.3.</summary>
TimeAverage2,
// ---- Interpolation (Part 13 §5.3) ----
/// <summary>Interpolated value at each interval boundary. Part 13 §5.3.</summary>
Interpolative,
// ---- Min/Max with timestamps and range (Part 13 §5.5–§5.7) ----
/// <summary>Timestamp of the minimum-value sample. Part 13 §5.5.4.</summary>
MinimumActualTime,
/// <summary>Timestamp of the maximum-value sample. Part 13 §5.6.4.</summary>
MaximumActualTime,
/// <summary>Maximum minus minimum across the interval. Part 13 §5.7.</summary>
Range,
/// <summary>Range computed using simple-bounds extrapolation. Part 13 §5.7.</summary>
Range2,
// ---- Annotation / duration / quality coverage (Part 13 §5.16–§5.21) ----
/// <summary>Number of annotations attached to samples in the interval. Part 13 §5.21.</summary>
AnnotationCount,
/// <summary>Total time (ms) covered by Good-quality data. Part 13 §5.16.</summary>
DurationGood,
/// <summary>Total time (ms) covered by Bad-quality data. Part 13 §5.16.</summary>
DurationBad,
/// <summary>Percent of the interval covered by Good-quality data (0-100). Part 13 §5.17.</summary>
PercentGood,
/// <summary>Percent of the interval covered by Bad-quality data (0-100). Part 13 §5.17.</summary>
PercentBad,
/// <summary>Worst (most-severe) quality code seen in the interval. Part 13 §5.20.</summary>
WorstQuality,
/// <summary>Worst-quality code using simple-bounds extrapolation. Part 13 §5.20.</summary>
WorstQuality2,
// ---- Statistical (Part 13 §5.13) ----
/// <summary>Sample-population standard deviation (n-1 divisor). Part 13 §5.13.</summary>
StandardDeviationSample,
/// <summary>Whole-population standard deviation (n divisor). Part 13 §5.13.</summary>
StandardDeviationPopulation,
/// <summary>Sample-population variance (n-1 divisor). Part 13 §5.13.</summary>
VarianceSample,
/// <summary>Whole-population variance (n divisor). Part 13 §5.13.</summary>
VariancePopulation,
// ---- State-based (Part 13 §5.12, §5.19) ----
/// <summary>Number of value transitions observed in the interval. Part 13 §5.12.</summary>
NumberOfTransitions,
/// <summary>Total time (ms) the value was 0 (state Zero). Part 13 §5.19.</summary>
DurationInStateZero,
/// <summary>Total time (ms) the value was non-zero (state NonZero). Part 13 §5.19.</summary>
DurationInStateNonZero,
// ---- Interval bounds and deltas (Part 13 §5.8–§5.9, §5.11) ----
/// <summary>First Good-quality sample at or after the interval start. Part 13 §5.8.</summary>
Start,
/// <summary>Last Good-quality sample at or before the interval end. Part 13 §5.9.</summary>
End,
/// <summary>End sample minus Start sample. Part 13 §5.11.</summary>
Delta,
/// <summary>Boundary value (extrapolated) at the interval start. Part 13 §5.8.</summary>
StartBound,
/// <summary>Boundary value (extrapolated) at the interval end. Part 13 §5.9.</summary>
EndBound,
}
/// <summary>
/// One row returned by the fixed-field
/// <see cref="IHistoryProvider.ReadEventsAsync(string?, DateTime, DateTime, int, CancellationToken)"/>
/// overload — a historical alarm/event record, not the OPC UA live-event stream. Fields
/// match the minimum set the Server needs to populate a <c>HistoryEventFieldList</c>
/// for HistoryReadEvents responses.
/// </summary>
/// <param name="EventId">Stable unique id for the event — driver-specific format.</param>
/// <param name="SourceName">Source object that emitted the event. May differ from the <c>sourceName</c> filter the caller passed (fuzzy matches).</param>
/// <param name="EventTimeUtc">Process-side timestamp — when the event actually occurred.</param>
/// <param name="ReceivedTimeUtc">Historian-side timestamp — when the historian persisted the row; may lag <paramref name="EventTimeUtc"/> by the historian's buffer flush cadence.</param>
/// <param name="Message">Human-readable message text.</param>
/// <param name="Severity">OPC UA severity (1-1000). Drivers map their native priority scale onto this range.</param>
public sealed record HistoricalEvent(
string EventId,
string? SourceName,
DateTime EventTimeUtc,
DateTime ReceivedTimeUtc,
string? Message,
ushort Severity);
/// <summary>Result of a <see cref="IHistoryProvider.ReadEventsAsync(string?, DateTime, DateTime, int, CancellationToken)"/> call.</summary>
/// <param name="Events">Events in chronological order by <c>EventTimeUtc</c>.</param>
/// <param name="ContinuationPoint">Opaque token for the next call when more events are available; null when complete.</param>
public sealed record HistoricalEventsResult(
IReadOnlyList<HistoricalEvent> Events,
byte[]? ContinuationPoint);
/// <summary>
/// One row returned by the filter-aware
/// <see cref="IHistoryProvider.ReadEventsAsync(string, EventHistoryRequest, CancellationToken)"/>
/// overload. Carries an open-ended <see cref="Fields"/> bag keyed by
/// <see cref="SimpleAttributeSpec.FieldName"/> (or a stable default name when no
/// SelectClauses were supplied) so the server-side dispatcher can re-align fields with
/// the client's requested order — without forcing every driver to honour the entire
/// OPC UA EventFilter shape verbatim.
/// </summary>
/// <param name="Fields">
/// SelectClause results. Keys match the <c>FieldName</c> on the corresponding
/// <see cref="SimpleAttributeSpec"/>; values are the raw .NET payload (string,
/// <c>DateTime</c>, severity int, etc.). <c>null</c> values are legitimate (the
/// upstream had a missing field).
/// </param>
/// <param name="OccurrenceTime">
/// Wall-clock event time — convenience for ordering / windowing without picking a key
/// out of <see cref="Fields"/>. Drivers populate this from the underlying event row.
/// </param>
public sealed record HistoricalEventRow(
IReadOnlyDictionary<string, object?> Fields,
DateTimeOffset OccurrenceTime);
/// <summary>
/// Result of the filter-aware
/// <see cref="IHistoryProvider.ReadEventsAsync(string, EventHistoryRequest, CancellationToken)"/>
/// overload. Mirrors <see cref="HistoricalEventsResult"/> but carries
/// <see cref="HistoricalEventRow"/> instead of the fixed-shape
/// <see cref="HistoricalEvent"/> — the server-side dispatcher unpacks the keyed fields
/// into a <c>HistoryEventFieldList</c> aligned with the client's SelectClauses.
/// </summary>
/// <param name="Events">Events in chronological order by <see cref="HistoricalEventRow.OccurrenceTime"/>.</param>
/// <param name="ContinuationPoint">Opaque token for the next call when more events are available; null when complete.</param>
public sealed record HistoricalEventBatch(
IReadOnlyList<HistoricalEventRow> Events,
byte[]? ContinuationPoint);