0d249f3ee9
Review follow-up: on the Gateway every value is a Double (AVEVA stores a Boolean as a 0/1 Int2), so the raw-evaluated AtTime interpolated a Boolean node to 0.5. - IHistorianDataSource gains a default-interface overload ReadAtTimeAsync(fullReference, timestamps, TypeCode? valueType, ct) that forwards to the existing one (SqlServer/QuestDB/Null/fakes unchanged). - OtOpcUaNodeManager.HistoryReadAtTime passes the node's scalar DataType (AtTimeValueType: built-in type -> TypeCode; arrays/other types => null); Raw and UNS NodeIds alike. - GatewayAtTimeReader converts each raw row to the hinted kind BEFORE the shared AtTimeEvaluator runs (Boolean = value != 0 => stepped; integers => Int64 => interpolated, rounded half away from zero, returned as the node's integer type; floats Double), so the evaluator's own type branch decides. - AtTimeEvaluator boxed every interpolated Int64 as a Double (`cond ? (long)x : v` widens to double); its "keeping its type" test passed because Shouldly ShouldBe compares numbers across types. Fixed (affects SqlServer/QuestDB too) and pinned with ShouldBeOfType.
70 lines
4.5 KiB
C#
70 lines
4.5 KiB
C#
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common;
|
|
|
|
/// <summary>The AtTime rules, applied to one requested time and its stored neighbours.</summary>
|
|
public static class AtTimeEvaluator
|
|
{
|
|
/// <summary>
|
|
/// Applies the AtTime rules to one requested time <paramref name="t"/> (the result is stamped <c>t</c>):
|
|
/// <list type="bullet">
|
|
/// <item><c>prev</c> at exactly <c>t</c> ⇒ the stored value and status, unchanged.</item>
|
|
/// <item>No <c>prev</c> ⇒ <c>BadNoData</c>, null value.</item>
|
|
/// <item><c>prev</c> Bad (incl. an <c>Empty</c> outage marker) ⇒ <c>t</c> lies inside an outage: null value
|
|
/// carrying <c>prev</c>'s Bad status code (info bits stripped) — never better than its source.</item>
|
|
/// <item><c>prev</c> usable and <c>next</c> Bad ⇒ <c>prev</c>'s value held (stepped, NOT interpolated into the
|
|
/// outage) with <c>UncertainDataSubNormal</c> + Interpolated bits.</item>
|
|
/// <item>Double/Int64 with a usable <c>next</c> ⇒ linear interpolation (Int64 rounded to the nearest integer,
|
|
/// ties away from zero, keeping the type); status <c>Good</c> + Interpolated when both neighbours are Good,
|
|
/// else <c>UncertainDataSubNormal</c> + Interpolated.</item>
|
|
/// <item>Boolean/String/DateTime, or no <c>next</c> (after the last sample) ⇒ stepped: <c>prev</c>'s value with
|
|
/// <c>prev</c>'s status code + Interpolated bits — except that a time later than now that is stepped from the
|
|
/// last sample is <c>UncertainDataSubNormal</c> + Interpolated.</item>
|
|
/// </list>
|
|
/// </summary>
|
|
/// <param name="t">Requested time.</param>
|
|
/// <param name="prev">The stored sample at or before <paramref name="t"/>, if any.</param>
|
|
/// <param name="next">The stored sample after <paramref name="t"/>, if any.</param>
|
|
/// <param name="serverTime">Server timestamp for the result.</param>
|
|
/// <param name="now">The current time (for the beyond-now rule).</param>
|
|
/// <returns>The value at <paramref name="t"/>.</returns>
|
|
public static DataValueSnapshot Evaluate(DateTime t, StoredSample? prev, StoredSample? next, DateTime serverTime, DateTime now)
|
|
{
|
|
if (prev is not { } p)
|
|
return new DataValueSnapshot(null, HistorianStatusCodes.BadNoData, t, serverTime);
|
|
if (p.TimeUtc == t)
|
|
return new DataValueSnapshot(p.Value.ToObject(), p.Status, t, serverTime);
|
|
|
|
// Inside an outage: report the outage's own Bad code, never an upgraded Uncertain.
|
|
if (!p.IsUsable)
|
|
return new DataValueSnapshot(null, p.Status & StatusBits.CodeBitsMask, t, serverTime);
|
|
|
|
// Usable previous, Bad next: hold the previous value up to the outage; do not interpolate into it.
|
|
if (next is { IsUsable: false })
|
|
return new DataValueSnapshot(p.Value.ToObject(),
|
|
HistorianInfoBits.WithInterpolated(HistorianStatusCodes.UncertainDataSubNormal), t, serverTime);
|
|
|
|
if (next is { } n && p.Value.Kind is HistorianValueKind.Double or HistorianValueKind.Int64
|
|
&& n.Value.TryGetDouble(out var nv))
|
|
{
|
|
var fraction = (double)(t.Ticks - p.TimeUtc.Ticks) / (n.TimeUtc.Ticks - p.TimeUtc.Ticks);
|
|
var v = p.Value.Number + (nv - p.Value.Number) * fraction;
|
|
// Each arm boxed on its own: `cond ? (long)x : v` is a DOUBLE expression in C# (the long widens), which returned
|
|
// every interpolated Int64 as a Double until #693.
|
|
object value = p.Value.Kind == HistorianValueKind.Int64
|
|
? (object)(long)Math.Round(v, MidpointRounding.AwayFromZero)
|
|
: v;
|
|
var status = p.IsGood && n.IsGood ? HistorianStatusCodes.Good : HistorianStatusCodes.UncertainDataSubNormal;
|
|
return new DataValueSnapshot(value, HistorianInfoBits.WithInterpolated(status), t, serverTime);
|
|
}
|
|
|
|
// Beyond the last sample AND beyond now: the value is held into the future, so it is not known to be Good.
|
|
if (next is null && t > now)
|
|
return new DataValueSnapshot(p.Value.ToObject(),
|
|
HistorianInfoBits.WithInterpolated(HistorianStatusCodes.UncertainDataSubNormal), t, serverTime);
|
|
|
|
// Stepped (non-numeric kind, or after the last sample): hold the previous value and its status.
|
|
return new DataValueSnapshot(p.Value.ToObject(), HistorianInfoBits.WithInterpolated(p.Status), t, serverTime);
|
|
}
|
|
}
|