feat(cli): historyupdate command (insert/replace/update/delete-raw/delete-at-time)

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-09 12:47:53 -04:00
parent ff7c6c531a
commit 70dde115f0
9 changed files with 486 additions and 0 deletions
@@ -131,6 +131,26 @@ internal sealed class DefaultSessionAdapter : ISessionAdapter
return references != null && references.Count > 0;
}
/// <inheritdoc />
public async Task<Models.HistoryUpdateOutcome> HistoryUpdateAsync(HistoryUpdateDetails details, CancellationToken ct)
{
var response = await _session.HistoryUpdateAsync(
null,
new ExtensionObjectCollection { new ExtensionObject(details) },
ct).ConfigureAwait(false);
var statuses = new List<StatusCode>();
var ops = new List<IReadOnlyList<StatusCode>>();
if (response.Results != null)
foreach (var r in response.Results)
{
statuses.Add(r.StatusCode);
ops.Add(r.OperationResults?.ToArray() ?? Array.Empty<StatusCode>());
}
return new Models.HistoryUpdateOutcome(response.ResponseHeader.ServiceResult, statuses, ops);
}
/// <inheritdoc />
public async Task<IReadOnlyList<DataValue>> HistoryReadRawAsync(
NodeId nodeId, DateTime startTime, DateTime endTime, int maxValues, CancellationToken ct)
@@ -110,6 +110,15 @@ internal interface ISessionAdapter : IDisposable
Task<IReadOnlyList<DataValue>> HistoryReadRawAsync(NodeId nodeId, DateTime startTime, DateTime endTime,
int maxValues, CancellationToken ct = default);
/// <summary>
/// Sends a HistoryUpdate request for the supplied details.
/// </summary>
/// <param name="details">The history update details.</param>
/// <param name="ct">The cancellation token that aborts the call.</param>
/// <returns>The overall service result and per-item status codes.</returns>
Task<Models.HistoryUpdateOutcome> HistoryUpdateAsync(HistoryUpdateDetails details, CancellationToken ct = default)
=> throw new NotSupportedException("HistoryUpdate is not supported by this session adapter.");
/// <summary>
/// Reads processed/aggregate historical data.
/// </summary>
@@ -193,6 +193,15 @@ public interface IOpcUaClientService : IDisposable
Task<IReadOnlyList<DataValue>> HistoryReadRawAsync(NodeId nodeId, DateTime startTime, DateTime endTime,
int maxValues = 1000, CancellationToken ct = default);
/// <summary>
/// Sends a HistoryUpdate request (insert/replace/update or delete) for the supplied details.
/// </summary>
/// <param name="details">The history update details (UpdateDataDetails, DeleteRawModifiedDetails, DeleteAtTimeDetails...).</param>
/// <param name="ct">The cancellation token that aborts the call.</param>
/// <returns>The overall service result and per-item status codes.</returns>
Task<HistoryUpdateOutcome> HistoryUpdateAsync(HistoryUpdateDetails details, CancellationToken ct = default)
=> throw new NotSupportedException("HistoryUpdate is not supported by this client service.");
/// <summary>
/// Reads aggregate historical values for a historized node using an OPC UA aggregate function.
/// </summary>
@@ -0,0 +1,15 @@
using Opc.Ua;
namespace ZB.MOM.WW.OtOpcUa.Client.Shared.Models;
/// <summary>
/// Result of a HistoryUpdate service call: the overall service result plus, per history-update result,
/// the operation-level status codes (one per value / requested time) and the result's own status code.
/// </summary>
/// <param name="ServiceResult">The service-level result from the response header.</param>
/// <param name="ResultStatus">The per-details-entry status codes.</param>
/// <param name="OperationResults">Per-details-entry operation status codes (one per value or requested time; may be empty).</param>
public sealed record HistoryUpdateOutcome(
StatusCode ServiceResult,
IReadOnlyList<StatusCode> ResultStatus,
IReadOnlyList<IReadOnlyList<StatusCode>> OperationResults);
@@ -559,6 +559,14 @@ public sealed class OpcUaClientService : IOpcUaClientService
return await _session!.HistoryReadRawAsync(nodeId, startTime, endTime, maxValues, ct);
}
/// <inheritdoc />
public async Task<HistoryUpdateOutcome> HistoryUpdateAsync(HistoryUpdateDetails details, CancellationToken ct = default)
{
ThrowIfDisposed();
ThrowIfNotConnected();
return await _session!.HistoryUpdateAsync(details, ct);
}
/// <inheritdoc />
public async Task<IReadOnlyList<DataValue>> HistoryReadAggregateAsync(
NodeId nodeId, DateTime startTime, DateTime endTime, AggregateType aggregate,