Auto: opcuaclient-13 — Part 13 aggregate catalog mapping

Closes #285
This commit is contained in:
Joseph Doherty
2026-04-26 09:46:33 -04:00
parent 7cbc566db9
commit 0adc5adb59
7 changed files with 537 additions and 8 deletions

View File

@@ -184,14 +184,108 @@ public sealed record HistoryReadResult(
IReadOnlyList<DataValueSnapshot> Samples,
byte[]? ContinuationPoint);
/// <summary>Aggregate function for processed history reads. Mirrors OPC UA Part 13 standard aggregates.</summary>
/// <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>

View File

@@ -2700,14 +2700,67 @@ public sealed class OpcUaClientDriver(OpcUaClientDriverOptions options, string d
finally { _gate.Release(); }
}
/// <summary>Map <see cref="HistoryAggregateType"/> to the OPC UA Part 13 standard aggregate NodeId.</summary>
/// <summary>
/// Map <see cref="HistoryAggregateType"/> to the OPC UA Part 13 standard aggregate
/// NodeId. Each enum value resolves to <c>Opc.Ua.ObjectIds.AggregateFunction_*</c>;
/// the upstream server may still reject individual aggregates with
/// <c>BadAggregateNotSupported</c> on the per-row HistoryRead result — that's a
/// server-capability signal, not a driver-side error, so callers should treat the
/// mapping itself as best-effort.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException">
/// The supplied enum value is outside the declared <see cref="HistoryAggregateType"/>
/// range — most likely a future-extension value the driver hasn't been recompiled for.
/// </exception>
internal static NodeId MapAggregateToNodeId(HistoryAggregateType aggregate) => aggregate switch
{
// ---- Original 5 (ordinals 0-4) ----
HistoryAggregateType.Average => ObjectIds.AggregateFunction_Average,
HistoryAggregateType.Minimum => ObjectIds.AggregateFunction_Minimum,
HistoryAggregateType.Maximum => ObjectIds.AggregateFunction_Maximum,
HistoryAggregateType.Total => ObjectIds.AggregateFunction_Total,
HistoryAggregateType.Count => ObjectIds.AggregateFunction_Count,
// ---- Time-weighted averages ----
HistoryAggregateType.TimeAverage => ObjectIds.AggregateFunction_TimeAverage,
HistoryAggregateType.TimeAverage2 => ObjectIds.AggregateFunction_TimeAverage2,
// ---- Interpolation ----
HistoryAggregateType.Interpolative => ObjectIds.AggregateFunction_Interpolative,
// ---- Min/Max with timestamps + range ----
HistoryAggregateType.MinimumActualTime => ObjectIds.AggregateFunction_MinimumActualTime,
HistoryAggregateType.MaximumActualTime => ObjectIds.AggregateFunction_MaximumActualTime,
HistoryAggregateType.Range => ObjectIds.AggregateFunction_Range,
HistoryAggregateType.Range2 => ObjectIds.AggregateFunction_Range2,
// ---- Annotation / duration / quality coverage ----
HistoryAggregateType.AnnotationCount => ObjectIds.AggregateFunction_AnnotationCount,
HistoryAggregateType.DurationGood => ObjectIds.AggregateFunction_DurationGood,
HistoryAggregateType.DurationBad => ObjectIds.AggregateFunction_DurationBad,
HistoryAggregateType.PercentGood => ObjectIds.AggregateFunction_PercentGood,
HistoryAggregateType.PercentBad => ObjectIds.AggregateFunction_PercentBad,
HistoryAggregateType.WorstQuality => ObjectIds.AggregateFunction_WorstQuality,
HistoryAggregateType.WorstQuality2 => ObjectIds.AggregateFunction_WorstQuality2,
// ---- Statistical ----
HistoryAggregateType.StandardDeviationSample => ObjectIds.AggregateFunction_StandardDeviationSample,
HistoryAggregateType.StandardDeviationPopulation => ObjectIds.AggregateFunction_StandardDeviationPopulation,
HistoryAggregateType.VarianceSample => ObjectIds.AggregateFunction_VarianceSample,
HistoryAggregateType.VariancePopulation => ObjectIds.AggregateFunction_VariancePopulation,
// ---- State-based ----
HistoryAggregateType.NumberOfTransitions => ObjectIds.AggregateFunction_NumberOfTransitions,
HistoryAggregateType.DurationInStateZero => ObjectIds.AggregateFunction_DurationInStateZero,
HistoryAggregateType.DurationInStateNonZero => ObjectIds.AggregateFunction_DurationInStateNonZero,
// ---- Interval bounds and deltas ----
HistoryAggregateType.Start => ObjectIds.AggregateFunction_Start,
HistoryAggregateType.End => ObjectIds.AggregateFunction_End,
HistoryAggregateType.Delta => ObjectIds.AggregateFunction_Delta,
HistoryAggregateType.StartBound => ObjectIds.AggregateFunction_StartBound,
HistoryAggregateType.EndBound => ObjectIds.AggregateFunction_EndBound,
_ => throw new ArgumentOutOfRangeException(nameof(aggregate), aggregate, null),
};