Files
Marc Schier 0000513cde Pluggable user-identity provider model (#3807)
# Description

Introduces a pluggable user-identity provider model covering all OPC UA
Part 6 §6.5 mechanisms (Anonymous, UserName, X509, IssuedToken/JWT) on
both client and server, with full
`Microsoft.Extensions.DependencyInjection` integration, plus the Part 12
§9 `AuthorizationService` two-phase token-request flow **including
refresh-token rotation**, the Part 12 §8 KeyCredential Push model, and a
clean provider model for OPC 10000-12 §7.2 `ApplicationSelfAdmin`. The
work is split across phase-by-phase commits so it can be reviewed
independently; nothing here changes wire-level behaviour or forces
existing applications to migrate.

### Documentation

All under `Docs/`:

- `IdentityProviders.md` — full developer guide: three-layer model,
recipes, configuration reference, "what gets auto-wired" matrix, JWS
algorithm table, identity-augmenters section, **"Implementing your own
provider"** how-to subsections for Entra / OIDC / Windows / AspNetCore
(sibling files merged in per review feedback).
- `DependencyInjection.md` — `Identity` sections with `appsettings.json`
examples, identity-extension matrix, `WithAuthorizationService` +
`WithKeyCredentialPush` + `AddIdentityAugmenter` extensions documented.
- `RoleBasedUserManagement.md` — claim-criteria callout describing
`IIdentityClaims` probe surface.
- `AuthorizationService.md` — Architecture diagram +
`IAccessTokenProvider` conceptual narrative restored from master; modern
`StartRequestToken` / `FinishRequestToken` flow leads; refresh-token
recipe added.
- `KeyCredentialService.md` — Push model + Pull vs Push vs Bridge
decision matrix + Hybrid (Push + Pull) section; explicit experimental
warning for the bridge.
- `GDS.md` — KeyCredential Push, `StartRequestToken`,
`GdsApplicationSelfAdminProvider`, **shipped Refresh-token support with
rotation policy and audit semantics** all documented.
- `MigrationGuide.md` — User Identity Providers section listing
`[Obsolete]` markers + mechanical recipes; SelfAdmin migration via
`IIdentityAugmenter`; `EnableRefreshTokens` default note.
- `README.md` index entries.

## Checklist

- [x] 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.
- [x] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [x] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [x] I have addressed **all** PR feedback received.
2026-06-02 01:14:54 +02:00

1519 lines
52 KiB
C#

/* ========================================================================
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
*
* OPC Foundation MIT License 1.00
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* The complete license agreement can be found here:
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
namespace Opc.Ua.Server
{
/// <summary>
/// Calculates the value of an aggregate.
/// </summary>
public class AggregateCalculator : IAggregateCalculator
{
/// <summary>
/// Create calculator
/// </summary>
[Obsolete("Use constructor with ITelemetryContext")]
public AggregateCalculator(
NodeId aggregateId,
DateTimeUtc startTime,
DateTimeUtc endTime,
double processingInterval,
bool stepped,
AggregateConfiguration configuration)
: this(aggregateId, startTime, endTime, processingInterval, stepped, configuration, null!)
{
}
/// <summary>
/// Initializes the calculation stream.
/// </summary>
/// <param name="aggregateId">The aggregate function to apply.</param>
/// <param name="startTime">The start time.</param>
/// <param name="endTime">The end time.</param>
/// <param name="processingInterval">The processing interval.</param>
/// <param name="stepped">Whether to use stepped interpolation.</param>
/// <param name="configuration">The aggregate configuration.</param>
/// <param name="telemetry">The telemetry context to use to create obvservability instruments</param>
public AggregateCalculator(
NodeId aggregateId,
DateTimeUtc startTime,
DateTimeUtc endTime,
double processingInterval,
bool stepped,
AggregateConfiguration configuration,
ITelemetryContext telemetry)
{
m_logger = telemetry.CreateLogger<AggregateCalculator>();
AggregateId = aggregateId;
StartTime = startTime;
EndTime = endTime;
ProcessingInterval = processingInterval;
Stepped = stepped;
Configuration = configuration;
TimeFlowsBackward = endTime < startTime;
if (processingInterval == 0)
{
if (endTime == DateTimeUtc.MinValue || startTime == DateTimeUtc.MinValue)
{
throw new ArgumentException(
"Non-zero processingInterval required.",
nameof(processingInterval));
}
ProcessingInterval = Math.Abs((endTime - startTime).TotalMilliseconds);
}
m_values = new LinkedList<DataValue>();
}
/// <summary>
/// The aggregate function applied by the calculator.
/// </summary>
public NodeId AggregateId { get; }
/// <summary>
/// Queues a raw value for processing.
/// </summary>
/// <param name="value">The data value to process.</param>
/// <returns>True if successful, false if the timestamp has been superseded by values already in the stream.</returns>
public bool QueueRawValue(DataValue value)
{
// ignore bad data.
if (value.IsNull)
{
return false;
}
// ignore placeholders in the stream.
if (value.StatusCode.CodeBits == StatusCodes.BadNoData)
{
return true;
}
// check for start of data.
if (m_startOfData == DateTimeUtc.MinValue)
{
m_startOfData = value.SourceTimestamp;
}
// update end of data.
m_endOfData = value.SourceTimestamp;
// ensure values are being queued in the right order.
if (TimeFlowsBackward)
{
if (m_values.First != null && CompareTimestamps(value, m_values.First) > 0)
{
return false;
}
}
else if (m_values.Last != null && CompareTimestamps(value, m_values.Last) < 0)
{
return false;
}
// ensure value list is always ordered from past to future.
if (TimeFlowsBackward)
{
m_values.AddFirst(value);
}
else
{
m_values.AddLast(value);
}
return true;
}
/// <summary>
/// Tries to produce the next processed value.
/// </summary>
/// <param name="returnPartial">If true a partial interval should be processed.</param>
/// <param name="value">The processed value when this method returns <c>true</c>.</param>
/// <returns><c>true</c> if a processed value is available; otherwise <c>false</c>.</returns>
public bool TryGetProcessedValue(bool returnPartial, out DataValue value)
{
// check if all done.
if (Complete)
{
value = default;
return false;
}
// update the slice.
if (CurrentSlice == null)
{
CurrentSlice = CreateSlice(null!);
}
else
{
UpdateSlice(CurrentSlice);
}
// check if a value can be produced.
if (!CurrentSlice.Complete && !returnPartial)
{
value = default;
return false;
}
// check if the slice extends beyond the range of available data.
DateTimeUtc earlyTime = CurrentSlice.StartTime;
DateTimeUtc lateTime = CurrentSlice.EndTime;
if (CompareTimestamps(lateTime, m_values.First!) < 0 ||
CompareTimestamps(earlyTime, m_values.Last!) > 0)
{
CurrentSlice.OutOfDataRange = true;
}
m_logger.LogTrace("Computing Aggregate {StartTime:HH:mm:ss.fff}", CurrentSlice.StartTime);
// compute the value.
DataValue computed = ComputeValue(CurrentSlice);
// check if overlapping the start of data.
if (SetPartialBit)
{
if (m_startOfData > earlyTime && m_startOfData < lateTime)
{
computed = computed.WithStatus(computed.StatusCode.WithAggregateBits(
computed.StatusCode.AggregateBits | AggregateBits.Partial));
}
if (!UsingExtrapolation &&
!TimeFlowsBackward &&
m_endOfData >= earlyTime &&
m_endOfData < lateTime)
{
computed = computed.WithStatus(computed.StatusCode.WithAggregateBits(
computed.StatusCode.AggregateBits | AggregateBits.Partial));
}
}
// force value to null if status code is bad.
if (StatusCode.IsBad(computed.StatusCode))
{
computed = computed.WithWrappedValue(Variant.Null);
}
// delete unneeded data.
if (TimeFlowsBackward)
{
if (CurrentSlice.LateBound != null)
{
LinkedListNode<DataValue>? ii = CurrentSlice.LateBound.Next;
while (ii != null)
{
LinkedListNode<DataValue>? next = ii.Next;
m_values.Remove(ii);
ii = next;
}
}
}
else if (CurrentSlice.EarlyBound != null)
{
LinkedListNode<DataValue>? ii = CurrentSlice.EarlyBound.Previous;
if (CurrentSlice.SecondEarlyBound != null)
{
ii = CurrentSlice.SecondEarlyBound.Previous;
}
while (ii != null)
{
LinkedListNode<DataValue>? next = ii.Previous;
m_values.Remove(ii);
ii = next;
}
}
// check if more to be done.
Complete =
(!TimeFlowsBackward && CurrentSlice.EndTime >= EndTime) ||
(TimeFlowsBackward && CurrentSlice.StartTime <= EndTime);
if (Complete)
{
// check if overlapping the end of data.
if (SetPartialBit &&
!UsingExtrapolation &&
!TimeFlowsBackward &&
m_endOfData >= earlyTime &&
m_endOfData < lateTime)
{
computed = computed.WithStatus(computed.StatusCode.WithAggregateBits(
computed.StatusCode.AggregateBits | AggregateBits.Partial));
}
}
else
{
CurrentSlice = CreateSlice(CurrentSlice);
}
// return the processed value.
value = computed;
return true;
}
/// <summary>
/// Returns true if the specified time is later than the end of the current interval.
/// </summary>
/// <remarks>Return true if time flows forward and the time is later than the end time.</remarks>
public bool HasEndTimePassed(DateTimeUtc currentTime)
{
if (CurrentSlice == null)
{
return false;
}
if (TimeFlowsBackward)
{
return CurrentSlice.EndTime >= currentTime;
}
return CurrentSlice.EndTime <= currentTime;
}
/// <summary>
/// The start time for the request.
/// </summary>
protected DateTimeUtc StartTime { get; }
/// <summary>
/// The end time for the request.
/// </summary>
protected DateTimeUtc EndTime { get; }
/// <summary>
/// The processing interval for the request.
/// </summary>
protected double ProcessingInterval { get; }
/// <summary>
/// True if the data series requires stepped interpolation.
/// </summary>
protected bool Stepped { get; }
/// <summary>
/// The configuration to use when processing.
/// </summary>
protected AggregateConfiguration Configuration { get; }
/// <summary>
/// Whether to use the server timestamp for all processing.
/// </summary>
protected bool UseServerTimestamp { get; }
/// <summary>
/// True if data is being processed in reverse order.
/// </summary>
protected bool TimeFlowsBackward { get; }
/// <summary>
/// Whether to use the server timestamp for all processing.
/// </summary>
protected TimeSlice CurrentSlice { get; private set; } = null!;
/// <summary>
/// True if all values required for the request have been received and processed
/// </summary>
protected bool Complete { get; private set; }
/// <summary>
/// True if the GetProcessedValue method should set the Partial bit when appropriate.
/// </summary>
protected bool SetPartialBit { get; set; }
/// <summary>
/// True if data is extrapolated after the end of data.
/// </summary>
protected bool UsingExtrapolation { get; set; }
/// <summary>
/// Compares timestamps for two DataValues according to the current UseServerTimestamp setting.
/// </summary>
/// <param name="value1">The first value to compare.</param>
/// <param name="value2">The second value to compare.</param>
/// <returns>Less than 0 if value1 is earlier than value2; 0 if they are equal; Greater than zero otherwise.</returns>
protected int CompareTimestamps(DataValue value1, DataValue value2)
{
if (value1.IsNull)
{
return value2.IsNull ? 0 : -1;
}
if (value2.IsNull)
{
return +1;
}
if (UseServerTimestamp)
{
int result = value1.ServerTimestamp.CompareTo(value2.ServerTimestamp);
if (result == 0)
{
return value1.ServerPicoseconds.CompareTo(value2.ServerPicoseconds);
}
return result;
}
else
{
int result = value1.SourceTimestamp.CompareTo(value2.SourceTimestamp);
if (result == 0)
{
return value1.SourcePicoseconds.CompareTo(value2.SourcePicoseconds);
}
return result;
}
}
/// <summary>
/// Compares timestamps for two DataValues according to the current UseServerTimestamp setting.
/// </summary>
/// <param name="value1">The first value to compare.</param>
/// <param name="value2">The second value to compare.</param>
/// <returns>Less than 0 if value1 is earlier than value2; 0 if they are equal; Greater than zero otherwise.</returns>
protected int CompareTimestamps(DataValue value1, LinkedListNode<DataValue> value2)
{
if (value2 == null)
{
return value1.IsNull ? 0 : +1;
}
return CompareTimestamps(value1, value2.Value);
}
/// <summary>
/// Compares timestamps for two DataValues according to the current UseServerTimestamp setting.
/// </summary>
/// <param name="value1">The first value to compare.</param>
/// <param name="value2">The second value to compare.</param>
/// <returns>Less than 0 if value1 is earlier than value2; 0 if they are equal; Greater than zero otherwise.</returns>
protected int CompareTimestamps(
LinkedListNode<DataValue> value1,
LinkedListNode<DataValue> value2)
{
if (value1 == null)
{
return value2 == null ? 0 : -1;
}
if (value2 == null)
{
return +1;
}
return CompareTimestamps(value1.Value, value2.Value);
}
/// <summary>
/// Compares timestamps for a timestamp to a DataValue according to the current UseServerTimestamp setting.
/// </summary>
/// <param name="value1">The timestamp to compare.</param>
/// <param name="value2">The data value to compare.</param>
/// <returns>Less than 0 if value1 is earlier than value2; 0 if they are equal; Greater than zero otherwise.</returns>
protected int CompareTimestamps(DateTimeUtc value1, LinkedListNode<DataValue> value2)
{
if (value2 == null || value2.Value.IsNull)
{
return +1;
}
if (UseServerTimestamp)
{
return value1.CompareTo(value2.Value.ServerTimestamp);
}
return value1.CompareTo(value2.Value.SourceTimestamp);
}
/// <summary>
/// Checks if the value is good according to the configuration rules.
/// </summary>
/// <param name="value">The value to test.</param>
/// <returns>True if the value is good.</returns>
protected bool IsGood(DataValue value)
{
if (value.IsNull)
{
return false;
}
if (Configuration.TreatUncertainAsBad)
{
if (StatusCode.IsNotGood(value.StatusCode))
{
return false;
}
}
else if (StatusCode.IsBad(value.StatusCode))
{
return false;
}
return true;
}
/// <summary>
/// Stores information about a slice of data to be processed.
/// </summary>
protected class TimeSlice
{
/// <summary>
/// The start time for the slice.
/// </summary>
public DateTimeUtc StartTime { get; set; }
/// <summary>
/// The end time for the slice.
/// </summary>
public DateTimeUtc EndTime { get; set; }
/// <summary>
/// True if the slice is a partial interval.
/// </summary>
public bool Partial { get; set; }
/// <summary>
/// True if all of the data required to process the slice has been collected.
/// </summary>
public bool Complete { get; set; }
/// <summary>
/// True if the slice includes times that are outside of the available dataset.
/// </summary>
public bool OutOfDataRange { get; set; }
/// <summary>
/// The first early bound for the slice.
/// </summary>
public LinkedListNode<DataValue> EarlyBound { get; set; } = null!;
/// <summary>
/// The second early bound for the slice (always earlier than the first).
/// </summary>
public LinkedListNode<DataValue> SecondEarlyBound { get; set; } = null!;
/// <summary>
/// The beginning of the slice.
/// </summary>
public LinkedListNode<DataValue> Begin { get; set; } = null!;
/// <summary>
/// The end of the slice.
/// </summary>
public LinkedListNode<DataValue> End { get; set; } = null!;
/// <summary>
/// The late bound for the slice.
/// </summary>
public LinkedListNode<DataValue> LateBound { get; set; } = null!;
/// <summary>
/// The last value which was processed.
/// </summary>
public LinkedListNode<DataValue> LastProcessedValue { get; set; } = null!;
}
/// <summary>
/// Creates a new time slice to process.
/// </summary>
/// <param name="previousSlice">The previous processed slice.</param>
/// <returns>The new time slice.</returns>
protected TimeSlice CreateSlice(TimeSlice previousSlice)
{
var slice = new TimeSlice();
// ensure slice is oriented from past to future even if request is going backwards.
if (TimeFlowsBackward)
{
if (previousSlice == null)
{
slice.EndTime = StartTime;
}
else
{
slice.EndTime = previousSlice.StartTime;
}
slice.StartTime = slice.EndTime.SubtractMilliseconds(ProcessingInterval);
// check for end of request.
if (slice.StartTime < EndTime)
{
slice.StartTime = EndTime;
slice.Partial = true;
}
}
else
{
if (previousSlice == null)
{
slice.StartTime = StartTime;
}
else
{
slice.StartTime = previousSlice.EndTime;
}
slice.EndTime = slice.StartTime.AddMilliseconds(ProcessingInterval);
// check for end of request.
if (slice.EndTime > EndTime)
{
slice.EndTime = EndTime;
slice.Partial = true;
}
}
// update the slice with current data.
UpdateSlice(slice);
return slice;
}
/// <summary>
/// Creates a new time slice to process.
/// </summary>
/// <param name="slice">The slice to update.</param>
/// <returns>True if the slice is complete.</returns>
protected bool UpdateSlice(TimeSlice slice)
{
// check if nothing to do.
if (m_values.First == null)
{
return slice.Complete;
}
// restart processing from where it left off.
LinkedListNode<DataValue>? start = m_values.First;
if (!TimeFlowsBackward && slice.LastProcessedValue != null)
{
start = slice.LastProcessedValue.Next;
}
// reset the begin bound each time we go through the values.
if (TimeFlowsBackward)
{
slice.Begin = null!;
}
// initialize slice from value list.
for (LinkedListNode<DataValue>? ii = start; ii != null; ii = ii.Next)
{
if (TimeFlowsBackward)
{
// check if before the beginning of the slice.
if (CompareTimestamps(slice.StartTime, ii) >= 0)
{
if (IsGood(ii.Value))
{
slice.SecondEarlyBound = slice.EarlyBound;
slice.EarlyBound = ii;
}
continue;
}
// check if after the end if the slice.
if (CompareTimestamps(slice.EndTime, ii) < 0)
{
if (IsGood(ii.Value))
{
slice.LateBound = ii;
break;
}
continue;
}
// save first value in the slice.
slice.End ??= ii;
// save end of slice.
if (slice.Begin == null)
{
slice.Begin = ii;
slice.LastProcessedValue = ii;
}
}
else
{
// check if before the beginning of the slice.
if (CompareTimestamps(slice.StartTime, ii) > 0)
{
if (IsGood(ii.Value))
{
slice.SecondEarlyBound = slice.EarlyBound;
slice.EarlyBound = ii;
slice.LastProcessedValue = ii;
}
continue;
}
// check if after the end if the slice.
if (CompareTimestamps(slice.EndTime, ii) < 0)
{
if (IsGood(ii.Value))
{
slice.LateBound = ii;
slice.LastProcessedValue = ii;
break;
}
continue;
}
// save first value in the slice.
slice.Begin ??= ii;
// save end of slice.
slice.End = ii;
slice.LastProcessedValue = ii;
}
}
// check if no more data needs to be collected.
LinkedListNode<DataValue> requiredBound;
if (TimeFlowsBackward)
{
// only need second early bound if using sloped extrapolation and there is no late bound.
if (Configuration.UseSlopedExtrapolation && slice.LateBound == null)
{
requiredBound = slice.SecondEarlyBound;
}
else
{
requiredBound = slice.EarlyBound;
}
}
else
{
requiredBound = slice.LateBound;
}
// all done if required bound exists.
if (requiredBound != null)
{
slice.Complete = true;
}
return slice.Complete;
}
/// <summary>
/// Calculates the value for the timeslice.
/// </summary>
/// <param name="slice">The slice to process.</param>
/// <returns>The processed value.</returns>
protected virtual DataValue ComputeValue(TimeSlice slice)
{
return Interpolate(slice);
}
/// <summary>
/// Calculate the interpolate aggregate for the timeslice.
/// </summary>
protected DataValue Interpolate(TimeSlice slice)
{
if (TimeFlowsBackward)
{
return Interpolate(slice.EndTime, slice);
}
return Interpolate(slice.StartTime, slice);
}
/// <summary>
/// Return a value indicating there is no data in the time slice.
/// </summary>
protected DataValue GetNoDataValue(TimeSlice slice)
{
if (TimeFlowsBackward)
{
return GetNoDataValue(slice.EndTime);
}
return GetNoDataValue(slice.StartTime);
}
/// <summary>
/// Returns the timestamp to use for the slice value.
/// </summary>
protected DateTimeUtc GetTimestamp(TimeSlice slice)
{
if (TimeFlowsBackward)
{
return slice.EndTime;
}
return slice.StartTime;
}
/// <summary>
/// Return a value indicating there is no data in the time slice.
/// </summary>
protected DataValue GetNoDataValue(DateTimeUtc timestamp)
{
return new DataValue(Variant.Null, StatusCodes.BadNoData, timestamp, timestamp);
}
/// <summary>
/// Interpolates a value at the timestamp.
/// </summary>
/// <param name="timestamp">The timestamp.</param>
/// <param name="reference">The timeslice that contains the timestamp.</param>
/// <returns>The interpolated value.</returns>
protected DataValue Interpolate(DateTimeUtc timestamp, TimeSlice reference)
{
var slice = new TimeSlice { StartTime = timestamp, EndTime = timestamp };
UpdateSlice(slice);
// check for value at the timestamp.
if (slice.Begin != null && IsGood(slice.Begin.Value))
{
return slice.Begin.Value;
}
bool stepped = Stepped;
DataValue dataValue;
// check if the required bounds are available.
if (!Stepped)
{
// check if sloped interpolation is possible.
if (slice.EarlyBound != null && slice.LateBound != null)
{
dataValue = SlopedInterpolate(
timestamp,
slice.EarlyBound.Value,
slice.LateBound.Value);
if (!ReferenceEquals(slice.EarlyBound.Next, slice.LateBound))
{
dataValue = dataValue.WithStatus(dataValue.StatusCode
.WithCodeBits(StatusCodes.UncertainDataSubNormal));
}
return dataValue;
}
// check if extrapolation is possible.
if (slice.EarlyBound != null)
{
if (Configuration.UseSlopedExtrapolation &&
slice.SecondEarlyBound != null)
{
UsingExtrapolation = true;
dataValue = SlopedInterpolate(
timestamp,
slice.SecondEarlyBound.Value,
slice.EarlyBound.Value);
dataValue = dataValue.WithStatus(dataValue.StatusCode
.WithCodeBits(StatusCodes.UncertainDataSubNormal));
return dataValue;
}
// do stepped extrapolation.
stepped = true;
}
}
// do stepped interpolation.
if (stepped && slice.EarlyBound != null)
{
dataValue = SteppedInterpolate(timestamp, slice.EarlyBound.Value);
if (slice.EarlyBound.Next == null ||
CompareTimestamps(timestamp, slice.EarlyBound.Next) > 0)
{
UsingExtrapolation = true;
dataValue = dataValue.WithStatus(dataValue.StatusCode
.WithCodeBits(StatusCodes.UncertainDataSubNormal));
}
return dataValue;
}
// no data found.
return GetNoDataValue(timestamp);
}
/// <summary>
/// Calculate the value at the timestamp using slopped interpolation.
/// </summary>
public static DataValue SteppedInterpolate(DateTimeUtc timestamp, DataValue earlyBound)
{
// can't interpolate if no start bound.
if (StatusCode.IsBad(earlyBound.StatusCode))
{
return new DataValue(Variant.Null, StatusCodes.BadNoData, timestamp, timestamp);
}
var dataValue = new DataValue(
earlyBound.WrappedValue,
StatusCodes.Good,
timestamp,
timestamp);
// update status code.
if (StatusCode.IsBad(earlyBound.StatusCode))
{
dataValue = dataValue.WithStatus(StatusCodes.BadNoData);
}
// update status code.
if (StatusCode.IsNotGood(earlyBound.StatusCode))
{
dataValue = dataValue.WithStatus(StatusCodes.UncertainDataSubNormal);
}
return dataValue.WithStatus(dataValue.StatusCode
.WithAggregateBits(AggregateBits.Interpolated));
}
/// <summary>
/// Calculate the value at the timestamp using slopped interpolation.
/// </summary>
public static DataValue SlopedInterpolate(
DateTimeUtc timestamp,
DataValue earlyBound,
DataValue lateBound)
{
try
{
// can't interpolate if no start bound.
if (StatusCode.IsBad(earlyBound.StatusCode))
{
return new DataValue(Variant.Null, StatusCodes.BadNoData, timestamp, timestamp);
}
// revert to stepped if no end bound.
if (StatusCode.IsBad(lateBound.StatusCode))
{
DataValue dataValue2 = SteppedInterpolate(timestamp, earlyBound);
if (StatusCode.IsNotBad(dataValue2.StatusCode))
{
dataValue2 = dataValue2.WithStatus(dataValue2.StatusCode
.WithCodeBits(StatusCodes.UncertainDataSubNormal));
}
return dataValue2;
}
// convert to doubles.
double earlyValue = CastToDouble(earlyBound);
double lateValue = CastToDouble(lateBound);
// do interpolation.
double range = (lateBound.SourceTimestamp - earlyBound.SourceTimestamp).TotalMilliseconds;
double slope = (lateValue - earlyValue) / range;
double calculatedValue =
(slope * (timestamp - earlyBound.SourceTimestamp).TotalMilliseconds) +
earlyValue;
// convert back to original type.
var dataValue = new DataValue(
CastToOriginalType(calculatedValue, earlyBound),
StatusCodes.Good,
timestamp,
timestamp);
// update status code.
if (StatusCode.IsNotGood(earlyBound.StatusCode) ||
StatusCode.IsNotGood(lateBound.StatusCode))
{
dataValue = dataValue.WithStatus(StatusCodes.UncertainDataSubNormal);
}
return dataValue.WithStatus(dataValue.StatusCode
.WithAggregateBits(AggregateBits.Interpolated));
}
// exception occurs on data conversion errors.
catch (Exception)
{
return new DataValue(
Variant.Null,
StatusCodes.BadTypeMismatch,
timestamp,
timestamp);
}
}
/// <summary>
/// Converts the value to a double for use in calculations (throws exceptions if conversion fails).
/// </summary>
protected static double CastToDouble(DataValue value)
{
return value.WrappedValue.ConvertToDouble().GetDouble();
}
/// <summary>
/// Converts the value back to its original type (throws exceptions if conversion fails).
/// </summary>
protected static Variant CastToOriginalType(double value, DataValue original)
{
return Variant.From(value).ConvertTo(original.WrappedValue.TypeInfo.BuiltInType);
}
/// <summary>
/// Returns the simple bound for the timestamp.
/// </summary>
protected DataValue GetSimpleBound(DateTimeUtc timestamp, TimeSlice slice)
{
// choose the start point
LinkedListNode<DataValue>? start = slice.EarlyBound ?? m_values.First;
// look for a raw value at or immediately before the timestamp.
LinkedListNode<DataValue>? startBound = start;
for (LinkedListNode<DataValue>? ii = start; ii != null; ii = ii.Next)
{
// check for an exact match.
if (CompareTimestamps(timestamp, ii) == 0)
{
return ii.Value.Copy();
}
// looking for an end bound.
if (CompareTimestamps(timestamp, ii) < 0)
{
// only can find an end bound.
if (ii.Previous == null)
{
return GetNoDataValue(timestamp);
}
startBound = ii.Previous;
break;
}
// update start bound.
startBound = ii;
}
// check if no data found or if start bound is bad..
if (startBound == null || !IsGood(startBound.Value))
{
return GetNoDataValue(timestamp);
}
// look for an end bound.
bool revertToStepped = false;
LinkedListNode<DataValue>? endBound = startBound.Next;
if (!Stepped)
{
if (endBound != null)
{
// do sloped interpolation if two good bounds exist.
if (IsGood(endBound.Value))
{
return SlopedInterpolate(timestamp, startBound.Value, endBound.Value);
}
}
// have to use stepped because end bound is not good.
revertToStepped = true;
}
// check if end of data.
if (startBound.Next == null)
{
return GetNoDataValue(timestamp);
}
// do stepped interpolation for all other cases.
DataValue value = SteppedInterpolate(timestamp, startBound.Value);
// need to make it uncertain if interpolation was required but not used.
if (StatusCode.IsGood(value.StatusCode) && revertToStepped)
{
value = value.WithStatus(StatusCodes.UncertainDataSubNormal);
value = value.WithStatus(value.StatusCode.WithAggregateBits(AggregateBits.Interpolated));
}
return value;
}
/// <summary>
/// Returns the values in the list with simple bounds.
/// </summary>
protected List<DataValue>? GetValuesWithSimpleBounds(TimeSlice slice)
{
// check if slice is beyond end of available data.
if (CompareTimestamps(slice.StartTime, m_values.Last!) > 0 ||
CompareTimestamps(slice.EndTime, m_values.First!) < 0)
{
return null;
}
var values = new List<DataValue>();
// add the start point.
DataValue startBound = GetSimpleBound(slice.StartTime, slice);
if (!startBound.IsNull)
{
values.Add(startBound);
}
// initialize slice from value list.
for (LinkedListNode<DataValue>? ii = slice.Begin; ii != null; ii = ii.Next)
{
if (CompareTimestamps(slice.EndTime, ii) <= 0)
{
break;
}
if (CompareTimestamps(slice.StartTime, ii) < 0)
{
values.Add(ii.Value);
}
}
// add the end point.
DataValue endBound = GetSimpleBound(slice.EndTime, slice);
if (!endBound.IsNull)
{
values.Add(endBound);
}
return values;
}
/// <summary>
/// Returns the values between the start time and the end time for the slice.
/// </summary>
protected List<DataValue>? GetValues(TimeSlice slice)
{
// check if slice is beyond end of available data.
if (CompareTimestamps(slice.StartTime, m_values.Last!) > 0 ||
CompareTimestamps(slice.EndTime, m_values.First!) < 0)
{
return null;
}
var values = new List<DataValue>();
// initialize slice from value list.
for (LinkedListNode<DataValue>? ii = slice.Begin; ii != null; ii = ii.Next)
{
if (TimeFlowsBackward)
{
if (CompareTimestamps(slice.EndTime, ii) < 0)
{
break;
}
if (CompareTimestamps(slice.StartTime, ii) < 0)
{
values.Add(ii.Value);
}
}
else
{
if (CompareTimestamps(slice.EndTime, ii) <= 0)
{
break;
}
if (CompareTimestamps(slice.StartTime, ii) <= 0)
{
values.Add(ii.Value);
}
}
}
return values;
}
/// <summary>
/// Returns the values in the list with interpolated bounds.
/// </summary>
protected List<DataValue>? GetValuesWithInterpolatedBounds(TimeSlice slice)
{
// check if slice is before the available data.
if (CompareTimestamps(slice.EndTime, m_values.First!) < 0)
{
return null;
}
var values = new List<DataValue>();
// add the start point.
DataValue startBound = Interpolate(slice.StartTime, slice);
if (!startBound.IsNull)
{
values.Add(startBound);
}
// initialize slice from value list.
for (LinkedListNode<DataValue>? ii = slice.Begin; ii != null; ii = ii.Next)
{
if (CompareTimestamps(slice.EndTime, ii) <= 0)
{
break;
}
if (CompareTimestamps(slice.StartTime, ii) <= 0)
{
values.Add(ii.Value);
}
}
// add the end point.
DataValue endBound = Interpolate(slice.EndTime, slice);
if (!endBound.IsNull)
{
values.Add(endBound);
}
return values;
}
/// <summary>
/// A subset of a slice bounded by two raw data points.
/// </summary>
protected class SubRegion
{
/// <summary>
/// The value at the start of the region.
/// </summary>
public double StartValue { get; set; }
/// <summary>
/// The value at the end of the region.
/// </summary>
public double EndValue { get; set; }
/// <summary>
/// The timestamp at the start of the region.
/// </summary>
public DateTimeUtc StartTime;
/// <summary>
/// The length of the region.
/// </summary>
public double Duration { get; set; }
/// <summary>
/// The status for the region.
/// </summary>
public StatusCode StatusCode;
/// <summary>
/// The data point at the start of the region.
/// </summary>
public DataValue DataPoint;
}
/// <summary>
/// Returns the values in the list with simple bounds.
/// </summary>
protected List<SubRegion>? GetRegionsInValueSet(
List<DataValue> values,
bool ignoreBadData,
bool useSteppedCalculations)
{
// nothing to do if no data.
if (values == null)
{
return null;
}
SubRegion? currentRegion = null;
var regions = new List<SubRegion>();
for (int ii = 0; ii < values.Count; ii++)
{
double currentValue = 0;
DateTimeUtc currentTime = values[ii].SourceTimestamp;
StatusCode currentStatus = values[ii].StatusCode;
// convert to doubles to facilitate numeric calculations.
if (StatusCode.IsNotBad(currentStatus))
{
try
{
currentValue = CastToDouble(values[ii]);
}
catch (Exception)
{
currentStatus = StatusCodes.BadTypeMismatch;
}
}
else
{
// use the previous value if end of region is bad.
if (currentRegion != null)
{
currentValue = currentRegion.StartValue;
}
}
// some aggregates ignore bad data so remove them from the set.
if (ignoreBadData)
{
// always keep the first region.
if (currentRegion != null)
{
if (!IsGood(values[ii]))
{
// set the status to sub normal if bad end data ignored.
if (StatusCode.IsNotBad(currentRegion.StatusCode))
{
currentRegion.StatusCode = StatusCodes.UncertainDataSubNormal;
}
// skip everything but the endpoint.
if (ii < values.Count - 1)
{
continue;
}
}
else if (!useSteppedCalculations &&
StatusCode.IsNotGood(values[ii].StatusCode))
{
currentRegion.StatusCode = StatusCodes.UncertainDataSubNormal;
}
}
}
if (currentRegion != null)
{
// if using stepped calculations the end value is not used.
if (useSteppedCalculations)
{
currentRegion.EndValue = currentRegion.StartValue;
}
// using interpolated calculations means the end affects the status of the current region.
else if (IsGood(values[ii]))
{
// handle case with uncertain end point.
if (StatusCode.IsNotGood(values[ii].StatusCode) &&
StatusCode.IsNotBad(currentRegion.StatusCode))
{
currentRegion.StatusCode = StatusCodes.UncertainDataSubNormal;
}
currentRegion.EndValue = currentValue;
}
else
{
if (StatusCode.IsNotBad(currentRegion.StatusCode))
{
currentRegion.StatusCode = StatusCodes.UncertainDataSubNormal;
}
if (ignoreBadData && StatusCode.IsNotBad(currentStatus))
{
currentRegion.EndValue = currentValue;
}
}
// if at end of data then duration is 1 tick.
// must be end of data if start of region is good yet end bound is bad.
if (!ignoreBadData &&
IsGood(currentRegion.DataPoint!) &&
currentStatus == StatusCodes.BadNoData &&
ii == values.Count - 1)
{
currentRegion.Duration = 1;
}
// calculate region span.
else
{
// set uncertain status to bad if treat uncertain as bad is true.
if (StatusCode.IsUncertain(currentStatus) && !IsGood(values[ii]))
{
currentStatus = StatusCodes.BadNoData;
}
currentRegion.Duration = (currentTime - currentRegion.StartTime)
.TotalMilliseconds;
}
regions.Add(currentRegion);
}
// start a new region.
currentRegion = new SubRegion
{
StartValue = currentValue,
EndValue = currentValue,
StartTime = currentTime,
StatusCode = currentStatus,
DataPoint = values[ii]
};
}
return regions;
}
/// <summary>
/// Calculates the value based status code for the slice
/// </summary>
protected StatusCode GetValueBasedStatusCode(
TimeSlice slice,
List<DataValue> values,
StatusCode statusCode)
{
// compute the total good/bad/uncertain.
double badCount = 0;
double goodCount = 0;
double totalCount = 0;
for (int ii = 0; ii < values.Count; ii++)
{
totalCount++;
if (StatusCode.IsBad(values[ii].StatusCode))
{
badCount++;
continue;
}
if (StatusCode.IsGood(values[ii].StatusCode))
{
goodCount++;
}
}
if (totalCount == 0 || goodCount / totalCount * 100 >= Configuration.PercentDataGood)
{
// good if the good count is greater than or equal to the configured threshold.
return statusCode.WithCodeBits(StatusCodes.Good);
}
else if (badCount / totalCount * 100 >= Configuration.PercentDataBad)
{
// bad if the bad count is greater than or equal to the configured threshold.
return StatusCodes.Bad;
}
else
{
// uncertain if did not meet the Good or Bad requirements
return statusCode.WithCodeBits(StatusCodes.UncertainDataSubNormal);
}
}
/// <summary>
/// Calculates the status code for the slice
/// </summary>
protected StatusCode GetTimeBasedStatusCode(
TimeSlice slice,
List<DataValue> values,
StatusCode defaultCode)
{
// get the regions in the slice.
List<SubRegion>? regions = GetRegionsInValueSet(values, false, Stepped);
if (regions == null || regions.Count == 0)
{
return StatusCodes.BadNoData;
}
return GetTimeBasedStatusCode(regions, defaultCode);
}
/// <summary>
/// Calculates the status code for the slice
/// </summary>
protected StatusCode GetTimeBasedStatusCode(List<SubRegion> regions, StatusCode statusCode)
{
// check for empty set.
if (regions == null || regions.Count == 0)
{
return StatusCodes.BadNoData;
}
// compute the total good/bad/uncertain.
double badDuration = 0;
double goodDuration = 0;
double totalDuration = 0;
foreach (SubRegion region in regions)
{
totalDuration += region.Duration;
if (StatusCode.IsBad(region.StatusCode))
{
badDuration += region.Duration;
continue;
}
// Take into account the Uncertain status code
if (StatusCode.IsGood(region.StatusCode) ||
(!Configuration.TreatUncertainAsBad &&
StatusCode.IsUncertain(region.StatusCode)))
{
goodDuration += region.Duration;
}
}
if (totalDuration == 0 ||
goodDuration / totalDuration * 100 >= Configuration.PercentDataGood)
{
// good if the good duration is greater than or equal to the configured threshold.
return statusCode.WithCodeBits(StatusCodes.Good);
}
else if (badDuration / totalDuration * 100 >= Configuration.PercentDataBad)
{
// bad if the bad duration is greater than or equal to the configured threshold.
return StatusCodes.Bad;
}
else
{
// uncertain if did not meet the Good or Bad requirements
return statusCode.WithCodeBits(StatusCodes.UncertainDataSubNormal);
}
}
private readonly ILogger m_logger;
private readonly LinkedList<DataValue> m_values;
private DateTimeUtc m_startOfData;
private DateTimeUtc m_endOfData;
}
}