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; } }