8a0032bfdf
When the data starts inside an interval (no start bound) and its first value is
Uncertain, the frozen 1.5.378.182 AverageAggregateCalculator integrated the
uncovered span as a ramp from 0 and divided TimeAverage by the whole interval:
GetRegionsInValueSet (ignoreBadData, sloped) overwrites the BadNoData start
region's status with UncertainDataSubNormal in its one branch without a not-Bad
guard. Live AggregateFilter therefore disagreed with HistoryRead and Part 13
§5.4.3.6/§5.4.3.8 ("No Start Bound: Calculate Partial Interval"); QP: Total
188.96 vs 163.96, TimeAverage 9.448 vs 10.931. SDK 2.0.1 stock does the same.
Owner-approved 2026-11-11.
- New app-owned CoveredTimeAverageAggregateCalculator subclasses the baseline
and recomputes only TimeAverage/Total, restoring the Bad status of a region
that starts at a Bad point (only the start bound can). Only such an interval's
value changes; status, timestamps, type and bits are the baseline's. Stepped
and TreatUncertainAsBad=true never reached the defect. CompatibilityAggregate-
Factory routes exactly those two IDs; copied SDK files untouched.
- Golden test: 0 fixture vectors change (every data set has a usable value at
-10 s), asserted. New sweep over late-start variants of every data set (cut
+5 s / +25 s, first non-Bad sample Uncertain, all directions/stepping/configs/
incremental): 150 / 142 of 384 change, each pinned to a baseline-derived
oracle (untouched baseline's value with that sample Good, its status/bits on
the variant itself) - never our calculator. Plus a hand-computed case.
- Live-vs-HistoryRead parity (TimeWeightedAggregator) forward + reverse, no
masks, with discrimination against the pre-#821 baseline; QualityMix now
compares QP's TimeAverage/Total with the live oracle on both SQL backends;
booted-runtime facts test expects 163.96 / 10.93.
- README recorded decision, ServerAggregateConfiguration remarks, oracle
remarks, docs/Historian.md and .claude/rules/historian.md updated.
Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
85 lines
5.5 KiB
C#
85 lines
5.5 KiB
C#
using Opc.Ua;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer;
|
|
|
|
/// <summary>
|
|
/// The Part 13 <see cref="AggregateConfiguration"/> processed reads are evaluated under, and the rule for a
|
|
/// client-supplied one in <c>ReadProcessedDetails</c>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// The aggregate evaluators (<c>AggregateEvaluator</c> / <c>TimeWeightedAggregator</c> in
|
|
/// <c>Driver.Historian.Sql.Common</c>) are written against fixed settings, not parameterised: Uncertain time counts as
|
|
/// Good (<see cref="TreatUncertainAsBad"/> = false) in TimeAverage2 / Total2, while PercentGood / DurationGood count
|
|
/// Good time only and PercentBad / DurationBad Bad time only — Uncertain in neither (Part 13 §5.4.3.31-34, Mantis
|
|
/// 0006166); the per-sample Average / Minimum /
|
|
/// Maximum / Range / *ActualTime / Count are defined by Part 13 over Good values whatever this setting, as the
|
|
/// SDK computes them under it; an interval is Good only when all of
|
|
/// it is (<see cref="PercentDataGood"/> = 100) and Bad only when none of it is non-Bad
|
|
/// (<see cref="PercentDataBad"/> = 100), otherwise <c>UncertainDataSubNormal</c>; the value past the last sample
|
|
/// is held (stepped, <see cref="UseSlopedExtrapolation"/> = false). Part 13's AggregateConfiguration defaults are
|
|
/// TreatUncertainAsBad = false, PercentDataBad = PercentDataGood = 100, UseSlopedExtrapolation = false (from the
|
|
/// spec as recalled, not re-read offline). The SDK matched these until Opc.Ua.Server 1.5.378.182, which flipped
|
|
/// <c>TreatUncertainAsBad</c> to true: that is a DELIBERATE DIVERGENCE we do not follow (consumers such as
|
|
/// ScadaBridge read these aggregates). <c>OtOpcUaSdkServer.CreateAggregateManager</c> pushes these values into
|
|
/// the SDK manager so live AggregateFilter subscriptions agree with history reads; a test pins both sides.
|
|
/// </para>
|
|
/// <para>
|
|
/// The live calculators are the application's frozen 1.5.378.182 copies (<c>Compatibility/Aggregates</c>), whose
|
|
/// <c>ComputeDurationGoodBad</c> counts Uncertain time as Good under <see cref="TreatUncertainAsBad"/> = false. That
|
|
/// made live PercentGood / DurationGood disagree with HistoryRead whenever Uncertain data was present. #809
|
|
/// (owner-approved 2026-11-11) overrides exactly those two (<c>GoodOnlyStatusAggregateCalculator</c>), so live and
|
|
/// HistoryRead now agree on them — pinned by
|
|
/// <c>SdkAggregateParityTests.Live_AggregateFilter_PercentGood_and_DurationGood_equal_HistoryRead_over_Uncertain_samples</c>.
|
|
/// Likewise the frozen <c>AverageAggregateCalculator</c> integrated the span before an <b>Uncertain</b> first value
|
|
/// as a ramp from 0 when the data starts inside an interval (no start bound) — live TimeAverage / Total disagreed
|
|
/// with HistoryRead and Part 13 ("No Start Bound: Calculate Partial Interval"). #821 (owner-approved 2026-11-11)
|
|
/// overrides exactly those two (<c>CoveredTimeAverageAggregateCalculator</c>) — pinned by
|
|
/// <c>SdkAggregateParityTests.Live_AggregateFilter_TimeAverage_and_Total_equal_HistoryRead_over_an_Uncertain_first_late_start</c>.
|
|
/// The remaining documented live-vs-HistoryRead differences are listed in <c>docs/Historian.md</c>.
|
|
/// </para>
|
|
/// <para>
|
|
/// A request asking for <c>UseServerCapabilitiesDefaults</c>, or spelling out exactly these values, is served;
|
|
/// any other configuration is refused per node with <c>BadAggregateConfigurationRejected</c> rather than silently
|
|
/// answered under settings the client did not ask for (it used to be ignored).
|
|
/// </para>
|
|
/// </remarks>
|
|
public static class ServerAggregateConfiguration
|
|
{
|
|
/// <summary>Uncertain data is not treated as Bad.</summary>
|
|
public const bool TreatUncertainAsBad = false;
|
|
|
|
/// <summary>An interval is Bad only when 100 % of it is Bad.</summary>
|
|
public const byte PercentDataBad = 100;
|
|
|
|
/// <summary>An interval is Good only when 100 % of it is Good.</summary>
|
|
public const byte PercentDataGood = 100;
|
|
|
|
/// <summary>The value past the last sample is held (stepped), not sloped.</summary>
|
|
public const bool UseSlopedExtrapolation = false;
|
|
|
|
/// <summary>The server defaults as an <see cref="AggregateConfiguration"/> (explicit values, not "use defaults").</summary>
|
|
public static AggregateConfiguration Defaults => new()
|
|
{
|
|
UseServerCapabilitiesDefaults = false,
|
|
TreatUncertainAsBad = TreatUncertainAsBad,
|
|
PercentDataBad = PercentDataBad,
|
|
PercentDataGood = PercentDataGood,
|
|
UseSlopedExtrapolation = UseSlopedExtrapolation,
|
|
};
|
|
|
|
/// <summary>
|
|
/// True when a processed read with <paramref name="requested"/> can be served: none supplied,
|
|
/// <c>UseServerCapabilitiesDefaults</c>, or every setting equal to the server's.
|
|
/// </summary>
|
|
/// <param name="requested">The request's configuration (may be null).</param>
|
|
/// <returns>Whether to serve; false ⇒ <c>BadAggregateConfigurationRejected</c>.</returns>
|
|
public static bool IsServable(AggregateConfiguration? requested) =>
|
|
requested is null
|
|
|| requested.UseServerCapabilitiesDefaults
|
|
|| (requested.TreatUncertainAsBad == TreatUncertainAsBad
|
|
&& requested.PercentDataBad == PercentDataBad
|
|
&& requested.PercentDataGood == PercentDataGood
|
|
&& requested.UseSlopedExtrapolation == UseSlopedExtrapolation);
|
|
}
|