using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Opc.Ua;
using ZB.MOM.WW.OtOpcUa.Client.Shared;
using ZB.MOM.WW.OtOpcUa.Client.Shared.Models;
using ZB.MOM.WW.OtOpcUa.Client.UI.Services;
namespace ZB.MOM.WW.OtOpcUa.Client.UI.ViewModels;
///
/// ViewModel for the alarms panel.
///
public partial class AlarmsViewModel : ObservableObject
{
private readonly IUiDispatcher _dispatcher;
private readonly IOpcUaClientService _service;
[ObservableProperty] private int _interval = 1000;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SubscribeCommand))]
[NotifyCanExecuteChangedFor(nameof(UnsubscribeCommand))]
[NotifyCanExecuteChangedFor(nameof(RefreshCommand))]
private bool _isConnected;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SubscribeCommand))]
[NotifyCanExecuteChangedFor(nameof(UnsubscribeCommand))]
[NotifyCanExecuteChangedFor(nameof(RefreshCommand))]
private bool _isSubscribed;
[ObservableProperty] private string? _monitoredNodeIdText;
[ObservableProperty] private int _activeAlarmCount;
public AlarmsViewModel(IOpcUaClientService service, IUiDispatcher dispatcher)
{
_service = service;
_dispatcher = dispatcher;
_service.AlarmEvent += OnAlarmEvent;
}
/// Received alarm events.
public ObservableCollection AlarmEvents { get; } = [];
private void OnAlarmEvent(object? sender, AlarmEventArgs e)
{
// Only display alarm/condition events (those with a ConditionName), not generic events
if (string.IsNullOrEmpty(e.ConditionName)) return;
_dispatcher.Post(() =>
{
// Find existing row by source + condition and update it, or add new
var existing = AlarmEvents.FirstOrDefault(a =>
a.SourceName == e.SourceName && a.ConditionName == e.ConditionName);
if (existing != null)
{
var index = AlarmEvents.IndexOf(existing);
AlarmEvents[index] = new AlarmEventViewModel(
e.SourceName, e.ConditionName, e.Severity, e.Message,
e.Retain, e.ActiveState, e.AckedState, e.Time,
e.EventId, e.ConditionNodeId);
// Remove alarms that are no longer retained
if (!e.Retain)
AlarmEvents.RemoveAt(index);
}
else if (e.Retain)
{
AlarmEvents.Add(new AlarmEventViewModel(
e.SourceName, e.ConditionName, e.Severity, e.Message,
e.Retain, e.ActiveState, e.AckedState, e.Time,
e.EventId, e.ConditionNodeId));
}
ActiveAlarmCount = AlarmEvents.Count(a => a.ActiveState && !a.AckedState);
});
}
private bool CanSubscribe()
{
return IsConnected && !IsSubscribed;
}
[RelayCommand(CanExecute = nameof(CanSubscribe))]
private async Task SubscribeAsync()
{
try
{
var sourceNodeId = string.IsNullOrWhiteSpace(MonitoredNodeIdText)
? null
: NodeId.Parse(MonitoredNodeIdText);
await _service.SubscribeAlarmsAsync(sourceNodeId, Interval);
IsSubscribed = true;
try
{
await _service.RequestConditionRefreshAsync();
}
catch
{
// Refresh not supported
}
}
catch
{
// Subscribe failed
}
}
private bool CanUnsubscribe()
{
return IsConnected && IsSubscribed;
}
[RelayCommand(CanExecute = nameof(CanUnsubscribe))]
private async Task UnsubscribeAsync()
{
try
{
await _service.UnsubscribeAlarmsAsync();
IsSubscribed = false;
}
catch
{
// Unsubscribe failed
}
}
[RelayCommand(CanExecute = nameof(CanUnsubscribe))]
private async Task RefreshAsync()
{
try
{
await _service.RequestConditionRefreshAsync();
}
catch
{
// Refresh failed
}
}
///
/// Acknowledges an alarm and returns (success, message).
///
public async Task<(bool Success, string Message)> AcknowledgeAlarmAsync(AlarmEventViewModel alarm, string comment)
{
if (!IsConnected || alarm.EventId == null || alarm.ConditionNodeId == null)
return (false, "Alarm cannot be acknowledged (missing EventId or ConditionId).");
try
{
var result = await _service.AcknowledgeAlarmAsync(alarm.ConditionNodeId, alarm.EventId, comment);
if (Opc.Ua.StatusCode.IsGood(result))
return (true, "Alarm acknowledged successfully.");
return (false, $"Acknowledge 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.
///
public string? GetAlarmSourceNodeId()
{
return IsSubscribed ? MonitoredNodeIdText : null;
}
///
/// Restores an alarm subscription and requests a condition refresh.
///
public async Task RestoreAlarmSubscriptionAsync(string? sourceNodeId)
{
if (!IsConnected || string.IsNullOrWhiteSpace(sourceNodeId)) return;
MonitoredNodeIdText = sourceNodeId;
try
{
var nodeId = string.IsNullOrWhiteSpace(sourceNodeId)
? null
: NodeId.Parse(sourceNodeId);
await _service.SubscribeAlarmsAsync(nodeId, Interval);
IsSubscribed = true;
try
{
await _service.RequestConditionRefreshAsync();
}
catch
{
// Refresh not supported
}
}
catch
{
// Subscribe failed
}
}
///
/// Clears alarm events and resets state.
///
public void Clear()
{
AlarmEvents.Clear();
IsSubscribed = false;
ActiveAlarmCount = 0;
}
///
/// Re-hooks event handlers to the service after a server-side reconnect.
/// Safe to call when already attached (duplicate += is a no-op in .NET multicast delegates).
///
public void Reattach()
{
_service.AlarmEvent -= OnAlarmEvent;
_service.AlarmEvent += OnAlarmEvent;
}
///
/// Unhooks event handlers from the service.
///
public void Teardown()
{
_service.AlarmEvent -= OnAlarmEvent;
}
}