diff --git a/src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/ViewModels/AlarmEventViewModel.cs b/src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/ViewModels/AlarmEventViewModel.cs
index 3be319a2..0a9362be 100644
--- a/src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/ViewModels/AlarmEventViewModel.cs
+++ b/src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/ViewModels/AlarmEventViewModel.cs
@@ -96,4 +96,10 @@ public class AlarmEventViewModel : ObservableObject
/// Whether this alarm can be acknowledged (active, not yet acked, has EventId).
public bool CanAcknowledge => ActiveState && !AckedState && EventId != null && ConditionNodeId != null;
+
+ /// Whether this alarm can be shelved/unshelved (has a ConditionNodeId).
+ public bool CanShelve => ConditionNodeId != null;
+
+ /// Whether this alarm can be confirmed (already acked, has EventId + ConditionNodeId).
+ public bool CanConfirm => AckedState && EventId != null && ConditionNodeId != null;
}
diff --git a/src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/ViewModels/AlarmsViewModel.cs b/src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/ViewModels/AlarmsViewModel.cs
index f7033901..50fe5159 100644
--- a/src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/ViewModels/AlarmsViewModel.cs
+++ b/src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/ViewModels/AlarmsViewModel.cs
@@ -192,6 +192,54 @@ public partial class AlarmsViewModel : ObservableObject
}
}
+ /// Shelves / unshelves an alarm and returns (success, message).
+ /// The alarm event to shelve or unshelve.
+ /// The shelve operation (OneShot / Timed / Unshelve).
+ /// The timed-shelve duration in seconds (required for ).
+ /// A tuple with success flag and message.
+ public async Task<(bool Success, string Message)> ShelveAlarmAsync(
+ AlarmEventViewModel alarm, ShelveKind kind, double durationSeconds)
+ {
+ if (!IsConnected || alarm.ConditionNodeId == null)
+ return (false, "Alarm cannot be shelved (not connected or missing ConditionId).");
+ if (kind == ShelveKind.Timed && durationSeconds <= 0)
+ return (false, "Timed shelve requires a positive duration (seconds).");
+
+ try
+ {
+ var result = await _service.ShelveAlarmAsync(alarm.ConditionNodeId, kind, durationSeconds);
+ if (Opc.Ua.StatusCode.IsGood(result))
+ return (true, $"Alarm {kind.ToString().ToLowerInvariant()} succeeded.");
+ return (false, $"Shelve failed: {Helpers.StatusCodeFormatter.Format(result)}");
+ }
+ catch (Exception ex)
+ {
+ return (false, $"Error: {ex.Message}");
+ }
+ }
+
+ /// Confirms an alarm and returns (success, message).
+ /// The alarm event to confirm.
+ /// Optional comment for the confirmation.
+ /// A tuple with success flag and message.
+ public async Task<(bool Success, string Message)> ConfirmAlarmAsync(AlarmEventViewModel alarm, string comment)
+ {
+ if (!IsConnected || alarm.EventId == null || alarm.ConditionNodeId == null)
+ return (false, "Alarm cannot be confirmed (missing EventId or ConditionId).");
+
+ try
+ {
+ var result = await _service.ConfirmAlarmAsync(alarm.ConditionNodeId, alarm.EventId, comment);
+ if (Opc.Ua.StatusCode.IsGood(result))
+ return (true, "Alarm confirmed successfully.");
+ return (false, $"Confirm failed: {Helpers.StatusCodeFormatter.Format(result)}");
+ }
+ catch (Exception ex)
+ {
+ return (false, $"Error: {ex.Message}");
+ }
+ }
+
///
/// Returns the monitored node ID for persistence, or null if not subscribed.
///
diff --git a/tests/Client/ZB.MOM.WW.OtOpcUa.Client.UI.Tests/AlarmsViewModelTests.cs b/tests/Client/ZB.MOM.WW.OtOpcUa.Client.UI.Tests/AlarmsViewModelTests.cs
index d24a1043..503ab5b3 100644
--- a/tests/Client/ZB.MOM.WW.OtOpcUa.Client.UI.Tests/AlarmsViewModelTests.cs
+++ b/tests/Client/ZB.MOM.WW.OtOpcUa.Client.UI.Tests/AlarmsViewModelTests.cs
@@ -1,3 +1,4 @@
+using Opc.Ua;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Client.Shared.Models;
@@ -168,4 +169,220 @@ public class AlarmsViewModelTests
_vm.StatusMessage.ShouldNotBeNullOrWhiteSpace();
_vm.StatusMessage.ShouldContain("BadSubscriptionIdInvalid");
}
+
+ /// Builds a representative alarm row for shelve/confirm tests.
+ /// Whether the alarm is already acknowledged.
+ /// Whether the row carries an EventId; false models a missing EventId.
+ /// The condition node id; pass null to model a missing ConditionId.
+ private static AlarmEventViewModel BuildAlarm(
+ bool ackedState = false,
+ bool hasEventId = true,
+ string? conditionNodeId = "ns=2;s=Cond1")
+ {
+ return new AlarmEventViewModel(
+ "Source1", "HighAlarm", 500, "Temperature high",
+ true, true, ackedState, DateTime.UtcNow,
+ hasEventId ? [1, 2, 3] : null, conditionNodeId);
+ }
+
+ // --- ShelveAlarmAsync ---
+
+ /// Verifies that a OneShot shelve calls the service and reports success.
+ [Fact]
+ public async Task ShelveAlarm_Connected_OneShot_Succeeds()
+ {
+ _vm.IsConnected = true;
+ var alarm = BuildAlarm();
+
+ var (ok, _) = await _vm.ShelveAlarmAsync(alarm, ShelveKind.OneShot, 0);
+
+ ok.ShouldBeTrue();
+ _service.ShelveCallCount.ShouldBe(1);
+ _service.LastShelveCall.ShouldNotBeNull();
+ _service.LastShelveCall!.Value.Kind.ShouldBe(ShelveKind.OneShot);
+ _service.LastShelveCall.Value.ConditionNodeId.ShouldBe(alarm.ConditionNodeId);
+ }
+
+ /// Verifies that a Timed shelve passes the duration through to the service.
+ [Fact]
+ public async Task ShelveAlarm_Connected_Timed_PassesDuration()
+ {
+ _vm.IsConnected = true;
+ var alarm = BuildAlarm();
+
+ var (ok, _) = await _vm.ShelveAlarmAsync(alarm, ShelveKind.Timed, 300);
+
+ ok.ShouldBeTrue();
+ _service.LastShelveCall.ShouldNotBeNull();
+ _service.LastShelveCall!.Value.Kind.ShouldBe(ShelveKind.Timed);
+ _service.LastShelveCall.Value.ShelvingTimeSeconds.ShouldBe(300);
+ }
+
+ /// Verifies that an Unshelve calls the service and reports success.
+ [Fact]
+ public async Task ShelveAlarm_Connected_Unshelve_Succeeds()
+ {
+ _vm.IsConnected = true;
+ var alarm = BuildAlarm();
+
+ var (ok, _) = await _vm.ShelveAlarmAsync(alarm, ShelveKind.Unshelve, 0);
+
+ ok.ShouldBeTrue();
+ _service.LastShelveCall.ShouldNotBeNull();
+ _service.LastShelveCall!.Value.Kind.ShouldBe(ShelveKind.Unshelve);
+ }
+
+ /// Verifies that a Timed shelve with a non-positive duration is rejected without a service call.
+ [Fact]
+ public async Task ShelveAlarm_Timed_NonPositiveDuration_Fails_NoServiceCall()
+ {
+ _vm.IsConnected = true;
+ var alarm = BuildAlarm();
+
+ var (ok, _) = await _vm.ShelveAlarmAsync(alarm, ShelveKind.Timed, 0);
+
+ ok.ShouldBeFalse();
+ _service.ShelveCallCount.ShouldBe(0);
+ }
+
+ /// Verifies that shelving when disconnected is rejected without a service call.
+ [Fact]
+ public async Task ShelveAlarm_NotConnected_Fails_NoServiceCall()
+ {
+ _vm.IsConnected = false;
+ var alarm = BuildAlarm();
+
+ var (ok, _) = await _vm.ShelveAlarmAsync(alarm, ShelveKind.OneShot, 0);
+
+ ok.ShouldBeFalse();
+ _service.ShelveCallCount.ShouldBe(0);
+ }
+
+ /// Verifies that shelving an alarm without a ConditionNodeId is rejected without a service call.
+ [Fact]
+ public async Task ShelveAlarm_MissingConditionNodeId_Fails_NoServiceCall()
+ {
+ _vm.IsConnected = true;
+ var alarm = BuildAlarm(conditionNodeId: null);
+
+ var (ok, _) = await _vm.ShelveAlarmAsync(alarm, ShelveKind.OneShot, 0);
+
+ ok.ShouldBeFalse();
+ _service.ShelveCallCount.ShouldBe(0);
+ }
+
+ /// Verifies that a Bad status code from the service surfaces a "Shelve failed" message.
+ [Fact]
+ public async Task ShelveAlarm_ServiceBad_Fails_WithMessage()
+ {
+ _vm.IsConnected = true;
+ _service.ShelveResult = StatusCodes.BadNodeIdUnknown;
+ var alarm = BuildAlarm();
+
+ var (ok, message) = await _vm.ShelveAlarmAsync(alarm, ShelveKind.OneShot, 0);
+
+ ok.ShouldBeFalse();
+ message.ShouldContain("Shelve failed");
+ }
+
+ // --- ConfirmAlarmAsync ---
+
+ /// Verifies that confirming an alarm calls the service with the supplied comment.
+ [Fact]
+ public async Task ConfirmAlarm_Connected_Succeeds_PassesComment()
+ {
+ _vm.IsConnected = true;
+ var alarm = BuildAlarm();
+
+ var (ok, _) = await _vm.ConfirmAlarmAsync(alarm, "operator note");
+
+ ok.ShouldBeTrue();
+ _service.ConfirmCallCount.ShouldBe(1);
+ _service.LastConfirmCall.ShouldNotBeNull();
+ _service.LastConfirmCall!.Value.Comment.ShouldBe("operator note");
+ }
+
+ /// Verifies that confirming when disconnected is rejected without a service call.
+ [Fact]
+ public async Task ConfirmAlarm_NotConnected_Fails_NoServiceCall()
+ {
+ _vm.IsConnected = false;
+ var alarm = BuildAlarm();
+
+ var (ok, _) = await _vm.ConfirmAlarmAsync(alarm, "note");
+
+ ok.ShouldBeFalse();
+ _service.ConfirmCallCount.ShouldBe(0);
+ }
+
+ /// Verifies that confirming an alarm without an EventId is rejected without a service call.
+ [Fact]
+ public async Task ConfirmAlarm_MissingEventId_Fails_NoServiceCall()
+ {
+ _vm.IsConnected = true;
+ var alarm = BuildAlarm(hasEventId: false);
+
+ var (ok, _) = await _vm.ConfirmAlarmAsync(alarm, "note");
+
+ ok.ShouldBeFalse();
+ _service.ConfirmCallCount.ShouldBe(0);
+ }
+
+ /// Verifies that a Bad status code from the service surfaces a "Confirm failed" message.
+ [Fact]
+ public async Task ConfirmAlarm_ServiceBad_Fails_WithMessage()
+ {
+ _vm.IsConnected = true;
+ _service.ConfirmResult = StatusCodes.BadNodeIdUnknown;
+ var alarm = BuildAlarm();
+
+ var (ok, message) = await _vm.ConfirmAlarmAsync(alarm, "note");
+
+ ok.ShouldBeFalse();
+ message.ShouldContain("Confirm failed");
+ }
+
+ // --- CanShelve / CanConfirm predicates ---
+
+ /// Verifies CanShelve is true when a ConditionNodeId is present.
+ [Fact]
+ public void CanShelve_True_WhenConditionNodeIdPresent()
+ {
+ BuildAlarm(conditionNodeId: "ns=2;s=Cond1").CanShelve.ShouldBeTrue();
+ }
+
+ /// Verifies CanShelve is false when the ConditionNodeId is missing.
+ [Fact]
+ public void CanShelve_False_WhenConditionNodeIdNull()
+ {
+ BuildAlarm(conditionNodeId: null).CanShelve.ShouldBeFalse();
+ }
+
+ /// Verifies CanConfirm is true when acked with EventId and ConditionNodeId present.
+ [Fact]
+ public void CanConfirm_True_WhenAckedWithEventIdAndConditionNodeId()
+ {
+ BuildAlarm(ackedState: true).CanConfirm.ShouldBeTrue();
+ }
+
+ /// Verifies CanConfirm is false when the alarm has not been acknowledged.
+ [Fact]
+ public void CanConfirm_False_WhenNotAcked()
+ {
+ BuildAlarm(ackedState: false).CanConfirm.ShouldBeFalse();
+ }
+
+ /// Verifies CanConfirm is false when the EventId is missing.
+ [Fact]
+ public void CanConfirm_False_WhenEventIdNull()
+ {
+ BuildAlarm(ackedState: true, hasEventId: false).CanConfirm.ShouldBeFalse();
+ }
+
+ /// Verifies CanConfirm is false when the ConditionNodeId is missing.
+ [Fact]
+ public void CanConfirm_False_WhenConditionNodeIdNull()
+ {
+ BuildAlarm(ackedState: true, conditionNodeId: null).CanConfirm.ShouldBeFalse();
+ }
}
\ No newline at end of file