mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-27 04:59:55 +08:00
# 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.
1519 lines
52 KiB
C#
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;
|
|
}
|
|
}
|