Files
lmxopcua/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/ServerAggregateConfiguration.cs
T
Joseph Doherty ed05d50795 fix(aggregates): live PercentGood/DurationGood count Good time only (#809)
The frozen 1.5.378.182 StatusAggregateCalculator counts Uncertain regions as
Good under TreatUncertainAsBad=false, so live AggregateFilter PercentGood /
DurationGood disagreed with HistoryRead (Part 13 §5.4.3.31/§5.4.3.33, Mantis
0006166) whenever Uncertain data was present. Owner-approved 2026-11-11.

- New app-owned GoodOnlyStatusAggregateCalculator subclasses the baseline and
  replaces only the PercentGood/DurationGood computation (Good regions only);
  CompatibilityAggregateFactory routes exactly those two IDs to it. The copied
  SDK files are untouched; the other 35 aggregates are byte-identical.
- Golden test: the 64 changed vectors (38 PercentGood + 26 DurationGood) are
  exempted exactly — those whose baseline output differs from the same case's
  baseline TreatUncertainAsBad=true twin — and pinned to that twin (baseline
  evidence, not regenerated from the implementation); plus a full Good-only
  pin over all 352 PercentGood/DurationGood vectors and a hand-computed case.
- Live-vs-history parity over Good/Uncertain/Bad mixes (no masks), with a
  discrimination check against the pre-#809 baseline; the booted-runtime facts
  test now expects 8000 ms / 40 %.
- SdkAggregateOracle: Run/RunLive = the registered factory; RunSdkAsIs = the
  untouched baseline; its Part13GoodCalculator duplicate is removed.
- README (recorded decision), ServerAggregateConfiguration remarks,
  docs/Historian.md and .claude/rules/historian.md updated.

Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 12:05:34 -05:00

81 lines
4.9 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>.
/// Other known live-vs-HistoryRead differences (e.g. #821, an Uncertain first sample in a late-start interval) are
/// documented 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);
}