From a6dcbf62cdc74c327b50a83d1de1a7c573289e69 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 31 May 2026 02:30:05 -0400 Subject: [PATCH] feat(cli): native-alarm-source commands (template add/list/remove + instance set/clear) --- .../Commands/InstanceCommands.cs | 48 +++++++++++++ .../Commands/TemplateCommands.cs | 68 +++++++++++++++++++ src/ZB.MOM.WW.ScadaBridge.CLI/README.md | 60 ++++++++++++++++ .../CommandTreeTests.cs | 23 +++++++ 4 files changed, 199 insertions(+) diff --git a/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/InstanceCommands.cs b/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/InstanceCommands.cs index aacf645d..8dcdb641 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/InstanceCommands.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/InstanceCommands.cs @@ -24,6 +24,7 @@ public static class InstanceCommands command.Add(BuildSetBindings(urlOption, formatOption, usernameOption, passwordOption)); command.Add(BuildSetOverrides(urlOption, formatOption, usernameOption, passwordOption)); command.Add(BuildAlarmOverride(urlOption, formatOption, usernameOption, passwordOption)); + command.Add(BuildNativeAlarmSourceOverride(urlOption, formatOption, usernameOption, passwordOption)); command.Add(BuildSetArea(urlOption, formatOption, usernameOption, passwordOption)); command.Add(BuildDiff(urlOption, formatOption, usernameOption, passwordOption)); command.Add(BuildDeploy(urlOption, formatOption, usernameOption, passwordOption)); @@ -348,6 +349,53 @@ public static class InstanceCommands return group; } + private static Command BuildNativeAlarmSourceOverride(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) + { + var group = new Command("native-alarm-source") + { + Description = "Manage per-instance native alarm source overrides (retarget an inherited binding; blank = inherited)" + }; + + // set + var setIdOption = new Option("--instance-id") { Description = "Instance ID", Required = true }; + var setSourceOption = new Option("--source") { Description = "Source binding canonical name (e.g. 'Pressure' or 'Module.Pressure')", Required = true }; + var setConnectionOption = new Option("--connection") { Description = "Connection name override (blank = inherited)" }; + var setSourceRefOption = new Option("--source-ref") { Description = "Source reference override (blank = inherited)" }; + var setFilterOption = new Option("--filter") { Description = "Condition filter override (blank = inherited)" }; + var setCmd = new Command("set") { Description = "Set (upsert) a native alarm source override on an instance" }; + setCmd.Add(setIdOption); setCmd.Add(setSourceOption); setCmd.Add(setConnectionOption); + setCmd.Add(setSourceRefOption); setCmd.Add(setFilterOption); + setCmd.SetAction(async (ParseResult result) => + { + return await CommandHelpers.ExecuteCommandAsync( + result, urlOption, formatOption, usernameOption, passwordOption, + new SetInstanceNativeAlarmSourceOverrideCommand( + result.GetValue(setIdOption), + result.GetValue(setSourceOption)!, + result.GetValue(setConnectionOption), + result.GetValue(setSourceRefOption), + result.GetValue(setFilterOption))); + }); + group.Add(setCmd); + + // clear + var clearIdOption = new Option("--instance-id") { Description = "Instance ID", Required = true }; + var clearSourceOption = new Option("--source") { Description = "Source binding canonical name", Required = true }; + var clearCmd = new Command("clear") { Description = "Clear a native alarm source override on an instance (revert to inherited)" }; + clearCmd.Add(clearIdOption); clearCmd.Add(clearSourceOption); + clearCmd.SetAction(async (ParseResult result) => + { + return await CommandHelpers.ExecuteCommandAsync( + result, urlOption, formatOption, usernameOption, passwordOption, + new DeleteInstanceNativeAlarmSourceOverrideCommand( + result.GetValue(clearIdOption), + result.GetValue(clearSourceOption)!)); + }); + group.Add(clearCmd); + + return group; + } + private static Command BuildSetArea(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var idOption = new Option("--id") { Description = "Instance ID", Required = true }; diff --git a/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/TemplateCommands.cs b/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/TemplateCommands.cs index b07cf867..a6fe8cf5 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/TemplateCommands.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/TemplateCommands.cs @@ -23,6 +23,7 @@ public static class TemplateCommands command.Add(BuildDelete(urlOption, formatOption, usernameOption, passwordOption)); command.Add(BuildAttribute(urlOption, formatOption, usernameOption, passwordOption)); command.Add(BuildAlarm(urlOption, formatOption, usernameOption, passwordOption)); + command.Add(BuildNativeAlarmSource(urlOption, formatOption, usernameOption, passwordOption)); command.Add(BuildScript(urlOption, formatOption, usernameOption, passwordOption)); command.Add(BuildComposition(urlOption, formatOption, usernameOption, passwordOption)); @@ -293,6 +294,73 @@ public static class TemplateCommands return group; } + private static Command BuildNativeAlarmSource(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) + { + var group = new Command("native-alarm-source") + { + Description = "Manage template native alarm source bindings (read-only mirror of OPC UA A&C / MxGateway alarms)" + }; + + // add + var templateIdOption = new Option("--template-id") { Description = "Template ID", Required = true }; + var nameOption = new Option("--name") { Description = "Source binding name", Required = true }; + var connectionOption = new Option("--connection") { Description = "Alarm-capable data connection name", Required = true }; + var sourceRefOption = new Option("--source-ref") { Description = "Source reference (OPC UA SourceNode nodeId, or MxAccess object/area)", Required = true }; + var filterOption = new Option("--filter") { Description = "Optional condition filter (null = mirror all conditions under the source)" }; + var descOption = new Option("--description") { Description = "Description" }; + var lockedOption = new Option("--locked") { Description = "Lock status" }; + lockedOption.DefaultValueFactory = _ => false; + + var addCmd = new Command("add") { Description = "Add a native alarm source binding to a template" }; + addCmd.Add(templateIdOption); + addCmd.Add(nameOption); + addCmd.Add(connectionOption); + addCmd.Add(sourceRefOption); + addCmd.Add(filterOption); + addCmd.Add(descOption); + addCmd.Add(lockedOption); + addCmd.SetAction(async (ParseResult result) => + { + return await CommandHelpers.ExecuteCommandAsync( + result, urlOption, formatOption, usernameOption, passwordOption, + new AddTemplateNativeAlarmSourceCommand( + result.GetValue(templateIdOption), + result.GetValue(nameOption)!, + result.GetValue(connectionOption)!, + result.GetValue(sourceRefOption)!, + result.GetValue(filterOption), + result.GetValue(descOption), + result.GetValue(lockedOption))); + }); + group.Add(addCmd); + + // list + var listIdOption = new Option("--template-id") { Description = "Template ID", Required = true }; + var listCmd = new Command("list") { Description = "List native alarm source bindings on a template" }; + listCmd.Add(listIdOption); + listCmd.SetAction(async (ParseResult result) => + { + return await CommandHelpers.ExecuteCommandAsync( + result, urlOption, formatOption, usernameOption, passwordOption, + new ListTemplateNativeAlarmSourcesCommand(result.GetValue(listIdOption))); + }); + group.Add(listCmd); + + // remove + var removeIdOption = new Option("--id") { Description = "Native alarm source ID", Required = true }; + var removeCmd = new Command("remove") { Description = "Remove a native alarm source binding from a template" }; + removeCmd.Add(removeIdOption); + removeCmd.SetAction(async (ParseResult result) => + { + return await CommandHelpers.ExecuteCommandAsync( + result, urlOption, formatOption, usernameOption, passwordOption, + new DeleteTemplateNativeAlarmSourceCommand(result.GetValue(removeIdOption))); + }); + group.Add(removeCmd); + + return group; + } + private static Command BuildScript(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var group = new Command("script") { Description = "Manage template scripts" }; diff --git a/src/ZB.MOM.WW.ScadaBridge.CLI/README.md b/src/ZB.MOM.WW.ScadaBridge.CLI/README.md index 489fe912..c41ee97f 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CLI/README.md +++ b/src/ZB.MOM.WW.ScadaBridge.CLI/README.md @@ -252,6 +252,41 @@ scadabridge --url template alarm delete --template-id --name template native-alarm-source add --template-id --name --connection --source-ref [--filter ] [--description ] [--locked] +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--template-id` | yes | Template ID | +| `--name` | yes | Source binding name | +| `--connection` | yes | Alarm-capable data connection name (OPC UA or MxGateway) | +| `--source-ref` | yes | Source reference (OPC UA SourceNode nodeId, or MxAccess object/area) | +| `--filter` | no | Condition filter (omit to mirror all conditions under the source) | +| `--description` | no | Description | +| `--locked` | no | Lock the binding in derived templates | + +#### `template native-alarm-source list` + +List the native alarm source bindings on a template. + +```sh +scadabridge --url template native-alarm-source list --template-id +``` + +#### `template native-alarm-source remove` + +Remove a native alarm source binding from a template (by its ID). + +```sh +scadabridge --url template native-alarm-source remove --id +``` + #### `template script add` Add a script to a template. @@ -370,6 +405,31 @@ scadabridge --url instance create --name --template-id --si | `--site-id` | yes | Site where the instance will run | | `--area-id` | no | Area within the site | +#### `instance native-alarm-source set` + +Override an inherited native alarm source binding for a single instance (upsert). Blank +options keep the inherited value. + +```sh +scadabridge --url instance native-alarm-source set --instance-id --source [--connection ] [--source-ref ] [--filter ] +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--instance-id` | yes | Instance ID | +| `--source` | yes | Source binding canonical name (e.g. `Pressure` or `Module.Pressure`) | +| `--connection` | no | Connection name override (blank = inherited) | +| `--source-ref` | no | Source reference override (blank = inherited) | +| `--filter` | no | Condition filter override (blank = inherited) | + +#### `instance native-alarm-source clear` + +Clear an instance's native alarm source override, reverting to the inherited binding. + +```sh +scadabridge --url instance native-alarm-source clear --instance-id --source +``` + #### `instance deploy` Deploy an instance to its site. Acquires the per-instance operation lock. diff --git a/tests/ZB.MOM.WW.ScadaBridge.CLI.Tests/CommandTreeTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CLI.Tests/CommandTreeTests.cs index c3a63b71..d6823ce9 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.CLI.Tests/CommandTreeTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.CLI.Tests/CommandTreeTests.cs @@ -129,6 +129,27 @@ public class CommandTreeTests Assert.DoesNotContain("--instance-name", optionNames); } + [Fact] + public void TemplateNativeAlarmSource_HasAddListRemove() + { + var template = TemplateCommands.Build(Url, Format, Username, Password); + var group = template.Subcommands.Single(c => c.Name == "native-alarm-source"); + var subNames = group.Subcommands.Select(c => c.Name).ToHashSet(); + Assert.Contains("add", subNames); + Assert.Contains("list", subNames); + Assert.Contains("remove", subNames); + } + + [Fact] + public void InstanceNativeAlarmSource_HasSetAndClear() + { + var instance = InstanceCommands.Build(Url, Format, Username, Password); + var group = instance.Subcommands.Single(c => c.Name == "native-alarm-source"); + var subNames = group.Subcommands.Select(c => c.Name).ToHashSet(); + Assert.Contains("set", subNames); + Assert.Contains("clear", subNames); + } + [Theory] [InlineData(typeof(GetInstanceCommand))] [InlineData(typeof(ListSitesCommand))] @@ -141,6 +162,8 @@ public class CommandTreeTests [InlineData(typeof(ExportBundleCommand))] [InlineData(typeof(PreviewBundleCommand))] [InlineData(typeof(ImportBundleCommand))] + [InlineData(typeof(AddTemplateNativeAlarmSourceCommand))] + [InlineData(typeof(SetInstanceNativeAlarmSourceOverrideCommand))] public void CommandPayloadTypes_ResolveViaRegistry(Type commandType) { // GetCommandName throws ArgumentException for an unregistered type — the CLI