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") }; } }