diff --git a/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/Commands/ShelveCommand.cs b/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/Commands/ShelveCommand.cs
index 3b962643..0c03386d 100644
--- a/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/Commands/ShelveCommand.cs
+++ b/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/Commands/ShelveCommand.cs
@@ -31,9 +31,10 @@ public class ShelveCommand : CommandBase
public string Kind { get; init; } = default!;
///
- /// Gets the shelving duration in seconds for Timed shelving (ignored for OneShot and Unshelve).
+ /// Gets the shelving duration in seconds for Timed shelving (must be > 0; ignored for OneShot and Unshelve).
+ /// The value is passed in seconds by the operator and converted to milliseconds for the OPC UA TimedShelve call.
///
- [CommandOption("duration", 'd', Description = "Shelving duration in seconds (required for --kind Timed)")]
+ [CommandOption("duration", 'd', Description = "Shelving duration in seconds (must be > 0; in seconds, converted to milliseconds for the OPC UA call; required for --kind Timed)")]
public double DurationSeconds { get; init; }
///
@@ -61,9 +62,9 @@ public class ShelveCommand : CommandBase
var statusCode = await service.ShelveAlarmAsync(NodeId, shelveKind, DurationSeconds, ct);
if (StatusCode.IsGood(statusCode))
- await console.Output.WriteLineAsync($"Shelve ({shelveKind}) successful: {NodeId}");
+ await console.Output.WriteLineAsync($"{shelveKind} successful: {NodeId}");
else
- await console.Output.WriteLineAsync($"Shelve ({shelveKind}) failed: {statusCode}");
+ await console.Output.WriteLineAsync($"{shelveKind} failed: {statusCode}");
}
finally
{
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 8d9d5916..2eafcbd4 100644
--- a/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/IOpcUaClientService.cs
+++ b/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/IOpcUaClientService.cs
@@ -123,7 +123,9 @@ public interface IOpcUaClientService : IDisposable
/// The condition node associated with the alarm being shelved.
/// The shelve operation: OneShot, Timed, or Unshelve.
///
- /// For Timed shelving: the shelving duration in seconds. Ignored for OneShot and Unshelve.
+ /// For Timed shelving: the shelving duration in seconds (must be > 0).
+ /// This value is converted to milliseconds when passed to the OPC UA TimedShelve method
+ /// (Duration is milliseconds per OPC UA Part 9). Ignored for OneShot and Unshelve.
///
/// The cancellation token that aborts the shelve request.
///
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 45d88aec..28d9870c 100644
--- a/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/OpcUaClientService.cs
+++ b/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/OpcUaClientService.cs
@@ -417,7 +417,9 @@ public sealed class OpcUaClientService : IOpcUaClientService
// The shelve methods live on the ShelvingState child of the condition node.
// conditionNodeId is the alarm/condition node; append .ShelvingState to get the state machine node.
- var shelvingStateNodeId = NodeId.Parse(conditionNodeId + ".ShelvingState");
+ var shelvingStateNodeId = conditionNodeId.EndsWith(".ShelvingState")
+ ? NodeId.Parse(conditionNodeId)
+ : NodeId.Parse(conditionNodeId + ".ShelvingState");
NodeId methodId;
object[] inputArgs;
@@ -433,7 +435,8 @@ public sealed class OpcUaClientService : IOpcUaClientService
throw new ArgumentOutOfRangeException(nameof(shelvingTimeSeconds),
"Timed shelving requires a positive shelvingTimeSeconds value.");
methodId = MethodIds.AlarmConditionType_ShelvingState_TimedShelve;
- inputArgs = [shelvingTimeSeconds];
+ // OPC UA Part 9 TimedShelve ShelvingTime input is a Duration (Double) in milliseconds.
+ inputArgs = [shelvingTimeSeconds * 1000.0];
break;
case ShelveKind.Unshelve:
methodId = MethodIds.AlarmConditionType_ShelvingState_Unshelve;
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 d5ea10e6..5abf1068 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
@@ -202,11 +202,18 @@ internal sealed class FakeSessionAdapter : ISessionAdapter
///
public Exception? CallMethodException { get; set; }
+ ///
+ /// Records the input arguments passed to each invocation.
+ /// Index 0 is the first call, index 1 the second, etc.
+ ///
+ public List