mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-29 07:07:24 +08:00
# Description Brings the server-side **OPC UA Part 13 (Aggregates) v1.05.07** support to full conformance: fixes the three genuine non-compliances found by a spec review, completes value-validated test coverage for **all 37 standard aggregate functions**, and adds documentation. The existing aggregate engine is a faithful port and was already largely compliant; this PR closes the remaining gaps. ## Compliance fixes - **`TreatUncertainAsBad` default (§4.2.1.2).** The server-default `AggregateConfiguration` (used when a `ReadProcessedDetails` request sets `UseServerCapabilitiesDefaults = true`) now defaults `TreatUncertainAsBad` to **`True`** as the spec mandates, in both `AggregateManager.GetDefaultConfiguration` and the `HistorianDispatcher` fallback. Behavior change noted in `MigrationGuide.md`. - **`AnnotationCount` (§5.4.3.20).** Previously counted raw data values. It now counts **Annotations** per interval, computed in `HistorianDispatcher.DispatchProcessedReadAsync` from `IHistorianAnnotationProvider` (endTime-exclusive bucketing; reverse-time and `ProcessingInterval == 0` handled). Returns `Bad_AggregateNotSupported` when the provider has no annotation history. - **StandardDeviation/Variance use all n Good raw values (§5.4.3.37/.39).** `ComputeStdDev` previously computed over the N−1 sub-regions returned by `GetRegionsInValueSet`, silently dropping the last raw value. It now computes the mean/variance directly over all Good raw values in the interval (UseBounds = None), differing only by the divisor (sample n−1, population n). The population spec-example tests that used to add a duplicate trailing value as a workaround were updated to the natural 5-value datasets matching §A.35/.37. ## Tests - **Calculator coverage is complete for all 37 aggregates** — added `StatusAggregateCalculatorTests` (DurationGood/Bad, PercentGood/Bad, WorstQuality/WorstQuality2), the Min/Max `*2` simple-bound variants (`Minimum2`/`Maximum2`/`Range2`/`MinimumActualTime2`/`MaximumActualTime2`), and `Interpolative`. - **End-to-end integration** in `HistoryClientIntegrationTests` over the live in-process ReferenceServer: value-validated `Average`/`Minimum`/`Maximum` processed reads and an **AnnotationCount** write→read round-trip. - **Dispatcher-level** `AnnotationCount` tests (counts per interval; `Bad_AggregateNotSupported` without an annotation provider). - **Tightened `AggregateBaseTests`**: dropped `allowAnyResult` from `StandardDeviationSample`/`StandardDeviationPopulation`/`NumberOfTransitions` basecases (now deterministic on seeded data after the D3 fix). - **Validation tests** for `AggregateConfiguration` per §4.2.1.2: `PercentDataBad`/`PercentDataGood > 100` and the invalid relationship `PercentDataGood < (100 - PercentDataBad)`. - **Cross-cutting**: reversed time-range test (start > end). ## Verification - Builds: **0 warnings / 0 errors**. - Aggregate + historian dispatcher + History integration suites green on **.NET 10** and **.NET Framework 4.8** (cross-TFM). - Black-box (MCP) check against a standalone `ConsoleReferenceServer`: both `Server.ServerCapabilities.AggregateFunctions` and `HistoryServerCapabilities.AggregateFunctions` advertise all 37 functions (§4.2.2.1). ## Notes / out of scope - AnnotationCount advertisement remains the full standard set with per-node `Bad_AggregateNotSupported`. This is spec-acceptable: §4.2.2.1 lists what the server *can* support; per-node refinement is the optional `HistoricalConfiguration.AggregateFunctions`. Conditional advertisement could be added later as a quality refinement. - A native-vs-framework-fallback parity test would be useful once a third-party `IHistorianProcessedProvider` implementation exists; the bundled `InMemoryHistorianProvider` uses only the framework fallback today. ## Related Issues _No existing tracking issue; this addresses OPC 10000-13 (Part 13, Aggregates) v1.05.07 conformance gaps. Maintainers: please link or open a tracking issue if one is required._ ## Checklist - [ ] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [ ] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. _(Ran the aggregate/historian/integration suites on net48 + net10; full-solution run pending CI.)_ - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received.
139 lines
6.7 KiB
Markdown
139 lines
6.7 KiB
Markdown
# Aggregates (OPC UA Part 13)
|
|
|
|
## Overview
|
|
|
|
The .NET Standard stack implements **OPC UA Part 13 (OPC 10000-13) v1.05.07 Aggregates** on the server
|
|
side, computed over historical data retrieved through [Historical Access](HistoricalAccess.md) (Part 11).
|
|
All **37 standard aggregate functions** are supported and advertised through the address space.
|
|
|
|
Aggregates are requested with the `HistoryRead` service using `ReadProcessedDetails` (a `startTime`,
|
|
`endTime`, `ProcessingInterval`, one or more `AggregateType` NodeIds, and an optional
|
|
`AggregateConfiguration`). The server divides the time range into intervals of `ProcessingInterval`
|
|
milliseconds and produces one aggregate value per interval.
|
|
|
|
## Supported aggregate functions
|
|
|
|
| Category | Functions |
|
|
|---|---|
|
|
| Interpolative | `Interpolative` |
|
|
| Averages / integrals | `Average`, `TimeAverage`, `TimeAverage2`, `Total`, `Total2` |
|
|
| Extrema | `Minimum`, `Maximum`, `MinimumActualTime`, `MaximumActualTime`, `Range`, `Minimum2`, `Maximum2`, `MinimumActualTime2`, `MaximumActualTime2`, `Range2` |
|
|
| Counts | `Count`, `AnnotationCount`, `DurationInStateZero`, `DurationInStateNonZero`, `NumberOfTransitions` |
|
|
| Start / end | `Start`, `End`, `Delta`, `StartBound`, `EndBound`, `DeltaBounds` |
|
|
| Quality / time-in-state | `DurationGood`, `DurationBad`, `PercentGood`, `PercentBad`, `WorstQuality`, `WorstQuality2` |
|
|
| Statistics | `StandardDeviationSample`, `VarianceSample`, `StandardDeviationPopulation`, `VariancePopulation` |
|
|
|
|
The exact per-aggregate semantics (type, bounding behaviour, timestamp, status-code rules, special cases)
|
|
follow Part 13 §5.4.3.4–§5.4.3.40.
|
|
|
|
## Architecture
|
|
|
|
```
|
|
HistoryRead (ReadProcessedDetails)
|
|
↓
|
|
AsyncCustomNodeManager / CustomNodeManager2 → HistorianDispatcher.DispatchProcessedReadAsync
|
|
↓ ├─ provider implements IHistorianProcessedProvider → native push-down
|
|
↓ └─ otherwise: framework fallback
|
|
↓ ├─ AnnotationCount → IHistorianAnnotationProvider (counts Annotations)
|
|
↓ └─ other aggregates → AggregateManager.CreateCalculator
|
|
↓ → stream raw values through IAggregateCalculator
|
|
```
|
|
|
|
- **`AggregateManager`** (`Opc.Ua.Server`) owns the registered aggregate factories, the server default
|
|
`AggregateConfiguration`, and the `MinimumProcessingInterval`. The standard functions are registered via
|
|
`Aggregators` and advertised into `Server.ServerCapabilities.AggregateFunctions` and
|
|
`HistoryServerCapabilities.AggregateFunctions`.
|
|
- **`IAggregateCalculator`** implementations (`AggregateCalculator` and the specialized
|
|
`Average`/`MinMax`/`Count`/`StartEnd`/`Status`/`StdDev` calculators) compute the aggregates from a stream
|
|
of raw `DataValue`s.
|
|
- A historian provider may compute aggregates itself by implementing `IHistorianProcessedProvider`
|
|
(native push-down). When it does not, the framework streams raw values through the calculator.
|
|
|
|
### AnnotationCount
|
|
|
|
`AnnotationCount` (Part 13 §5.4.3.20) counts **Annotations** in each interval, not raw data values.
|
|
It is therefore computed from the node's annotation history via `IHistorianAnnotationProvider`, not from
|
|
the raw-value calculator. If the resolved provider does not expose annotation history, an `AnnotationCount`
|
|
request returns `Bad_AggregateNotSupported`.
|
|
|
|
## Server configuration
|
|
|
|
`AggregateConfiguration` controls how non-Good data affects the result:
|
|
|
|
| Property | Default | Meaning |
|
|
|---|---|---|
|
|
| `TreatUncertainAsBad` | **`true`** | Whether Uncertain samples are treated as Bad when computing the aggregate `StatusCode` (Part 13 §4.2.1.2). |
|
|
| `PercentDataBad` | `100` | Minimum % of Bad data in an interval for the interval `StatusCode` to be Bad. |
|
|
| `PercentDataGood` | `100` | Minimum % of Good data in an interval for the interval `StatusCode` to be Good. |
|
|
| `UseSlopedExtrapolation` | `false` | Stepped (hold-last) vs sloped extrapolation past the last value. Ignored for Simple Bounds. |
|
|
|
|
The server's default configuration is returned by `AggregateManager.GetDefaultConfiguration(...)` and is
|
|
used whenever a request sets `AggregateConfiguration.UseServerCapabilitiesDefaults = true`. Per Part 13
|
|
§4.2.1.2 the default `TreatUncertainAsBad` value is `true`.
|
|
|
|
> **Migration note:** in earlier builds the server default used `TreatUncertainAsBad = false`. Clients that
|
|
> require the old behaviour should send an explicit `AggregateConfiguration` with
|
|
> `TreatUncertainAsBad = false` rather than relying on `UseServerCapabilitiesDefaults`. See the
|
|
> [Migration Guide](MigrationGuide.md).
|
|
|
|
`AggregateManager.MinimumProcessingInterval` bounds the smallest interval the server will accept.
|
|
|
|
## Client usage
|
|
|
|
Use the `HistoryClient` (`session.Historian()`) `ReadProcessedAsync` helper, which streams one
|
|
`DataValue` per processing interval as an `IAsyncEnumerable<DataValue>`:
|
|
|
|
```csharp
|
|
// Average of a historizing variable over the last hour, in 1-minute buckets.
|
|
DateTime end = DateTime.UtcNow;
|
|
DateTime start = end.AddHours(-1);
|
|
|
|
await foreach (DataValue value in session.Historian().ReadProcessedAsync(
|
|
nodeId,
|
|
aggregateFunctionId: ObjectIds.AggregateFunction_Average,
|
|
startTime: start,
|
|
endTime: end,
|
|
processingInterval: 60_000 /* ms */))
|
|
{
|
|
Console.WriteLine($"{value.SourceTimestamp:O}: {value.Value} ({value.StatusCode})");
|
|
}
|
|
```
|
|
|
|
Pass an explicit `AggregateConfiguration` to override the server defaults:
|
|
|
|
```csharp
|
|
await foreach (DataValue value in session.Historian().ReadProcessedAsync(
|
|
nodeId,
|
|
ObjectIds.AggregateFunction_TimeAverage,
|
|
start,
|
|
end,
|
|
processingInterval: 60_000,
|
|
configuration: new AggregateConfiguration
|
|
{
|
|
TreatUncertainAsBad = false,
|
|
PercentDataBad = 100,
|
|
PercentDataGood = 100,
|
|
UseSlopedExtrapolation = false
|
|
}))
|
|
{
|
|
// ...
|
|
}
|
|
```
|
|
|
|
Discover the aggregates a server supports with `session.Historian().GetServerCapabilitiesAsync(...)` or by
|
|
browsing `Server.ServerCapabilities.AggregateFunctions`.
|
|
|
|
## Notes and limitations
|
|
|
|
- `AnnotationCount` requires a provider with annotation history; otherwise it returns
|
|
`Bad_AggregateNotSupported`.
|
|
- The bundled in-memory historian (`InMemoryHistorianProvider`) supports all aggregates through the
|
|
framework fallback and supports annotation history, so `AnnotationCount` works out of the box.
|
|
- Custom providers can override aggregate computation by implementing `IHistorianProcessedProvider`.
|
|
|
|
## References
|
|
|
|
- OPC 10000-13 (Aggregates) v1.05.07: https://reference.opcfoundation.org/Core/Part13/v105/docs/
|
|
- [Historical Access (Part 11)](HistoricalAccess.md)
|
|
- [Migration Guide](MigrationGuide.md)
|