Wire Galaxy security_classification to OPC UA AccessLevel (ReadOnly for SecuredWrite/VerifiedWrite/ViewOnly). Use deployed package chain for attribute queries to exclude undeployed attributes. Group primitive attributes under their parent variable node (merged Variable+Object). Add is_historized and is_alarm detection via HistoryExtension/AlarmExtension primitives. Implement OPC UA HistoryRead backed by Wonderware Historian Runtime database. Implement AlarmConditionState nodes driven by InAlarm with condition refresh support. Add historyread and alarms CLI commands for testing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
169 lines
6.1 KiB
C#
169 lines
6.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
|
|
{
|
|
[CommandOption("url", 'u', Description = "OPC UA server endpoint URL", IsRequired = true)]
|
|
public string Url { get; init; } = default!;
|
|
|
|
[CommandOption("node", 'n', Description = "Node ID to monitor for events (default: Server node)")]
|
|
public string? NodeId { get; init; }
|
|
|
|
[CommandOption("interval", 'i', Description = "Publishing interval in milliseconds")]
|
|
public int Interval { get; init; } = 1000;
|
|
|
|
[CommandOption("refresh", Description = "Request a ConditionRefresh after subscribing")]
|
|
public bool Refresh { get; init; }
|
|
|
|
public async ValueTask ExecuteAsync(IConsole console)
|
|
{
|
|
using var session = await OpcUaHelper.ConnectAsync(Url);
|
|
|
|
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}";
|
|
}
|
|
}
|