fix(historian): #680 review — recovery/only-sample TA2/Total2 value, outage-last + Boolean parity, docs

TimeAverage2/Total2: the interval the data ends inside with no non-Bad time
before its last sample (comms-loss recovery, a single sample) answers that
sample's value (Total2 0), Uncertain + Partial, instead of Bad/BadNoData —
as the SDK. Parity datasets RecoveryEnds, OutageLast, BoolSwitch with
documented masks; ProcessedConformance's reverse TA/Total mask narrowed to
the later-edge-sample interval. Docs: report-by-exception consequence,
outage-last and Boolean differences; CLAUDE.md historian bullet.
This commit is contained in:
Joseph Doherty
2026-10-11 16:02:22 -04:00
parent b61975f7d7
commit 0d206a0884
6 changed files with 218 additions and 16 deletions
@@ -337,11 +337,16 @@ public sealed class TimeWeightedAggregator
{
// After the end of the data: nothing to aggregate (Part 13 "After End of Data: Bad_NoData").
if (lo > dataEnd) return AggregateEvaluator.NoData(stamp, now);
if (lo == dataEnd && _prevAny is { } last)
if (_usableTicks[i] == 0 && dataEnd < hi && _prevAny is { } last)
{
// The data ends exactly on this interval's start: nothing covered; the value at that instant.
// The data ends inside this interval and no non-Bad time precedes its last sample here (the data
// ends on the interval's start; or the last sample is the first usable one after an outage — the
// comms-loss recovery shape — or the tag's only sample): nothing covered, so the value at that
// instant (Total2 0). Good only when the data ends exactly on the interval start with a Good
// sample (nothing before it in the interval); otherwise the interval also holds Bad / no-data time
// ⇒ Uncertain (the SDK answers the same, modulo its 1 ms artifact).
if (!IsNumeric(last)) return Calc(null, HistorianStatusCodes.BadAggregateInvalidInputs);
var st = last.IsGood ? HistorianStatusCodes.Good : HistorianStatusCodes.UncertainDataSubNormal;
var st = lo == dataEnd && last.IsGood ? HistorianStatusCodes.Good : HistorianStatusCodes.UncertainDataSubNormal;
return Calc(_aggregate == HistoryAggregateType.TimeAverage2 ? last.Value.Number : 0d, st, endOfData);
}
duration = Math.Min(hi, dataEnd) - lo;