using Opc.Ua; using Shouldly; using Xunit; namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests; /// /// #821 / #809 facts, through the BOOTED server's runtime AggregateManager — the calculators a live /// AggregateFilter monitored item gets, under the server's own default configuration. This pins current behaviour for /// the owner decision; it does not choose. The HistoryRead (Part 13) answers and SDK 2.0.1's stock answers for the /// same input are pinned beside it in SdkAggregateParityTests. /// public sealed class LiveAggregateLateStartFactsTests { private static readonly DateTime T0 = new(2026, 11, 11, 8, 0, 0, DateTimeKind.Utc); /// The #810 QualityMix tag QP: Uncertain 10 at +5, Good 11 at +12, Uncertain 12 at +25, nothing after. private static readonly (double Seconds, double Value, uint Status)[] Qp = [ (5, 10, 0x40000000u), (12, 11, 0x00000000u), (25, 12, 0x40000000u), // Uncertain, Good, Uncertain ]; [Fact] public async Task Live_AggregateFilter_calculators_on_an_Uncertain_first_late_start_interval() { await using var rig = await ServerEventRig.StartAsync("AggregateLateStart", encrypted: false, TestContext.Current.CancellationToken); var manager = rig.Server.CurrentInstance.AggregateManager; var config = manager.GetDefaultConfiguration(NodeId.Null); config.TreatUncertainAsBad.ShouldBeFalse("the server pins TreatUncertainAsBad = false (#782)"); double One(NodeId aggregate) { var calc = manager.CreateCalculator(aggregate, T0, T0.AddSeconds(20), 20_000, false, config); calc.ShouldNotBeNull(); foreach (var (s, v, st) in Qp) calc.QueueRawValue(new DataValue(Variant.From(v), st, T0.AddSeconds(s), T0.AddSeconds(s))); calc.TryGetProcessedValue(true, out var dv).ShouldBeTrue(); dv.WrappedValue.TryGetValue(out double d).ShouldBeTrue(); return d; } // Part 13 (HistoryRead on SqlServer/QuestDB): Total = 73.5 + 88 + 32/13 = 163.96…, TimeAverage = Total ÷ 15 s. // The live calculators integrate the uncovered [0,5) as a ramp from 0 (+25) and divide by the whole 20 s. const double part13Total = 73.5 + 88 + 32d / 13; One(ObjectIds.AggregateFunction_Total).ShouldBe(part13Total + 25, 1e-9); // 188.96… One(ObjectIds.AggregateFunction_TimeAverage).ShouldBe((part13Total + 25) / 20, 1e-9); // 9.448… (Part 13: 10.93…) // #809: the Uncertain [5,12) counts as Good live (HistoryRead: 8 000 ms / 40 %). One(ObjectIds.AggregateFunction_DurationGood).ShouldBe(15_000d, 1e-9); One(ObjectIds.AggregateFunction_PercentGood).ShouldBe(75d, 1e-9); // DurationBad / PercentBad agree with HistoryRead: the 5 s before the data is Bad. One(ObjectIds.AggregateFunction_DurationBad).ShouldBe(5_000d, 1e-9); One(ObjectIds.AggregateFunction_PercentBad).ShouldBe(25d, 1e-9); } }