64e3fbe035
v2-ci / build (push) Failing after 1m43s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
Adds <summary>, <param>, <typeparam>, and <inheritdoc/> tags to public members surfaced by commentchecker — resolves 5,847 of 5,869 issues (99.6%) across three /fixdocs passes.
179 lines
6.8 KiB
C#
179 lines
6.8 KiB
C#
using System.Globalization;
|
|
using CliFx.Attributes;
|
|
using CliFx.Exceptions;
|
|
using CliFx.Infrastructure;
|
|
using Opc.Ua;
|
|
using ZB.MOM.WW.OtOpcUa.Client.CLI.Helpers;
|
|
using ZB.MOM.WW.OtOpcUa.Client.Shared;
|
|
using ZB.MOM.WW.OtOpcUa.Client.Shared.Models;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Commands;
|
|
|
|
[Command("historyread", Description = "Read historical data from a node")]
|
|
public class HistoryReadCommand : CommandBase
|
|
{
|
|
/// <summary>
|
|
/// Creates the historical-data command used to inspect raw or aggregate history for a node.
|
|
/// </summary>
|
|
/// <param name="factory">The factory that creates the shared client service for the command run.</param>
|
|
public HistoryReadCommand(IOpcUaClientServiceFactory factory) : base(factory)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the historized node to query.
|
|
/// </summary>
|
|
[CommandOption("node", 'n', Description = "Node ID (e.g. ns=2;s=MyNode)", IsRequired = true)]
|
|
public string NodeId { get; init; } = default!;
|
|
|
|
/// <summary>
|
|
/// Gets the optional history start time string supplied by the operator.
|
|
/// </summary>
|
|
[CommandOption("start", Description = "Start time in ISO 8601 UTC format, e.g. 2026-01-15T08:00:00Z (default: 24 hours ago)")]
|
|
public string? StartTime { get; init; }
|
|
|
|
/// <summary>
|
|
/// Gets the optional history end time string supplied by the operator.
|
|
/// </summary>
|
|
[CommandOption("end", Description = "End time in ISO 8601 UTC format, e.g. 2026-01-15T09:00:00Z (default: now)")]
|
|
public string? EndTime { get; init; }
|
|
|
|
/// <summary>
|
|
/// Gets the maximum number of raw values the command should print.
|
|
/// </summary>
|
|
[CommandOption("max", Description = "Maximum number of values to return")]
|
|
public int MaxValues { get; init; } = 1000;
|
|
|
|
/// <summary>
|
|
/// Gets the optional aggregate name used when the operator wants processed history instead of raw samples.
|
|
/// </summary>
|
|
[CommandOption("aggregate", Description = "Aggregate function: Average, Minimum, Maximum, Count, Start, End, StandardDeviation")]
|
|
public string? Aggregate { get; init; }
|
|
|
|
/// <summary>
|
|
/// Gets the aggregate bucket interval, in milliseconds, for processed history reads.
|
|
/// </summary>
|
|
[CommandOption("interval", Description = "Processing interval in milliseconds for aggregates")]
|
|
public double IntervalMs { get; init; } = 3600000;
|
|
|
|
/// <inheritdoc />
|
|
public override async ValueTask ExecuteAsync(IConsole console)
|
|
{
|
|
ConfigureLogging();
|
|
if (MaxValues <= 0)
|
|
throw new CommandException($"--max must be greater than 0 (was {MaxValues}).");
|
|
if (!string.IsNullOrEmpty(Aggregate) && IntervalMs <= 0)
|
|
throw new CommandException($"--interval must be greater than 0 (was {IntervalMs}).");
|
|
|
|
NodeId nodeId;
|
|
try
|
|
{
|
|
nodeId = NodeIdParser.ParseRequired(NodeId);
|
|
}
|
|
catch (Exception ex) when (ex is FormatException or ArgumentException)
|
|
{
|
|
throw new CommandException($"Invalid --node value: {ex.Message}");
|
|
}
|
|
|
|
DateTime start, end;
|
|
try
|
|
{
|
|
start = string.IsNullOrEmpty(StartTime)
|
|
? DateTime.UtcNow.AddHours(-24)
|
|
: DateTime.Parse(StartTime, CultureInfo.InvariantCulture,
|
|
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal);
|
|
}
|
|
catch (FormatException ex)
|
|
{
|
|
throw new CommandException($"Invalid --start value '{StartTime}': {ex.Message}. Expected ISO 8601 UTC format, e.g. 2026-01-15T08:00:00Z.");
|
|
}
|
|
|
|
try
|
|
{
|
|
end = string.IsNullOrEmpty(EndTime)
|
|
? DateTime.UtcNow
|
|
: DateTime.Parse(EndTime, CultureInfo.InvariantCulture,
|
|
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal);
|
|
}
|
|
catch (FormatException ex)
|
|
{
|
|
throw new CommandException($"Invalid --end value '{EndTime}': {ex.Message}. Expected ISO 8601 UTC format, e.g. 2026-01-15T08:00:00Z.");
|
|
}
|
|
|
|
AggregateType aggregateType = default;
|
|
if (!string.IsNullOrEmpty(Aggregate))
|
|
{
|
|
try
|
|
{
|
|
aggregateType = ParseAggregateType(Aggregate);
|
|
}
|
|
catch (ArgumentException ex)
|
|
{
|
|
throw new CommandException($"Invalid --aggregate value: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
IOpcUaClientService? service = null;
|
|
try
|
|
{
|
|
var ct = console.RegisterCancellationHandler();
|
|
(service, _) = await CreateServiceAndConnectAsync(ct);
|
|
|
|
IReadOnlyList<DataValue> values;
|
|
|
|
if (string.IsNullOrEmpty(Aggregate))
|
|
{
|
|
await console.Output.WriteLineAsync(
|
|
$"History for {NodeId} ({start:yyyy-MM-dd HH:mm} -> {end:yyyy-MM-dd HH:mm})");
|
|
values = await service.HistoryReadRawAsync(nodeId, start, end, MaxValues, ct);
|
|
}
|
|
else
|
|
{
|
|
await console.Output.WriteLineAsync(
|
|
$"History for {NodeId} ({Aggregate}, interval={IntervalMs}ms)");
|
|
values = await service.HistoryReadAggregateAsync(
|
|
nodeId, start, end, aggregateType, IntervalMs, ct);
|
|
}
|
|
|
|
await console.Output.WriteLineAsync();
|
|
await console.Output.WriteLineAsync($"{"Timestamp",-35} {"Value",-15} {"Status"}");
|
|
|
|
foreach (var dv in values)
|
|
{
|
|
var status = StatusCode.IsGood(dv.StatusCode) ? "Good"
|
|
: StatusCode.IsBad(dv.StatusCode) ? "Bad"
|
|
: "Uncertain";
|
|
await console.Output.WriteLineAsync(
|
|
$"{dv.SourceTimestamp.ToString("O"),-35} {dv.Value,-15} {status}");
|
|
}
|
|
|
|
await console.Output.WriteLineAsync();
|
|
await console.Output.WriteLineAsync($"{values.Count} values returned.");
|
|
}
|
|
finally
|
|
{
|
|
if (service != null)
|
|
{
|
|
await service.DisconnectAsync();
|
|
service.Dispose();
|
|
}
|
|
}
|
|
}
|
|
|
|
private static AggregateType ParseAggregateType(string name)
|
|
{
|
|
return name.Trim().ToLowerInvariant() switch
|
|
{
|
|
"average" or "avg" => AggregateType.Average,
|
|
"minimum" or "min" => AggregateType.Minimum,
|
|
"maximum" or "max" => AggregateType.Maximum,
|
|
"count" => AggregateType.Count,
|
|
"start" or "first" => AggregateType.Start,
|
|
"end" or "last" => AggregateType.End,
|
|
"standarddeviation" or "stddev" or "stdev" => AggregateType.StandardDeviation,
|
|
_ => throw new ArgumentException(
|
|
$"Unknown aggregate: '{name}'. Supported: Average, Minimum, Maximum, Count, Start, End, StandardDeviation")
|
|
};
|
|
}
|
|
}
|