diff --git a/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/Commands/DisableCommand.cs b/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/Commands/DisableCommand.cs
new file mode 100644
index 00000000..68641697
--- /dev/null
+++ b/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/Commands/DisableCommand.cs
@@ -0,0 +1,55 @@
+using CliFx.Attributes;
+using CliFx.Infrastructure;
+using Opc.Ua;
+using ZB.MOM.WW.OtOpcUa.Client.Shared;
+
+namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Commands;
+
+[Command("disable", Description = "Disable an alarm condition (OPC UA Part 9 ConditionType Disable)")]
+public class DisableCommand : CommandBase
+{
+ ///
+ /// Creates the disable command used to disable an OPC UA alarm condition from the terminal.
+ ///
+ /// The factory that creates the shared client service for the command run.
+ public DisableCommand(IOpcUaClientServiceFactory factory) : base(factory)
+ {
+ }
+
+ ///
+ /// Gets the condition node ID of the alarm to disable.
+ ///
+ [CommandOption("node", 'n', Description = "Condition node ID of the alarm to disable", IsRequired = true)]
+ public string NodeId { get; init; } = default!;
+
+ ///
+ /// Connects to the server and disables the specified alarm condition.
+ ///
+ /// 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 statusCode = await service.DisableAsync(NodeId, ct);
+
+ if (StatusCode.IsGood(statusCode))
+ await console.Output.WriteLineAsync($"Disable successful: {NodeId}");
+ else
+ await console.Output.WriteLineAsync($"Disable failed: {statusCode}");
+ }
+ finally
+ {
+ if (service != null)
+ {
+ await service.DisconnectAsync();
+ service.Dispose();
+ }
+ }
+ }
+}
diff --git a/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/Commands/EnableCommand.cs b/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/Commands/EnableCommand.cs
new file mode 100644
index 00000000..2ecca347
--- /dev/null
+++ b/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/Commands/EnableCommand.cs
@@ -0,0 +1,55 @@
+using CliFx.Attributes;
+using CliFx.Infrastructure;
+using Opc.Ua;
+using ZB.MOM.WW.OtOpcUa.Client.Shared;
+
+namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Commands;
+
+[Command("enable", Description = "Enable an alarm condition (OPC UA Part 9 ConditionType Enable)")]
+public class EnableCommand : CommandBase
+{
+ ///
+ /// Creates the enable command used to enable an OPC UA alarm condition from the terminal.
+ ///
+ /// The factory that creates the shared client service for the command run.
+ public EnableCommand(IOpcUaClientServiceFactory factory) : base(factory)
+ {
+ }
+
+ ///
+ /// Gets the condition node ID of the alarm to enable.
+ ///
+ [CommandOption("node", 'n', Description = "Condition node ID of the alarm to enable", IsRequired = true)]
+ public string NodeId { get; init; } = default!;
+
+ ///
+ /// Connects to the server and enables the specified alarm condition.
+ ///
+ /// 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 statusCode = await service.EnableAsync(NodeId, ct);
+
+ if (StatusCode.IsGood(statusCode))
+ await console.Output.WriteLineAsync($"Enable successful: {NodeId}");
+ else
+ await console.Output.WriteLineAsync($"Enable failed: {statusCode}");
+ }
+ finally
+ {
+ if (service != null)
+ {
+ await service.DisconnectAsync();
+ service.Dispose();
+ }
+ }
+ }
+}
diff --git a/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/IOpcUaClientService.cs b/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/IOpcUaClientService.cs
index 2eafcbd4..42726851 100644
--- a/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/IOpcUaClientService.cs
+++ b/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/IOpcUaClientService.cs
@@ -135,6 +135,32 @@ public interface IOpcUaClientService : IDisposable
///
Task ShelveAlarmAsync(string conditionNodeId, ShelveKind kind, double shelvingTimeSeconds = 0, CancellationToken ct = default);
+ ///
+ /// Enables an alarm condition by invoking the OPC UA Part 9 ConditionType Enable method
+ /// (the H4 client-driven path). The method takes no input arguments.
+ ///
+ /// The condition node associated with the alarm being enabled.
+ /// The cancellation token that aborts the enable request.
+ ///
+ /// on success, or the server's bad
+ /// (from the underlying ) when the enable call
+ /// returns a bad result. Other transport-level failures still surface as exceptions.
+ ///
+ Task EnableAsync(string conditionNodeId, CancellationToken ct = default);
+
+ ///
+ /// Disables an alarm condition by invoking the OPC UA Part 9 ConditionType Disable method
+ /// (the H4 client-driven path). The method takes no input arguments.
+ ///
+ /// The condition node associated with the alarm being disabled.
+ /// The cancellation token that aborts the disable request.
+ ///
+ /// on success, or the server's bad
+ /// (from the underlying ) when the disable call
+ /// returns a bad result. Other transport-level failures still surface as exceptions.
+ ///
+ Task DisableAsync(string conditionNodeId, CancellationToken ct = default);
+
///
/// Reads raw historical samples for a historized node.
///
diff --git a/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/OpcUaClientService.cs b/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/OpcUaClientService.cs
index 255ce078..b54f1f5b 100644
--- a/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/OpcUaClientService.cs
+++ b/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/OpcUaClientService.cs
@@ -461,6 +461,50 @@ public sealed class OpcUaClientService : IOpcUaClientService
return StatusCodes.Good;
}
+ ///
+ public Task EnableAsync(string conditionNodeId, CancellationToken ct = default) =>
+ CallEnableDisableAsync(conditionNodeId, enabling: true, ct);
+
+ ///
+ public Task DisableAsync(string conditionNodeId, CancellationToken ct = default) =>
+ CallEnableDisableAsync(conditionNodeId, enabling: false, ct);
+
+ ///
+ /// Shared body for the H4 Enable/Disable client path. Invokes the OPC UA Part 9 ConditionType
+ /// Enable/Disable method (no input arguments) on the condition object node, mirroring
+ /// the convention: the methods live on the .Condition
+ /// child node, and a bad call result surfaces as the returned rather than
+ /// escaping as an uncaught .
+ ///
+ /// The condition node associated with the alarm.
+ /// true to invoke Enable; false to invoke Disable.
+ /// The cancellation token that aborts the request.
+ private async Task CallEnableDisableAsync(string conditionNodeId, bool enabling, CancellationToken ct)
+ {
+ ThrowIfDisposed();
+ ThrowIfNotConnected();
+
+ // Enable/Disable live on the same .Condition child node as Acknowledge/Confirm.
+ var conditionObjId = conditionNodeId.EndsWith(".Condition")
+ ? NodeId.Parse(conditionNodeId)
+ : NodeId.Parse(conditionNodeId + ".Condition");
+ var methodId = enabling ? MethodIds.ConditionType_Enable : MethodIds.ConditionType_Disable;
+
+ try
+ {
+ await _session!.CallMethodAsync(conditionObjId, methodId, [], ct);
+ }
+ catch (ServiceResultException ex)
+ {
+ Logger.Warning(ex, "Failed to {Operation} alarm on {ConditionId} (status {Status})",
+ enabling ? "enable" : "disable", conditionNodeId, ex.StatusCode);
+ return ex.StatusCode;
+ }
+
+ Logger.Debug("{Operation} alarm on {ConditionId}", enabling ? "Enabled" : "Disabled", conditionNodeId);
+ return StatusCodes.Good;
+ }
+
///
public async Task> HistoryReadRawAsync(
NodeId nodeId, DateTime startTime, DateTime endTime, int maxValues = 1000, CancellationToken ct = default)
diff --git a/tests/Client/ZB.MOM.WW.OtOpcUa.Client.CLI.Tests/DisableCommandTests.cs b/tests/Client/ZB.MOM.WW.OtOpcUa.Client.CLI.Tests/DisableCommandTests.cs
new file mode 100644
index 00000000..1c1718c8
--- /dev/null
+++ b/tests/Client/ZB.MOM.WW.OtOpcUa.Client.CLI.Tests/DisableCommandTests.cs
@@ -0,0 +1,75 @@
+using Opc.Ua;
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.Client.CLI.Commands;
+using ZB.MOM.WW.OtOpcUa.Client.CLI.Tests.Fakes;
+
+namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
+
+public class DisableCommandTests
+{
+ /// Verifies that a successful disable prints a success message and targets the node.
+ [Fact]
+ public async Task Execute_Success_PrintsSuccessMessage()
+ {
+ var fakeService = new FakeOpcUaClientService
+ {
+ DisableResult = StatusCodes.Good
+ };
+ var factory = new FakeOpcUaClientServiceFactory(fakeService);
+ var command = new DisableCommand(factory)
+ {
+ Url = "opc.tcp://localhost:4840",
+ NodeId = "ns=2;s=MyAlarm"
+ };
+
+ using var console = TestConsoleHelper.CreateConsole();
+ await command.ExecuteAsync(console);
+
+ var output = TestConsoleHelper.GetOutput(console);
+ output.ShouldContain("Disable successful");
+ fakeService.DisableCalls.Count.ShouldBe(1);
+ fakeService.DisableCalls[0].ShouldBe("ns=2;s=MyAlarm");
+ }
+
+ /// Verifies that a bad StatusCode from the service prints a failure message.
+ [Fact]
+ public async Task Execute_BadStatus_PrintsFailureMessage()
+ {
+ var fakeService = new FakeOpcUaClientService
+ {
+ DisableResult = StatusCodes.BadConditionAlreadyDisabled
+ };
+ var factory = new FakeOpcUaClientServiceFactory(fakeService);
+ var command = new DisableCommand(factory)
+ {
+ Url = "opc.tcp://localhost:4840",
+ NodeId = "ns=2;s=MyAlarm"
+ };
+
+ using var console = TestConsoleHelper.CreateConsole();
+ await command.ExecuteAsync(console);
+
+ var output = TestConsoleHelper.GetOutput(console);
+ output.ShouldContain("Disable failed");
+ }
+
+ /// Verifies that the command disconnects and disposes in the finally block.
+ [Fact]
+ public async Task Execute_DisconnectsInFinally()
+ {
+ var fakeService = new FakeOpcUaClientService();
+ var factory = new FakeOpcUaClientServiceFactory(fakeService);
+ var command = new DisableCommand(factory)
+ {
+ Url = "opc.tcp://localhost:4840",
+ NodeId = "ns=2;s=MyAlarm"
+ };
+
+ using var console = TestConsoleHelper.CreateConsole();
+ await command.ExecuteAsync(console);
+
+ fakeService.DisconnectCalled.ShouldBeTrue();
+ fakeService.DisposeCalled.ShouldBeTrue();
+ }
+}
diff --git a/tests/Client/ZB.MOM.WW.OtOpcUa.Client.CLI.Tests/EnableCommandTests.cs b/tests/Client/ZB.MOM.WW.OtOpcUa.Client.CLI.Tests/EnableCommandTests.cs
new file mode 100644
index 00000000..936b13ab
--- /dev/null
+++ b/tests/Client/ZB.MOM.WW.OtOpcUa.Client.CLI.Tests/EnableCommandTests.cs
@@ -0,0 +1,75 @@
+using Opc.Ua;
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.Client.CLI.Commands;
+using ZB.MOM.WW.OtOpcUa.Client.CLI.Tests.Fakes;
+
+namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
+
+public class EnableCommandTests
+{
+ /// Verifies that a successful enable prints a success message and targets the node.
+ [Fact]
+ public async Task Execute_Success_PrintsSuccessMessage()
+ {
+ var fakeService = new FakeOpcUaClientService
+ {
+ EnableResult = StatusCodes.Good
+ };
+ var factory = new FakeOpcUaClientServiceFactory(fakeService);
+ var command = new EnableCommand(factory)
+ {
+ Url = "opc.tcp://localhost:4840",
+ NodeId = "ns=2;s=MyAlarm"
+ };
+
+ using var console = TestConsoleHelper.CreateConsole();
+ await command.ExecuteAsync(console);
+
+ var output = TestConsoleHelper.GetOutput(console);
+ output.ShouldContain("Enable successful");
+ fakeService.EnableCalls.Count.ShouldBe(1);
+ fakeService.EnableCalls[0].ShouldBe("ns=2;s=MyAlarm");
+ }
+
+ /// Verifies that a bad StatusCode from the service prints a failure message.
+ [Fact]
+ public async Task Execute_BadStatus_PrintsFailureMessage()
+ {
+ var fakeService = new FakeOpcUaClientService
+ {
+ EnableResult = StatusCodes.BadConditionAlreadyEnabled
+ };
+ var factory = new FakeOpcUaClientServiceFactory(fakeService);
+ var command = new EnableCommand(factory)
+ {
+ Url = "opc.tcp://localhost:4840",
+ NodeId = "ns=2;s=MyAlarm"
+ };
+
+ using var console = TestConsoleHelper.CreateConsole();
+ await command.ExecuteAsync(console);
+
+ var output = TestConsoleHelper.GetOutput(console);
+ output.ShouldContain("Enable failed");
+ }
+
+ /// Verifies that the command disconnects and disposes in the finally block.
+ [Fact]
+ public async Task Execute_DisconnectsInFinally()
+ {
+ var fakeService = new FakeOpcUaClientService();
+ var factory = new FakeOpcUaClientServiceFactory(fakeService);
+ var command = new EnableCommand(factory)
+ {
+ Url = "opc.tcp://localhost:4840",
+ NodeId = "ns=2;s=MyAlarm"
+ };
+
+ using var console = TestConsoleHelper.CreateConsole();
+ await command.ExecuteAsync(console);
+
+ fakeService.DisconnectCalled.ShouldBeTrue();
+ fakeService.DisposeCalled.ShouldBeTrue();
+ }
+}
diff --git a/tests/Client/ZB.MOM.WW.OtOpcUa.Client.CLI.Tests/Fakes/FakeOpcUaClientService.cs b/tests/Client/ZB.MOM.WW.OtOpcUa.Client.CLI.Tests/Fakes/FakeOpcUaClientService.cs
index 8b213c4c..316beb7c 100644
--- a/tests/Client/ZB.MOM.WW.OtOpcUa.Client.CLI.Tests/Fakes/FakeOpcUaClientService.cs
+++ b/tests/Client/ZB.MOM.WW.OtOpcUa.Client.CLI.Tests/Fakes/FakeOpcUaClientService.cs
@@ -262,6 +262,32 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
return Task.FromResult(ShelveAlarmResult);
}
+ /// Gets the list of condition node IDs passed to EnableAsync calls.
+ public List EnableCalls { get; } = [];
+
+ /// Gets or sets the status code returned by EnableAsync.
+ public StatusCode EnableResult { get; set; } = StatusCodes.Good;
+
+ ///
+ public Task EnableAsync(string conditionNodeId, CancellationToken ct = default)
+ {
+ EnableCalls.Add(conditionNodeId);
+ return Task.FromResult(EnableResult);
+ }
+
+ /// Gets the list of condition node IDs passed to DisableAsync calls.
+ public List DisableCalls { get; } = [];
+
+ /// Gets or sets the status code returned by DisableAsync.
+ public StatusCode DisableResult { get; set; } = StatusCodes.Good;
+
+ ///
+ public Task DisableAsync(string conditionNodeId, CancellationToken ct = default)
+ {
+ DisableCalls.Add(conditionNodeId);
+ return Task.FromResult(DisableResult);
+ }
+
///
public Task> HistoryReadRawAsync(
NodeId nodeId, DateTime startTime, DateTime endTime, int maxValues = 1000, CancellationToken ct = default)
diff --git a/tests/Client/ZB.MOM.WW.OtOpcUa.Client.Shared.Tests/Fakes/FakeSessionAdapter.cs b/tests/Client/ZB.MOM.WW.OtOpcUa.Client.Shared.Tests/Fakes/FakeSessionAdapter.cs
index 5abf1068..a73a9f65 100644
--- a/tests/Client/ZB.MOM.WW.OtOpcUa.Client.Shared.Tests/Fakes/FakeSessionAdapter.cs
+++ b/tests/Client/ZB.MOM.WW.OtOpcUa.Client.Shared.Tests/Fakes/FakeSessionAdapter.cs
@@ -208,11 +208,25 @@ internal sealed class FakeSessionAdapter : ISessionAdapter
///
public List