using CliFx; using CliFx.Attributes; using CliFx.Infrastructure; using Opc.Ua; using Opc.Ua.Client; namespace OpcUaCli.Commands; [Command("alarms", Description = "Subscribe to alarm events on a node")] public class AlarmsCommand : ICommand { /// /// Gets the OPC UA endpoint URL for the server whose alarm stream should be monitored. /// [CommandOption("url", 'u', Description = "OPC UA server endpoint URL", IsRequired = true)] public string Url { get; init; } = default!; [CommandOption("username", 'U', Description = "Username for authentication")] public string? Username { get; init; } [CommandOption("password", 'P', Description = "Password for authentication")] public string? Password { get; init; } [CommandOption("security", 'S', Description = "Transport security: none, sign, encrypt (default: none)")] public string Security { get; init; } = "none"; [CommandOption("failover-urls", 'F', Description = "Comma-separated failover endpoint URLs for redundancy")] public string? FailoverUrls { get; init; } /// /// Gets the node to subscribe to for event notifications, typically a source object or the server node. /// [CommandOption("node", 'n', Description = "Node ID to monitor for events (default: Server node)")] public string? NodeId { get; init; } /// /// Gets the requested publishing and sampling interval for the alarm subscription. /// [CommandOption("interval", 'i', Description = "Publishing interval in milliseconds")] public int Interval { get; init; } = 1000; /// /// Gets a value indicating whether the command should request a retained-condition refresh after subscribing. /// [CommandOption("refresh", Description = "Request a ConditionRefresh after subscribing")] public bool Refresh { get; init; } /// /// Connects to the target server and streams alarm or condition events to the operator console. /// /// The CLI console used for cancellation and alarm-event output. public async ValueTask ExecuteAsync(IConsole console) { var urls = FailoverUrlParser.Parse(Url, FailoverUrls); using var failover = new OpcUaFailoverHelper(urls, Username, Password, Security); using var session = await failover.ConnectAsync(); var nodeId = string.IsNullOrEmpty(NodeId) ? ObjectIds.Server : new NodeId(NodeId); var subscription = new Subscription(session.DefaultSubscription) { PublishingInterval = Interval, DisplayName = "CLI Alarm Subscription" }; var item = new MonitoredItem(subscription.DefaultItem) { StartNodeId = nodeId, DisplayName = "AlarmMonitor", SamplingInterval = Interval, NodeClass = NodeClass.Object, AttributeId = Attributes.EventNotifier, Filter = CreateEventFilter() }; item.Notification += (_, e) => { if (e.NotificationValue is EventFieldList eventFields) { PrintAlarmEvent(eventFields); } }; subscription.AddItem(item); session.AddSubscription(subscription); await subscription.CreateAsync(); Console.WriteLine($"Subscribed to alarm events on {nodeId} (interval: {Interval}ms). Press Ctrl+C to stop."); Console.Out.Flush(); if (Refresh) { try { await subscription.ConditionRefreshAsync(); Console.WriteLine("Condition refresh requested."); } catch (Exception ex) { Console.WriteLine($"Condition refresh not supported: {ex.Message}"); } Console.Out.Flush(); } var ct = console.RegisterCancellationHandler(); while (!ct.IsCancellationRequested) { await Task.Delay(2000, ct).ContinueWith(_ => { }); } await console.Output.WriteLineAsync("Unsubscribed."); } private static EventFilter CreateEventFilter() { var filter = new EventFilter(); // 0: EventId filter.AddSelectClause(ObjectTypeIds.BaseEventType, BrowseNames.EventId); // 1: EventType filter.AddSelectClause(ObjectTypeIds.BaseEventType, BrowseNames.EventType); // 2: SourceName filter.AddSelectClause(ObjectTypeIds.BaseEventType, BrowseNames.SourceName); // 3: Time filter.AddSelectClause(ObjectTypeIds.BaseEventType, BrowseNames.Time); // 4: Message filter.AddSelectClause(ObjectTypeIds.BaseEventType, BrowseNames.Message); // 5: Severity filter.AddSelectClause(ObjectTypeIds.BaseEventType, BrowseNames.Severity); // 6: ConditionName filter.AddSelectClause(ObjectTypeIds.ConditionType, BrowseNames.ConditionName); // 7: Retain filter.AddSelectClause(ObjectTypeIds.ConditionType, BrowseNames.Retain); // 8: AckedState/Id filter.AddSelectClause(ObjectTypeIds.AcknowledgeableConditionType, "AckedState/Id"); // 9: ActiveState/Id filter.AddSelectClause(ObjectTypeIds.AlarmConditionType, "ActiveState/Id"); // 10: EnabledState/Id filter.AddSelectClause(ObjectTypeIds.AlarmConditionType, "EnabledState/Id"); // 11: SuppressedOrShelved filter.AddSelectClause(ObjectTypeIds.AlarmConditionType, "SuppressedOrShelved"); return filter; } private static void PrintAlarmEvent(EventFieldList eventFields) { var fields = eventFields.EventFields; if (fields == null || fields.Count < 6) return; var time = fields.Count > 3 ? fields[3].Value as DateTime? : null; var sourceName = fields.Count > 2 ? fields[2].Value as string : null; var message = fields.Count > 4 ? (fields[4].Value as LocalizedText)?.Text : null; var severity = fields.Count > 5 ? fields[5].Value : null; var conditionName = fields.Count > 6 ? fields[6].Value as string : null; var retain = fields.Count > 7 ? fields[7].Value as bool? : null; var ackedState = fields.Count > 8 ? fields[8].Value as bool? : null; var activeState = fields.Count > 9 ? fields[9].Value as bool? : null; var enabledState = fields.Count > 10 ? fields[10].Value as bool? : null; var suppressed = fields.Count > 11 ? fields[11].Value as bool? : null; Console.WriteLine($"[{time:O}] ALARM {sourceName}"); if (conditionName != null) Console.WriteLine($" Condition: {conditionName}"); if (activeState.HasValue || ackedState.HasValue) { var state = FormatAlarmState(activeState, ackedState); Console.WriteLine($" State: {state}"); } if (enabledState.HasValue) Console.WriteLine($" Enabled: {enabledState.Value}"); Console.WriteLine($" Severity: {severity}"); if (!string.IsNullOrEmpty(message)) Console.WriteLine($" Message: {message}"); if (retain.HasValue) Console.WriteLine($" Retain: {retain.Value}"); if (suppressed == true) Console.WriteLine($" Suppressed/Shelved: True"); Console.WriteLine(); } private static string FormatAlarmState(bool? active, bool? acked) { var activePart = active == true ? "Active" : "Inactive"; var ackedPart = acked == true ? "Acknowledged" : "Unacknowledged"; return $"{activePart}, {ackedPart}"; } }