using CliFx.Attributes;
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
{
///
/// Creates the historical-data command used to inspect raw or aggregate history for a node.
///
/// The factory that creates the shared client service for the command run.
public HistoryReadCommand(IOpcUaClientServiceFactory factory) : base(factory)
{
}
///
/// Gets the historized node to query.
///
[CommandOption("node", 'n', Description = "Node ID (e.g. ns=2;s=MyNode)", IsRequired = true)]
public string NodeId { get; init; } = default!;
///
/// Gets the optional history start time string supplied by the operator.
///
[CommandOption("start", Description = "Start time (ISO 8601 or date string, default: 24 hours ago)")]
public string? StartTime { get; init; }
///
/// Gets the optional history end time string supplied by the operator.
///
[CommandOption("end", Description = "End time (ISO 8601 or date string, default: now)")]
public string? EndTime { get; init; }
///
/// Gets the maximum number of raw values the command should print.
///
[CommandOption("max", Description = "Maximum number of values to return")]
public int MaxValues { get; init; } = 1000;
///
/// Gets the optional aggregate name used when the operator wants processed history instead of raw samples.
///
[CommandOption("aggregate", Description = "Aggregate function: Average, Minimum, Maximum, Count, Start, End, StandardDeviation")]
public string? Aggregate { get; init; }
///
/// Gets the aggregate bucket interval, in milliseconds, for processed history reads.
///
[CommandOption("interval", Description = "Processing interval in milliseconds for aggregates")]
public double IntervalMs { get; init; } = 3600000;
///
/// Connects to the server and prints raw or processed historical values for the requested node.
///
/// The CLI console used for output and cancellation handling.
public override async ValueTask ExecuteAsync(IConsole console)
{
ConfigureLogging();
IOpcUaClientService? service = null;
try
{
var ct = console.RegisterCancellationHandler();
(service, _) = await CreateServiceAndConnectAsync(ct);
var nodeId = NodeIdParser.ParseRequired(NodeId);
var start = string.IsNullOrEmpty(StartTime)
? DateTime.UtcNow.AddHours(-24)
: DateTime.Parse(StartTime).ToUniversalTime();
var end = string.IsNullOrEmpty(EndTime)
? DateTime.UtcNow
: DateTime.Parse(EndTime).ToUniversalTime();
IReadOnlyList 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
{
var aggregateType = ParseAggregateType(Aggregate);
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")
};
}
}