feat(client-ui): AlarmsViewModel Shelve/Confirm methods + CanShelve/CanConfirm
This commit is contained in:
@@ -96,4 +96,10 @@ public class AlarmEventViewModel : ObservableObject
|
||||
|
||||
/// <summary>Whether this alarm can be acknowledged (active, not yet acked, has EventId).</summary>
|
||||
public bool CanAcknowledge => ActiveState && !AckedState && EventId != null && ConditionNodeId != null;
|
||||
|
||||
/// <summary>Whether this alarm can be shelved/unshelved (has a ConditionNodeId).</summary>
|
||||
public bool CanShelve => ConditionNodeId != null;
|
||||
|
||||
/// <summary>Whether this alarm can be confirmed (already acked, has EventId + ConditionNodeId).</summary>
|
||||
public bool CanConfirm => AckedState && EventId != null && ConditionNodeId != null;
|
||||
}
|
||||
|
||||
@@ -192,6 +192,54 @@ public partial class AlarmsViewModel : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Shelves / unshelves an alarm and returns (success, message).</summary>
|
||||
/// <param name="alarm">The alarm event to shelve or unshelve.</param>
|
||||
/// <param name="kind">The shelve operation (OneShot / Timed / Unshelve).</param>
|
||||
/// <param name="durationSeconds">The timed-shelve duration in seconds (required for <see cref="ShelveKind.Timed"/>).</param>
|
||||
/// <returns>A tuple with success flag and message.</returns>
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Confirms an alarm and returns (success, message).</summary>
|
||||
/// <param name="alarm">The alarm event to confirm.</param>
|
||||
/// <param name="comment">Optional comment for the confirmation.</param>
|
||||
/// <returns>A tuple with success flag and message.</returns>
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the monitored node ID for persistence, or null if not subscribed.
|
||||
/// </summary>
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
/// <summary>Builds a representative alarm row for shelve/confirm tests.</summary>
|
||||
/// <param name="ackedState">Whether the alarm is already acknowledged.</param>
|
||||
/// <param name="hasEventId">Whether the row carries an EventId; <c>false</c> models a missing EventId.</param>
|
||||
/// <param name="conditionNodeId">The condition node id; pass <c>null</c> to model a missing ConditionId.</param>
|
||||
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 ---
|
||||
|
||||
/// <summary>Verifies that a OneShot shelve calls the service and reports success.</summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a Timed shelve passes the duration through to the service.</summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an Unshelve calls the service and reports success.</summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a Timed shelve with a non-positive duration is rejected without a service call.</summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that shelving when disconnected is rejected without a service call.</summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that shelving an alarm without a ConditionNodeId is rejected without a service call.</summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a Bad status code from the service surfaces a "Shelve failed" message.</summary>
|
||||
[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 ---
|
||||
|
||||
/// <summary>Verifies that confirming an alarm calls the service with the supplied comment.</summary>
|
||||
[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");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that confirming when disconnected is rejected without a service call.</summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that confirming an alarm without an EventId is rejected without a service call.</summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a Bad status code from the service surfaces a "Confirm failed" message.</summary>
|
||||
[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 ---
|
||||
|
||||
/// <summary>Verifies CanShelve is true when a ConditionNodeId is present.</summary>
|
||||
[Fact]
|
||||
public void CanShelve_True_WhenConditionNodeIdPresent()
|
||||
{
|
||||
BuildAlarm(conditionNodeId: "ns=2;s=Cond1").CanShelve.ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <summary>Verifies CanShelve is false when the ConditionNodeId is missing.</summary>
|
||||
[Fact]
|
||||
public void CanShelve_False_WhenConditionNodeIdNull()
|
||||
{
|
||||
BuildAlarm(conditionNodeId: null).CanShelve.ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <summary>Verifies CanConfirm is true when acked with EventId and ConditionNodeId present.</summary>
|
||||
[Fact]
|
||||
public void CanConfirm_True_WhenAckedWithEventIdAndConditionNodeId()
|
||||
{
|
||||
BuildAlarm(ackedState: true).CanConfirm.ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <summary>Verifies CanConfirm is false when the alarm has not been acknowledged.</summary>
|
||||
[Fact]
|
||||
public void CanConfirm_False_WhenNotAcked()
|
||||
{
|
||||
BuildAlarm(ackedState: false).CanConfirm.ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <summary>Verifies CanConfirm is false when the EventId is missing.</summary>
|
||||
[Fact]
|
||||
public void CanConfirm_False_WhenEventIdNull()
|
||||
{
|
||||
BuildAlarm(ackedState: true, hasEventId: false).CanConfirm.ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <summary>Verifies CanConfirm is false when the ConditionNodeId is missing.</summary>
|
||||
[Fact]
|
||||
public void CanConfirm_False_WhenConditionNodeIdNull()
|
||||
{
|
||||
BuildAlarm(ackedState: true, conditionNodeId: null).CanConfirm.ShouldBeFalse();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user