fix(opcua): DeleteRawModified with an unspecified bound is BadInvalidArgument; HistoryUpdate node-manager tests
- A DateTime.MinValue start or end on DeleteRawModified is refused BadInvalidArgument before the backend. - Tests: anonymous without the HistoryUpdate role denied (with it allowed), UpdateTagAttributes enabling historization sets HistoryWrite, a throwing updater is BadInternalError, the MinValue bounds case; corrected the test helper's comment on how the SDK base seeds unadmitted slots. - GatewayHistorianValueWriter remarks describe the recorder's real status capture (and that it does not persist on the gateway). Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
+12
-6
@@ -28,12 +28,18 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Recorder;
|
||||
/// <b>Quality is a documented-dead input</b> on the <c>WriteLiveValues</c> SQL path
|
||||
/// (HistorianGateway 0.2.0 Contracts C-002): <c>HistorianLiveValue.Quality</c> /
|
||||
/// <c>QualityDetail</c> are server-managed columns — any value sent is accepted on the wire and
|
||||
/// then silently discarded. We populate <c>Quality</c> anyway (see the mapping below): proto3
|
||||
/// transmits <c>0</c> when a field is left unset, and <c>0</c> is "Bad" in OPC-DA terms, so
|
||||
/// <em>not</em> setting it would send a <em>wrong</em> quality to any future gateway version that
|
||||
/// starts honoring the field — whereas the recorder's <c>192</c> ("Good") is the correct value.
|
||||
/// <b>Do NOT extend the recorder to capture real node quality expecting it to persist</b> — it
|
||||
/// will not, until the gateway honors the field.
|
||||
/// then silently discarded. We populate <c>Quality</c> anyway: proto3 transmits <c>0</c> when a
|
||||
/// field is left unset, and <c>0</c> is "Bad" in OPC-DA terms, so <em>not</em> setting it would send a
|
||||
/// <em>wrong</em> quality to any future gateway version that starts honoring the field. The recorder
|
||||
/// now carries each sample's real OPC UA status, which <see cref="Mapping.GatewayQualityMapper.ToDaQuality"/>
|
||||
/// maps to the DA byte (Good ⇒ <c>192</c>, as before). <b>That status does not persist on this
|
||||
/// backend</b>: the gateway discards the field, so AVEVA still stores every sample Good — use the
|
||||
/// SqlServer backend where the stored status must be the real one.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Numeric only.</b> Values whose kind has no double form (String, DateTime, Empty) are skipped,
|
||||
/// never thrown; a batch with none left is acked without calling the gateway. The recorder already drops
|
||||
/// those kinds at capture for this backend, so the skip is a backstop.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class GatewayHistorianValueWriter : IHistorianValueWriter, IAsyncDisposable
|
||||
|
||||
@@ -5295,6 +5295,10 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
if (details.IsDeleteModified) return null;
|
||||
var start = details.StartTime;
|
||||
var end = details.EndTime;
|
||||
// Part 11 requires both bounds of a raw delete; DateTime.MinValue is the wire's "unspecified". Refuse
|
||||
// it here rather than let a backend read MinValue as "from the beginning of time".
|
||||
if (start == DateTime.MinValue || end == DateTime.MinValue)
|
||||
return (_, _, _) => Task.FromResult(new HistoryUpdateResult { StatusCode = StatusCodes.BadInvalidArgument });
|
||||
return async (updater, tagname, ct) =>
|
||||
{
|
||||
var code = await updater.DeleteRawAsync(tagname, start, end, ct).ConfigureAwait(false);
|
||||
|
||||
Reference in New Issue
Block a user