From 022265ae0eb5cbdfdac3ac7ffe5fc13a443ce0ed Mon Sep 17 00:00:00 2001 From: Per Kops Date: Sun, 15 Feb 2026 18:46:33 +0100 Subject: [PATCH 1/6] chore(deps): update NuGet dependencies --- Directory.Build.props | 4 ++-- sample/Atc.Kepware.Sample/Atc.Kepware.Sample.csproj | 4 ++-- .../Atc.Kepware.Configuration.CLI.csproj | 12 ++++++------ .../Atc.Kepware.Configuration.Contracts.csproj | 2 +- .../Atc.Kepware.Configuration.csproj | 6 +++--- src/Directory.Build.props | 2 +- .../Atc.Kepware.Configuration.Tests.csproj | 4 ++-- test/Directory.Build.props | 6 +++--- 8 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 4978ab48..67aea515 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -54,10 +54,10 @@ - + - + \ No newline at end of file diff --git a/sample/Atc.Kepware.Sample/Atc.Kepware.Sample.csproj b/sample/Atc.Kepware.Sample/Atc.Kepware.Sample.csproj index d6df31e0..07949c14 100644 --- a/sample/Atc.Kepware.Sample/Atc.Kepware.Sample.csproj +++ b/sample/Atc.Kepware.Sample/Atc.Kepware.Sample.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/src/Atc.Kepware.Configuration.CLI/Atc.Kepware.Configuration.CLI.csproj b/src/Atc.Kepware.Configuration.CLI/Atc.Kepware.Configuration.CLI.csproj index c4bae84f..7dcd48bd 100644 --- a/src/Atc.Kepware.Configuration.CLI/Atc.Kepware.Configuration.CLI.csproj +++ b/src/Atc.Kepware.Configuration.CLI/Atc.Kepware.Configuration.CLI.csproj @@ -17,12 +17,12 @@ - - - - - - + + + + + + diff --git a/src/Atc.Kepware.Configuration.Contracts/Atc.Kepware.Configuration.Contracts.csproj b/src/Atc.Kepware.Configuration.Contracts/Atc.Kepware.Configuration.Contracts.csproj index 2b7dca69..4e0b9ba7 100644 --- a/src/Atc.Kepware.Configuration.Contracts/Atc.Kepware.Configuration.Contracts.csproj +++ b/src/Atc.Kepware.Configuration.Contracts/Atc.Kepware.Configuration.Contracts.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/Atc.Kepware.Configuration/Atc.Kepware.Configuration.csproj b/src/Atc.Kepware.Configuration/Atc.Kepware.Configuration.csproj index bb65f8e4..94c76251 100644 --- a/src/Atc.Kepware.Configuration/Atc.Kepware.Configuration.csproj +++ b/src/Atc.Kepware.Configuration/Atc.Kepware.Configuration.csproj @@ -12,10 +12,10 @@ - + - - + + diff --git a/src/Directory.Build.props b/src/Directory.Build.props index acc4a622..1d166bfe 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -53,7 +53,7 @@ - + \ No newline at end of file diff --git a/test/Atc.Kepware.Configuration.Tests/Atc.Kepware.Configuration.Tests.csproj b/test/Atc.Kepware.Configuration.Tests/Atc.Kepware.Configuration.Tests.csproj index 69dd4ae4..b3b8adc6 100644 --- a/test/Atc.Kepware.Configuration.Tests/Atc.Kepware.Configuration.Tests.csproj +++ b/test/Atc.Kepware.Configuration.Tests/Atc.Kepware.Configuration.Tests.csproj @@ -1,4 +1,4 @@ - + false @@ -6,7 +6,7 @@ - + diff --git a/test/Directory.Build.props b/test/Directory.Build.props index 2b2e1fac..1f1fa3ae 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -17,11 +17,11 @@ - - + + - + all From 7852e456c5e5fc4c8b1660af25dd0be057ed8c86 Mon Sep 17 00:00:00 2001 From: Per Kops Date: Sun, 15 Feb 2026 18:48:03 +0100 Subject: [PATCH 2/6] docs: fix misleading driver list in Channel Management feature description - Replace hardcoded 3-driver example with reference to full supported drivers section --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 122988b1..20d41aa0 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Atc.Kepware is a .NET library and CLI tool for configuring Kepware servers via R The library provides comprehensive Kepware server management capabilities: -- ✅ **Channel Management**: Create, retrieve, and delete channels for various drivers (EuroMap63, OPC UA Client, Simulator) +- ✅ **Channel Management**: Create, retrieve, and delete channels for 70+ [supported drivers](#supported-drivers) - ✅ **Device Management**: Configure devices under channels with driver-specific settings - ✅ **Tag Management**: Create tags and tag groups with hierarchical structures, search for tags with wildcards - ✅ **IoT Gateway**: Manage MQTT and REST client/server agents and their associated items From 1e60d2cfb187c431d1202e796cedfb426b30e113 Mon Sep 17 00:00:00 2001 From: Per Kops Date: Sun, 15 Feb 2026 19:15:44 +0100 Subject: [PATCH 3/6] fix: skip unnecessary API calls in tag group iteration - Skip fetching tags when TagCountInGroup is 0 (no local tags) - Skip fetching sub-groups when TagCountInTree equals TagCountInGroup (no tags in child groups) - Pass tag count parameters through IterateTagGroup to enable conditional fetching --- .../KepwareConfigurationClientConnectivity.cs | 66 +++++++++++-------- 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/src/Atc.Kepware.Configuration/Services/Connectivity/KepwareConfigurationClientConnectivity.cs b/src/Atc.Kepware.Configuration/Services/Connectivity/KepwareConfigurationClientConnectivity.cs index fff6a97e..8f338088 100644 --- a/src/Atc.Kepware.Configuration/Services/Connectivity/KepwareConfigurationClientConnectivity.cs +++ b/src/Atc.Kepware.Configuration/Services/Connectivity/KepwareConfigurationClientConnectivity.cs @@ -268,19 +268,19 @@ public async Task> GetTags( } var tagGroupResult = await GetTagGroupResultForPathTemplate(basePathTemplate, cancellationToken); - if (tagGroupResult is { CommunicationSucceeded: true, HasData: true } && tagGroupResult.Data!.Any()) { foreach (var kepwareTagGroup in tagGroupResult.Data!) { var tagGroup = kepwareTagGroup.Adapt(); - if (maxDepth >= currentDepth && kepwareTagGroup.TagCountInTree > 0) { await IterateTagGroup( tagGroup, $"{basePathTemplate}/{EndpointPathTemplateConstants.TagGroups}", + kepwareTagGroup.TagCountInGroup, + kepwareTagGroup.TagCountInTree, currentDepth + 1, maxDepth, cancellationToken); @@ -922,6 +922,8 @@ private static bool IsValidConnectivityName( private async Task IterateTagGroup( TagGroup tagGroup, string tagGroupPathTemplate, + int tagCountInGroup, + int tagCountInTree, int currentDepth, int maxDepth, CancellationToken cancellationToken) @@ -933,43 +935,49 @@ private async Task IterateTagGroup( tagGroupPathTemplate = $"{tagGroupPathTemplate}/{tagGroup.Name}"; - // TODO: Optimize if tagGroup.TagCountInGroup > 0 - var tagResult = await GetTagsResultForPathTemplate(tagGroupPathTemplate, cancellationToken); - if (!tagResult.CommunicationSucceeded) + if (tagCountInGroup > 0) { - return; - } + var tagResult = await GetTagsResultForPathTemplate(tagGroupPathTemplate, cancellationToken); + if (!tagResult.CommunicationSucceeded) + { + return; + } - if (tagResult.HasData && - tagResult.Data!.Any()) - { - foreach (var tag in tagResult.Data!.Adapt>()) + if (tagResult.HasData && + tagResult.Data!.Any()) { - tagGroup.Tags.Add(tag); + foreach (var tag in tagResult.Data!.Adapt>()) + { + tagGroup.Tags.Add(tag); + } } } - // TODO: Optimize if tagGroup.TagCountInTree > 0 - var tagGroupResult = await GetTagGroupResultForPathTemplate(tagGroupPathTemplate, cancellationToken); - - if (tagGroupResult is { CommunicationSucceeded: true, HasData: true } && - tagGroupResult.Data!.Any()) + if (tagCountInTree > tagCountInGroup) { - foreach (var kepwareTagGroup in tagGroupResult.Data!) - { - var subTagGroup = kepwareTagGroup.Adapt(); + var tagGroupResult = await GetTagGroupResultForPathTemplate(tagGroupPathTemplate, cancellationToken); - if (kepwareTagGroup.TagCountInTree > 0) + if (tagGroupResult is { CommunicationSucceeded: true, HasData: true } && + tagGroupResult.Data!.Any()) + { + foreach (var kepwareTagGroup in tagGroupResult.Data!) { - await IterateTagGroup( - subTagGroup, - $"{tagGroupPathTemplate}/{EndpointPathTemplateConstants.TagGroups}", - currentDepth + 1, - maxDepth, - cancellationToken); - } + var subTagGroup = kepwareTagGroup.Adapt(); - tagGroup.TagGroups.Add(subTagGroup); + if (kepwareTagGroup.TagCountInTree > 0) + { + await IterateTagGroup( + subTagGroup, + $"{tagGroupPathTemplate}/{EndpointPathTemplateConstants.TagGroups}", + kepwareTagGroup.TagCountInGroup, + kepwareTagGroup.TagCountInTree, + currentDepth + 1, + maxDepth, + cancellationToken); + } + + tagGroup.TagGroups.Add(subTagGroup); + } } } } From 6065543081c5320567c12d8e6f373260bdb5e895 Mon Sep 17 00:00:00 2001 From: Per Kops Date: Sun, 15 Feb 2026 19:16:01 +0100 Subject: [PATCH 4/6] docs: add missing flow computer drivers to README - Add ABB Totalflow, Fisher ROC Ethernet, Fisher ROC Plus Ethernet, and Omni Flow Computer - Create new "Flow Computers / Metering" section in Supported Drivers --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 20d41aa0..9b5ec79a 100644 --- a/README.md +++ b/README.md @@ -397,6 +397,14 @@ The library supports a comprehensive range of Kepware drivers organized by manuf | Wago Ethernet | Wago controllers | | Yaskawa MP Series Ethernet | MP series motion controllers | +### Flow Computers / Metering +| Driver | Description | +|--------|-------------| +| ABB Totalflow | ABB Totalflow flow computers | +| Fisher ROC Ethernet | Fisher ROC flow computers over Ethernet | +| Fisher ROC Plus Ethernet | Fisher ROC Plus flow computers over Ethernet | +| Omni Flow Computer | Omni Flow Computer metering | + ### CNC / Motion | Driver | Description | |--------|-------------| From 1c59c90b396d309fdc4105c7687e090ba2dc2ee6 Mon Sep 17 00:00:00 2001 From: Per Kops Date: Sun, 15 Feb 2026 19:21:03 +0100 Subject: [PATCH 5/6] feat(cli): add meter commands for flow computer drivers - Add get commands for ABB Totalflow, Fisher ROC Ethernet, Fisher ROC Plus Ethernet, and Omni Flow Computer meters - Add create commands for ABB Totalflow, Fisher ROC Ethernet, and Fisher ROC Plus Ethernet meters - Add MeterCommandBaseSettings and MeterCreateCommandBaseSettings for meter operations - Register new meter commands under `connectivity meters get|create` in CommandAppExtensions --- .../Create/MeterCreateAbbTotalflowCommand.cs | 73 +++++++++++++++++ .../MeterCreateFisherRocEthernetCommand.cs | 73 +++++++++++++++++ ...MeterCreateFisherRocPlusEthernetCommand.cs | 73 +++++++++++++++++ .../Retrieve/MetersGetAbbTotalflowCommand.cs | 80 +++++++++++++++++++ .../MetersGetFisherRocEthernetCommand.cs | 80 +++++++++++++++++++ .../MetersGetFisherRocPlusEthernetCommand.cs | 80 +++++++++++++++++++ .../MetersGetOmniFlowComputerCommand.cs | 80 +++++++++++++++++++ .../Meters/MeterCommandBaseSettings.cs | 22 +++++ .../Meters/MeterCreateCommandBaseSettings.cs | 26 ++++++ .../Extensions/CommandAppExtensions.cs | 52 ++++++++++++ .../GlobalUsings.cs | 6 ++ 11 files changed, 645 insertions(+) create mode 100644 src/Atc.Kepware.Configuration.CLI/Commands/Connectivity/Meters/Create/MeterCreateAbbTotalflowCommand.cs create mode 100644 src/Atc.Kepware.Configuration.CLI/Commands/Connectivity/Meters/Create/MeterCreateFisherRocEthernetCommand.cs create mode 100644 src/Atc.Kepware.Configuration.CLI/Commands/Connectivity/Meters/Create/MeterCreateFisherRocPlusEthernetCommand.cs create mode 100644 src/Atc.Kepware.Configuration.CLI/Commands/Connectivity/Meters/Retrieve/MetersGetAbbTotalflowCommand.cs create mode 100644 src/Atc.Kepware.Configuration.CLI/Commands/Connectivity/Meters/Retrieve/MetersGetFisherRocEthernetCommand.cs create mode 100644 src/Atc.Kepware.Configuration.CLI/Commands/Connectivity/Meters/Retrieve/MetersGetFisherRocPlusEthernetCommand.cs create mode 100644 src/Atc.Kepware.Configuration.CLI/Commands/Connectivity/Meters/Retrieve/MetersGetOmniFlowComputerCommand.cs create mode 100644 src/Atc.Kepware.Configuration.CLI/Commands/Settings/Connectivity/Meters/MeterCommandBaseSettings.cs create mode 100644 src/Atc.Kepware.Configuration.CLI/Commands/Settings/Connectivity/Meters/MeterCreateCommandBaseSettings.cs diff --git a/src/Atc.Kepware.Configuration.CLI/Commands/Connectivity/Meters/Create/MeterCreateAbbTotalflowCommand.cs b/src/Atc.Kepware.Configuration.CLI/Commands/Connectivity/Meters/Create/MeterCreateAbbTotalflowCommand.cs new file mode 100644 index 00000000..2fc9c822 --- /dev/null +++ b/src/Atc.Kepware.Configuration.CLI/Commands/Connectivity/Meters/Create/MeterCreateAbbTotalflowCommand.cs @@ -0,0 +1,73 @@ +namespace Atc.Kepware.Configuration.CLI.Commands.Connectivity.Meters.Create; + +public sealed class MeterCreateAbbTotalflowCommand : AsyncCommand +{ + private readonly ILogger logger; + private readonly IKepwareConfigurationClient kepwareConfigurationClient; + + public MeterCreateAbbTotalflowCommand( + ILoggerFactory loggerFactory, + IKepwareConfigurationClient kepwareConfigurationClient) + { + logger = loggerFactory.CreateLogger(); + this.kepwareConfigurationClient = kepwareConfigurationClient; + } + + public override Task ExecuteAsync( + CommandContext context, + MeterCreateCommandBaseSettings settings, + CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(settings); + + return ExecuteInternalAsync(settings, cancellationToken); + } + + private async Task ExecuteInternalAsync( + MeterCreateCommandBaseSettings settings, + CancellationToken cancellationToken) + { + ConsoleHelper.WriteHeader(); + + try + { + kepwareConfigurationClient.SetConnectionInformation( + new Uri(settings.ServerUrl), + settings.UserName!.Value, + settings.Password!.Value); + + var request = BuildAbbTotalflowMeterRequest(settings); + var result = await kepwareConfigurationClient.CreateAbbTotalflowMeter( + request, + settings.ChannelName, + settings.DeviceName, + settings.MeterGroupName, + cancellationToken); + + if (!result.CommunicationSucceeded || + result.StatusCode is not (HttpStatusCode.OK or HttpStatusCode.Created)) + { + return ConsoleExitStatusCodes.Failure; + } + } + catch (Exception ex) + { + logger.LogError($"{EmojisConstants.Error} {ex.GetMessage()}"); + return ConsoleExitStatusCodes.Failure; + } + + logger.LogInformation($"{EmojisConstants.Success} Done"); + return ConsoleExitStatusCodes.Success; + } + + private static AbbTotalflowMeterRequest BuildAbbTotalflowMeterRequest( + MeterCreateCommandBaseSettings settings) + => new() + { + Name = settings.Name, + Description = settings.Description is not null && settings.Description.IsSet + ? settings.Description.Value + : string.Empty, + }; +} \ No newline at end of file diff --git a/src/Atc.Kepware.Configuration.CLI/Commands/Connectivity/Meters/Create/MeterCreateFisherRocEthernetCommand.cs b/src/Atc.Kepware.Configuration.CLI/Commands/Connectivity/Meters/Create/MeterCreateFisherRocEthernetCommand.cs new file mode 100644 index 00000000..73452e27 --- /dev/null +++ b/src/Atc.Kepware.Configuration.CLI/Commands/Connectivity/Meters/Create/MeterCreateFisherRocEthernetCommand.cs @@ -0,0 +1,73 @@ +namespace Atc.Kepware.Configuration.CLI.Commands.Connectivity.Meters.Create; + +public sealed class MeterCreateFisherRocEthernetCommand : AsyncCommand +{ + private readonly ILogger logger; + private readonly IKepwareConfigurationClient kepwareConfigurationClient; + + public MeterCreateFisherRocEthernetCommand( + ILoggerFactory loggerFactory, + IKepwareConfigurationClient kepwareConfigurationClient) + { + logger = loggerFactory.CreateLogger(); + this.kepwareConfigurationClient = kepwareConfigurationClient; + } + + public override Task ExecuteAsync( + CommandContext context, + MeterCreateCommandBaseSettings settings, + CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(settings); + + return ExecuteInternalAsync(settings, cancellationToken); + } + + private async Task ExecuteInternalAsync( + MeterCreateCommandBaseSettings settings, + CancellationToken cancellationToken) + { + ConsoleHelper.WriteHeader(); + + try + { + kepwareConfigurationClient.SetConnectionInformation( + new Uri(settings.ServerUrl), + settings.UserName!.Value, + settings.Password!.Value); + + var request = BuildFisherRocEthernetMeterRequest(settings); + var result = await kepwareConfigurationClient.CreateFisherRocEthernetMeter( + request, + settings.ChannelName, + settings.DeviceName, + settings.MeterGroupName, + cancellationToken); + + if (!result.CommunicationSucceeded || + result.StatusCode is not (HttpStatusCode.OK or HttpStatusCode.Created)) + { + return ConsoleExitStatusCodes.Failure; + } + } + catch (Exception ex) + { + logger.LogError($"{EmojisConstants.Error} {ex.GetMessage()}"); + return ConsoleExitStatusCodes.Failure; + } + + logger.LogInformation($"{EmojisConstants.Success} Done"); + return ConsoleExitStatusCodes.Success; + } + + private static FisherRocEthernetMeterRequest BuildFisherRocEthernetMeterRequest( + MeterCreateCommandBaseSettings settings) + => new() + { + Name = settings.Name, + Description = settings.Description is not null && settings.Description.IsSet + ? settings.Description.Value + : string.Empty, + }; +} \ No newline at end of file diff --git a/src/Atc.Kepware.Configuration.CLI/Commands/Connectivity/Meters/Create/MeterCreateFisherRocPlusEthernetCommand.cs b/src/Atc.Kepware.Configuration.CLI/Commands/Connectivity/Meters/Create/MeterCreateFisherRocPlusEthernetCommand.cs new file mode 100644 index 00000000..3351ec9b --- /dev/null +++ b/src/Atc.Kepware.Configuration.CLI/Commands/Connectivity/Meters/Create/MeterCreateFisherRocPlusEthernetCommand.cs @@ -0,0 +1,73 @@ +namespace Atc.Kepware.Configuration.CLI.Commands.Connectivity.Meters.Create; + +public sealed class MeterCreateFisherRocPlusEthernetCommand : AsyncCommand +{ + private readonly ILogger logger; + private readonly IKepwareConfigurationClient kepwareConfigurationClient; + + public MeterCreateFisherRocPlusEthernetCommand( + ILoggerFactory loggerFactory, + IKepwareConfigurationClient kepwareConfigurationClient) + { + logger = loggerFactory.CreateLogger(); + this.kepwareConfigurationClient = kepwareConfigurationClient; + } + + public override Task ExecuteAsync( + CommandContext context, + MeterCreateCommandBaseSettings settings, + CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(settings); + + return ExecuteInternalAsync(settings, cancellationToken); + } + + private async Task ExecuteInternalAsync( + MeterCreateCommandBaseSettings settings, + CancellationToken cancellationToken) + { + ConsoleHelper.WriteHeader(); + + try + { + kepwareConfigurationClient.SetConnectionInformation( + new Uri(settings.ServerUrl), + settings.UserName!.Value, + settings.Password!.Value); + + var request = BuildFisherRocPlusEthernetMeterRequest(settings); + var result = await kepwareConfigurationClient.CreateFisherRocPlusEthernetMeter( + request, + settings.ChannelName, + settings.DeviceName, + settings.MeterGroupName, + cancellationToken); + + if (!result.CommunicationSucceeded || + result.StatusCode is not (HttpStatusCode.OK or HttpStatusCode.Created)) + { + return ConsoleExitStatusCodes.Failure; + } + } + catch (Exception ex) + { + logger.LogError($"{EmojisConstants.Error} {ex.GetMessage()}"); + return ConsoleExitStatusCodes.Failure; + } + + logger.LogInformation($"{EmojisConstants.Success} Done"); + return ConsoleExitStatusCodes.Success; + } + + private static FisherRocPlusEthernetMeterRequest BuildFisherRocPlusEthernetMeterRequest( + MeterCreateCommandBaseSettings settings) + => new() + { + Name = settings.Name, + Description = settings.Description is not null && settings.Description.IsSet + ? settings.Description.Value + : string.Empty, + }; +} \ No newline at end of file diff --git a/src/Atc.Kepware.Configuration.CLI/Commands/Connectivity/Meters/Retrieve/MetersGetAbbTotalflowCommand.cs b/src/Atc.Kepware.Configuration.CLI/Commands/Connectivity/Meters/Retrieve/MetersGetAbbTotalflowCommand.cs new file mode 100644 index 00000000..7a02db5b --- /dev/null +++ b/src/Atc.Kepware.Configuration.CLI/Commands/Connectivity/Meters/Retrieve/MetersGetAbbTotalflowCommand.cs @@ -0,0 +1,80 @@ +namespace Atc.Kepware.Configuration.CLI.Commands.Connectivity.Meters.Retrieve; + +public sealed class MetersGetAbbTotalflowCommand : AsyncCommand +{ + private readonly ILogger logger; + private readonly IKepwareConfigurationClient kepwareConfigurationClient; + + public MetersGetAbbTotalflowCommand( + ILoggerFactory loggerFactory, + IKepwareConfigurationClient kepwareConfigurationClient) + { + logger = loggerFactory.CreateLogger(); + this.kepwareConfigurationClient = kepwareConfigurationClient; + } + + public override Task ExecuteAsync( + CommandContext context, + MeterCommandBaseSettings settings, + CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(settings); + + return ExecuteInternalAsync(settings, cancellationToken); + } + + private async Task ExecuteInternalAsync( + MeterCommandBaseSettings settings, + CancellationToken cancellationToken) + { + ConsoleHelper.WriteHeader(); + + try + { + kepwareConfigurationClient.SetConnectionInformation( + new Uri(settings.ServerUrl), + settings.UserName!.Value, + settings.Password!.Value); + + var result = await kepwareConfigurationClient.GetAbbTotalflowMeters( + settings.ChannelName, + settings.DeviceName, + settings.MeterGroupName, + cancellationToken); + + if (result is { CommunicationSucceeded: true, HasData: true }) + { + foreach (var item in result.Data!) + { + var properties = item.GetType().GetPublicProperties(); + foreach (var property in properties) + { + var typeName = $"{property.BeautifyName()}"; + var spaces = string.Empty.PadRight(10 - typeName.Length); + logger.LogInformation($"{typeName}{spaces}{property.Name}: {item.GetPropertyValue(property.Name)}"); + } + + logger.LogInformation(string.Empty); + } + } + else + { + if (result is { HasMessage: true }) + { + logger.LogWarning(result.Message); + } + + return ConsoleExitStatusCodes.Failure; + } + } + catch (Exception ex) + { + logger.LogError($"{EmojisConstants.Error} {ex.GetMessage()}"); + return ConsoleExitStatusCodes.Failure; + } + + logger.LogInformation($"{EmojisConstants.Success} Done"); + return ConsoleExitStatusCodes.Success; + } +} \ No newline at end of file diff --git a/src/Atc.Kepware.Configuration.CLI/Commands/Connectivity/Meters/Retrieve/MetersGetFisherRocEthernetCommand.cs b/src/Atc.Kepware.Configuration.CLI/Commands/Connectivity/Meters/Retrieve/MetersGetFisherRocEthernetCommand.cs new file mode 100644 index 00000000..0dc3e65e --- /dev/null +++ b/src/Atc.Kepware.Configuration.CLI/Commands/Connectivity/Meters/Retrieve/MetersGetFisherRocEthernetCommand.cs @@ -0,0 +1,80 @@ +namespace Atc.Kepware.Configuration.CLI.Commands.Connectivity.Meters.Retrieve; + +public sealed class MetersGetFisherRocEthernetCommand : AsyncCommand +{ + private readonly ILogger logger; + private readonly IKepwareConfigurationClient kepwareConfigurationClient; + + public MetersGetFisherRocEthernetCommand( + ILoggerFactory loggerFactory, + IKepwareConfigurationClient kepwareConfigurationClient) + { + logger = loggerFactory.CreateLogger(); + this.kepwareConfigurationClient = kepwareConfigurationClient; + } + + public override Task ExecuteAsync( + CommandContext context, + MeterCommandBaseSettings settings, + CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(settings); + + return ExecuteInternalAsync(settings, cancellationToken); + } + + private async Task ExecuteInternalAsync( + MeterCommandBaseSettings settings, + CancellationToken cancellationToken) + { + ConsoleHelper.WriteHeader(); + + try + { + kepwareConfigurationClient.SetConnectionInformation( + new Uri(settings.ServerUrl), + settings.UserName!.Value, + settings.Password!.Value); + + var result = await kepwareConfigurationClient.GetFisherRocEthernetMeters( + settings.ChannelName, + settings.DeviceName, + settings.MeterGroupName, + cancellationToken); + + if (result is { CommunicationSucceeded: true, HasData: true }) + { + foreach (var item in result.Data!) + { + var properties = item.GetType().GetPublicProperties(); + foreach (var property in properties) + { + var typeName = $"{property.BeautifyName()}"; + var spaces = string.Empty.PadRight(10 - typeName.Length); + logger.LogInformation($"{typeName}{spaces}{property.Name}: {item.GetPropertyValue(property.Name)}"); + } + + logger.LogInformation(string.Empty); + } + } + else + { + if (result is { HasMessage: true }) + { + logger.LogWarning(result.Message); + } + + return ConsoleExitStatusCodes.Failure; + } + } + catch (Exception ex) + { + logger.LogError($"{EmojisConstants.Error} {ex.GetMessage()}"); + return ConsoleExitStatusCodes.Failure; + } + + logger.LogInformation($"{EmojisConstants.Success} Done"); + return ConsoleExitStatusCodes.Success; + } +} \ No newline at end of file diff --git a/src/Atc.Kepware.Configuration.CLI/Commands/Connectivity/Meters/Retrieve/MetersGetFisherRocPlusEthernetCommand.cs b/src/Atc.Kepware.Configuration.CLI/Commands/Connectivity/Meters/Retrieve/MetersGetFisherRocPlusEthernetCommand.cs new file mode 100644 index 00000000..3fa2dccd --- /dev/null +++ b/src/Atc.Kepware.Configuration.CLI/Commands/Connectivity/Meters/Retrieve/MetersGetFisherRocPlusEthernetCommand.cs @@ -0,0 +1,80 @@ +namespace Atc.Kepware.Configuration.CLI.Commands.Connectivity.Meters.Retrieve; + +public sealed class MetersGetFisherRocPlusEthernetCommand : AsyncCommand +{ + private readonly ILogger logger; + private readonly IKepwareConfigurationClient kepwareConfigurationClient; + + public MetersGetFisherRocPlusEthernetCommand( + ILoggerFactory loggerFactory, + IKepwareConfigurationClient kepwareConfigurationClient) + { + logger = loggerFactory.CreateLogger(); + this.kepwareConfigurationClient = kepwareConfigurationClient; + } + + public override Task ExecuteAsync( + CommandContext context, + MeterCommandBaseSettings settings, + CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(settings); + + return ExecuteInternalAsync(settings, cancellationToken); + } + + private async Task ExecuteInternalAsync( + MeterCommandBaseSettings settings, + CancellationToken cancellationToken) + { + ConsoleHelper.WriteHeader(); + + try + { + kepwareConfigurationClient.SetConnectionInformation( + new Uri(settings.ServerUrl), + settings.UserName!.Value, + settings.Password!.Value); + + var result = await kepwareConfigurationClient.GetFisherRocPlusEthernetMeters( + settings.ChannelName, + settings.DeviceName, + settings.MeterGroupName, + cancellationToken); + + if (result is { CommunicationSucceeded: true, HasData: true }) + { + foreach (var item in result.Data!) + { + var properties = item.GetType().GetPublicProperties(); + foreach (var property in properties) + { + var typeName = $"{property.BeautifyName()}"; + var spaces = string.Empty.PadRight(10 - typeName.Length); + logger.LogInformation($"{typeName}{spaces}{property.Name}: {item.GetPropertyValue(property.Name)}"); + } + + logger.LogInformation(string.Empty); + } + } + else + { + if (result is { HasMessage: true }) + { + logger.LogWarning(result.Message); + } + + return ConsoleExitStatusCodes.Failure; + } + } + catch (Exception ex) + { + logger.LogError($"{EmojisConstants.Error} {ex.GetMessage()}"); + return ConsoleExitStatusCodes.Failure; + } + + logger.LogInformation($"{EmojisConstants.Success} Done"); + return ConsoleExitStatusCodes.Success; + } +} \ No newline at end of file diff --git a/src/Atc.Kepware.Configuration.CLI/Commands/Connectivity/Meters/Retrieve/MetersGetOmniFlowComputerCommand.cs b/src/Atc.Kepware.Configuration.CLI/Commands/Connectivity/Meters/Retrieve/MetersGetOmniFlowComputerCommand.cs new file mode 100644 index 00000000..037e64e3 --- /dev/null +++ b/src/Atc.Kepware.Configuration.CLI/Commands/Connectivity/Meters/Retrieve/MetersGetOmniFlowComputerCommand.cs @@ -0,0 +1,80 @@ +namespace Atc.Kepware.Configuration.CLI.Commands.Connectivity.Meters.Retrieve; + +public sealed class MetersGetOmniFlowComputerCommand : AsyncCommand +{ + private readonly ILogger logger; + private readonly IKepwareConfigurationClient kepwareConfigurationClient; + + public MetersGetOmniFlowComputerCommand( + ILoggerFactory loggerFactory, + IKepwareConfigurationClient kepwareConfigurationClient) + { + logger = loggerFactory.CreateLogger(); + this.kepwareConfigurationClient = kepwareConfigurationClient; + } + + public override Task ExecuteAsync( + CommandContext context, + MeterCommandBaseSettings settings, + CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(settings); + + return ExecuteInternalAsync(settings, cancellationToken); + } + + private async Task ExecuteInternalAsync( + MeterCommandBaseSettings settings, + CancellationToken cancellationToken) + { + ConsoleHelper.WriteHeader(); + + try + { + kepwareConfigurationClient.SetConnectionInformation( + new Uri(settings.ServerUrl), + settings.UserName!.Value, + settings.Password!.Value); + + var result = await kepwareConfigurationClient.GetOmniFlowComputerMeters( + settings.ChannelName, + settings.DeviceName, + settings.MeterGroupName, + cancellationToken); + + if (result is { CommunicationSucceeded: true, HasData: true }) + { + foreach (var item in result.Data!) + { + var properties = item.GetType().GetPublicProperties(); + foreach (var property in properties) + { + var typeName = $"{property.BeautifyName()}"; + var spaces = string.Empty.PadRight(10 - typeName.Length); + logger.LogInformation($"{typeName}{spaces}{property.Name}: {item.GetPropertyValue(property.Name)}"); + } + + logger.LogInformation(string.Empty); + } + } + else + { + if (result is { HasMessage: true }) + { + logger.LogWarning(result.Message); + } + + return ConsoleExitStatusCodes.Failure; + } + } + catch (Exception ex) + { + logger.LogError($"{EmojisConstants.Error} {ex.GetMessage()}"); + return ConsoleExitStatusCodes.Failure; + } + + logger.LogInformation($"{EmojisConstants.Success} Done"); + return ConsoleExitStatusCodes.Success; + } +} \ No newline at end of file diff --git a/src/Atc.Kepware.Configuration.CLI/Commands/Settings/Connectivity/Meters/MeterCommandBaseSettings.cs b/src/Atc.Kepware.Configuration.CLI/Commands/Settings/Connectivity/Meters/MeterCommandBaseSettings.cs new file mode 100644 index 00000000..c578ebf7 --- /dev/null +++ b/src/Atc.Kepware.Configuration.CLI/Commands/Settings/Connectivity/Meters/MeterCommandBaseSettings.cs @@ -0,0 +1,22 @@ +namespace Atc.Kepware.Configuration.CLI.Commands.Settings.Connectivity.Meters; + +public class MeterCommandBaseSettings : ChannelAndDeviceCommandBaseSettings +{ + [CommandOption("-m|--meter-group-name ")] + [Description("MeterGroupName")] + public string MeterGroupName { get; init; } = string.Empty; + + public override ValidationResult Validate() + { + var validationResult = base.Validate(); + if (!validationResult.Successful) + { + return validationResult; + } + + var isValidMeterGroupName = IsValidName("meter-group-name", MeterGroupName); + return isValidMeterGroupName.Successful + ? ValidationResult.Success() + : isValidMeterGroupName; + } +} \ No newline at end of file diff --git a/src/Atc.Kepware.Configuration.CLI/Commands/Settings/Connectivity/Meters/MeterCreateCommandBaseSettings.cs b/src/Atc.Kepware.Configuration.CLI/Commands/Settings/Connectivity/Meters/MeterCreateCommandBaseSettings.cs new file mode 100644 index 00000000..2d668b36 --- /dev/null +++ b/src/Atc.Kepware.Configuration.CLI/Commands/Settings/Connectivity/Meters/MeterCreateCommandBaseSettings.cs @@ -0,0 +1,26 @@ +namespace Atc.Kepware.Configuration.CLI.Commands.Settings.Connectivity.Meters; + +public class MeterCreateCommandBaseSettings : MeterCommandBaseSettings +{ + [CommandOption("-n|--name ")] + [Description("Requested Name")] + public string Name { get; init; } = string.Empty; + + [CommandOption("--description [DESCRIPTION]")] + [Description("Requested Description")] + public FlagValue? Description { get; init; } + + public override ValidationResult Validate() + { + var validationResult = base.Validate(); + if (!validationResult.Successful) + { + return validationResult; + } + + var isValidName = IsValidName(Name); + return isValidName.Successful + ? ValidationResult.Success() + : isValidName; + } +} \ No newline at end of file diff --git a/src/Atc.Kepware.Configuration.CLI/Extensions/CommandAppExtensions.cs b/src/Atc.Kepware.Configuration.CLI/Extensions/CommandAppExtensions.cs index 0d7ab84a..9c0823f3 100644 --- a/src/Atc.Kepware.Configuration.CLI/Extensions/CommandAppExtensions.cs +++ b/src/Atc.Kepware.Configuration.CLI/Extensions/CommandAppExtensions.cs @@ -19,6 +19,7 @@ private static Action> ConfigureConnectivityComma node.AddBranch("channels", ConfigureChannelsCommands()); node.AddBranch("devices", ConfigureDevicesCommands()); node.AddBranch("tags", ConfigureTagsCommands()); + node.AddBranch("meters", ConfigureMetersCommands()); }; private static Action> ConfigureChannelsCommands() @@ -1416,6 +1417,57 @@ private static void ConfigureTagDeleteCommands( .WithExample("connectivity tags delete taggroup -s [server-url] --name [tagGroupName]"); }); + private static Action> ConfigureMetersCommands() + => node => + { + node.SetDescription("Commands for meters"); + + ConfigureMeterGetCommands(node); + ConfigureMeterCreateCommands(node); + }; + + private static void ConfigureMeterGetCommands( + IConfigurator node) + => node.AddBranch("get", get => + { + get.SetDescription("Operations related to retrieving meters."); + + get.AddCommand("abbtotalflow") + .WithDescription("Get ABB Totalflow meters for a meter group.") + .WithExample("connectivity meters get abbtotalflow -s [server-url] -c [channelName] -d [deviceName] -m [meterGroupName]"); + + get.AddCommand("fisherrocethernet") + .WithDescription("Get Fisher ROC Ethernet meters for a meter group.") + .WithExample("connectivity meters get fisherrocethernet -s [server-url] -c [channelName] -d [deviceName] -m [meterGroupName]"); + + get.AddCommand("fisherrocplusethernet") + .WithDescription("Get Fisher ROC Plus Ethernet meters for a meter group.") + .WithExample("connectivity meters get fisherrocplusethernet -s [server-url] -c [channelName] -d [deviceName] -m [meterGroupName]"); + + get.AddCommand("omniflowcomputer") + .WithDescription("Get Omni Flow Computer meters for a meter group.") + .WithExample("connectivity meters get omniflowcomputer -s [server-url] -c [channelName] -d [deviceName] -m [meterGroupName]"); + }); + + private static void ConfigureMeterCreateCommands( + IConfigurator node) + => node.AddBranch("create", create => + { + create.SetDescription("Operations related to creating meters."); + + create.AddCommand("abbtotalflow") + .WithDescription("Creates an ABB Totalflow meter.") + .WithExample("connectivity meters create abbtotalflow -s [server-url] -c [channelName] -d [deviceName] -m [meterGroupName] -n [meterName]"); + + create.AddCommand("fisherrocethernet") + .WithDescription("Creates a Fisher ROC Ethernet meter.") + .WithExample("connectivity meters create fisherrocethernet -s [server-url] -c [channelName] -d [deviceName] -m [meterGroupName] -n [meterName]"); + + create.AddCommand("fisherrocplusethernet") + .WithDescription("Creates a Fisher ROC Plus Ethernet meter.") + .WithExample("connectivity meters create fisherrocplusethernet -s [server-url] -c [channelName] -d [deviceName] -m [meterGroupName] -n [meterName]"); + }); + private static Action> ConfigureIotGatewayCommands() => node => { diff --git a/src/Atc.Kepware.Configuration.CLI/GlobalUsings.cs b/src/Atc.Kepware.Configuration.CLI/GlobalUsings.cs index cc8ff6a8..bce45061 100644 --- a/src/Atc.Kepware.Configuration.CLI/GlobalUsings.cs +++ b/src/Atc.Kepware.Configuration.CLI/GlobalUsings.cs @@ -16,6 +16,8 @@ global using Atc.Kepware.Configuration.CLI.Commands.Connectivity.Devices.Create; global using Atc.Kepware.Configuration.CLI.Commands.Connectivity.Devices.Delete; global using Atc.Kepware.Configuration.CLI.Commands.Connectivity.Devices.Retrieve; +global using Atc.Kepware.Configuration.CLI.Commands.Connectivity.Meters.Create; +global using Atc.Kepware.Configuration.CLI.Commands.Connectivity.Meters.Retrieve; global using Atc.Kepware.Configuration.CLI.Commands.Connectivity.Tags; global using Atc.Kepware.Configuration.CLI.Commands.DescriptionAttributes.Connectivity; global using Atc.Kepware.Configuration.CLI.Commands.DescriptionAttributes.IotGateway; @@ -38,6 +40,7 @@ global using Atc.Kepware.Configuration.CLI.Commands.Settings.Connectivity.Devices.Create; global using Atc.Kepware.Configuration.CLI.Commands.Settings.Connectivity.Devices.Delete; global using Atc.Kepware.Configuration.CLI.Commands.Settings.Connectivity.Devices.Retrieve; +global using Atc.Kepware.Configuration.CLI.Commands.Settings.Connectivity.Meters; global using Atc.Kepware.Configuration.CLI.Commands.Settings.Connectivity.Tags; global using Atc.Kepware.Configuration.CLI.Commands.Settings.IotGateway; global using Atc.Kepware.Configuration.CLI.Commands.Settings.IotGateway.IotAgent.Create; @@ -48,6 +51,7 @@ global using Atc.Kepware.Configuration.CLI.Extensions; global using Atc.Kepware.Configuration.Contracts; global using Atc.Kepware.Configuration.Contracts.Connectivity; +global using Atc.Kepware.Configuration.Contracts.Connectivity.Drivers.AbbTotalflow; global using Atc.Kepware.Configuration.Contracts.Connectivity.Drivers.AllenBradleyControlLogixEthernet; global using Atc.Kepware.Configuration.Contracts.Connectivity.Drivers.AllenBradleyControlLogixServerEthernet; global using Atc.Kepware.Configuration.Contracts.Connectivity.Drivers.AllenBradleyEthernet; @@ -67,6 +71,8 @@ global using Atc.Kepware.Configuration.Contracts.Connectivity.Drivers.DnpClientEthernet; global using Atc.Kepware.Configuration.Contracts.Connectivity.Drivers.EuroMap63; global using Atc.Kepware.Configuration.Contracts.Connectivity.Drivers.FanucFocasEthernet; +global using Atc.Kepware.Configuration.Contracts.Connectivity.Drivers.FisherRocEthernet; +global using Atc.Kepware.Configuration.Contracts.Connectivity.Drivers.FisherRocPlusEthernet; global using Atc.Kepware.Configuration.Contracts.Connectivity.Drivers.GeEthernet; global using Atc.Kepware.Configuration.Contracts.Connectivity.Drivers.GeEthernetGlobalData; global using Atc.Kepware.Configuration.Contracts.Connectivity.Drivers.HoneywellHc900Ethernet; From 47cb898782509e3f59ac4fe96e05296eac00a9a7 Mon Sep 17 00:00:00 2001 From: Per Kops Date: Sun, 15 Feb 2026 19:21:28 +0100 Subject: [PATCH 6/6] docs: add meter CLI commands to README - Document connectivity meters get and create commands - Add examples for ABB Totalflow and Fisher ROC Ethernet meter operations --- README.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9b5ec79a..20e647fd 100644 --- a/README.md +++ b/README.md @@ -452,11 +452,11 @@ dotnet tool update --global atc-kepware-configuration ## Commands -The CLI is organized into two main command groups: +The CLI is organized into two main command groups. The connectivity group includes subcommands for channels, devices, tags, and meters: ### Connectivity Commands -Manage channels, devices, and tags: +Manage channels, devices, tags, and meters: ```bash # List all channels @@ -485,6 +485,19 @@ atc-kepware-configuration connectivity tags create tag -s \ --address R0001 \ --data-type Word \ --scan-rate 1000 + +# Get meters for a flow computer meter group +atc-kepware-configuration connectivity meters get abbtotalflow -s \ + --channel-name MyChannel \ + --device-name MyDevice \ + --meter-group-name MyMeterGroup + +# Create a meter +atc-kepware-configuration connectivity meters create fisherrocethernet -s \ + --channel-name MyChannel \ + --device-name MyDevice \ + --meter-group-name MyMeterGroup \ + --name MyMeter ``` ### IoT Gateway Commands