chore: organize solution into module folders (Core/Server/Drivers/Client/Tooling)
Group all 69 projects into category subfolders under src/ and tests/ so the Rider Solution Explorer mirrors the module structure. Folders: Core, Server, Drivers (with a nested Driver CLIs subfolder), Client, Tooling. - Move every project folder on disk with git mv (history preserved as renames). - Recompute relative paths in 57 .csproj files: cross-category ProjectReferences, the lib/ HintPath+None refs in Driver.Historian.Wonderware, and the external mxaccessgw refs in Driver.Galaxy and its test project. - Rebuild ZB.MOM.WW.OtOpcUa.slnx with nested solution folders. - Re-prefix project paths in functional scripts (e2e, compliance, smoke SQL, integration, install). Build green (0 errors); unit tests pass. Docs left for a separate pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
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
|
||||
{
|
||||
/// <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 (ISO 8601 or date string, 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 (ISO 8601 or date string, 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;
|
||||
|
||||
/// <summary>
|
||||
/// Connects to the server and prints raw or processed historical values for the requested node.
|
||||
/// </summary>
|
||||
/// <param name="console">The CLI console used for output and cancellation handling.</param>
|
||||
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<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
|
||||
{
|
||||
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")
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user