Add security classification, alarm detection, historical data access, and primitive grouping

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>
This commit is contained in:
Joseph Doherty
2026-03-26 11:32:33 -04:00
parent bb0a89b2a1
commit 415e62c585
30 changed files with 2734 additions and 217 deletions

View File

@@ -20,6 +20,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status
private PerformanceMetrics? _metrics;
private GalaxyRepositoryStats? _galaxyStats;
private OpcUaServerHost? _serverHost;
private LmxNodeManager? _nodeManager;
/// <summary>
/// Initializes a new status report service for the dashboard using the supplied health-check policy and refresh interval.
@@ -40,12 +41,14 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status
/// <param name="galaxyStats">The Galaxy repository statistics to surface on the dashboard.</param>
/// <param name="serverHost">The OPC UA server host whose active session count should be reported.</param>
public void SetComponents(IMxAccessClient? mxAccessClient, PerformanceMetrics? metrics,
GalaxyRepositoryStats? galaxyStats, OpcUaServerHost? serverHost)
GalaxyRepositoryStats? galaxyStats, OpcUaServerHost? serverHost,
LmxNodeManager? nodeManager = null)
{
_mxAccessClient = mxAccessClient;
_metrics = metrics;
_galaxyStats = galaxyStats;
_serverHost = serverHost;
_nodeManager = nodeManager;
}
/// <summary>
@@ -78,6 +81,13 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status
AttributeCount = _galaxyStats?.AttributeCount ?? 0,
LastRebuildTime = _galaxyStats?.LastRebuildTime
},
DataChange = new DataChangeInfo
{
EventsPerSecond = _nodeManager?.MxChangeEventsPerSecond ?? 0,
AvgBatchSize = _nodeManager?.AverageDispatchBatchSize ?? 0,
PendingItems = _nodeManager?.PendingDataChangeCount ?? 0,
TotalEvents = _nodeManager?.TotalMxChangeEvents ?? 0
},
Operations = _metrics?.GetStatistics() ?? new(),
Footer = new FooterInfo
{
@@ -97,6 +107,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status
var sb = new StringBuilder();
sb.AppendLine("<!DOCTYPE html><html><head>");
sb.AppendLine("<meta charset='utf-8'>");
sb.AppendLine($"<meta http-equiv='refresh' content='{_refreshIntervalSeconds}'>");
sb.AppendLine("<title>LmxOpcUa Status</title>");
sb.AppendLine("<style>");
@@ -124,6 +135,11 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status
sb.AppendLine($"<p>Active: {data.Subscriptions.ActiveCount}</p>");
sb.AppendLine("</div>");
// Data Change Dispatch panel
sb.AppendLine("<div class='panel gray'><h2>Data Change Dispatch</h2>");
sb.AppendLine($"<p>Events/sec: <b>{data.DataChange.EventsPerSecond:F1}</b> | Avg Batch Size: <b>{data.DataChange.AvgBatchSize:F1}</b> | Pending: {data.DataChange.PendingItems} | Total Events: {data.DataChange.TotalEvents:N0}</p>");
sb.AppendLine("</div>");
// Galaxy Info panel
sb.AppendLine("<div class='panel gray'><h2>Galaxy Info</h2>");
sb.AppendLine($"<p>Galaxy: <b>{data.Galaxy.GalaxyName}</b> | DB: {(data.Galaxy.DbConnected ? "Connected" : "Disconnected")}</p>");