Implements configurable user authentication (anonymous + username/password) with pluggable credential provider (IUserAuthenticationProvider). Anonymous writes can be disabled via AnonymousCanWrite setting while reads remain open. Adds -U/-P flags to all CLI commands for authenticated sessions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
191 lines
7.1 KiB
C#
191 lines
7.1 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// Gets the OPC UA endpoint URL for the server whose alarm stream should be monitored.
|
|
/// </summary>
|
|
[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; }
|
|
|
|
/// <summary>
|
|
/// Gets the node to subscribe to for event notifications, typically a source object or the server node.
|
|
/// </summary>
|
|
[CommandOption("node", 'n', Description = "Node ID to monitor for events (default: Server node)")]
|
|
public string? NodeId { get; init; }
|
|
|
|
/// <summary>
|
|
/// Gets the requested publishing and sampling interval for the alarm subscription.
|
|
/// </summary>
|
|
[CommandOption("interval", 'i', Description = "Publishing interval in milliseconds")]
|
|
public int Interval { get; init; } = 1000;
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether the command should request a retained-condition refresh after subscribing.
|
|
/// </summary>
|
|
[CommandOption("refresh", Description = "Request a ConditionRefresh after subscribing")]
|
|
public bool Refresh { get; init; }
|
|
|
|
/// <summary>
|
|
/// Connects to the target server and streams alarm or condition events to the operator console.
|
|
/// </summary>
|
|
/// <param name="console">The CLI console used for cancellation and alarm-event output.</param>
|
|
public async ValueTask ExecuteAsync(IConsole console)
|
|
{
|
|
using var session = await OpcUaHelper.ConnectAsync(Url, Username, Password);
|
|
|
|
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}";
|
|
}
|
|
}
|