Scope alarm tracking to selected templates and surface endpoint/security state on the dashboard so operators can deploy in large galaxies without drowning clients in irrelevant alarms or guessing what the server is advertising

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-04-13 09:48:57 -04:00
parent c5ed5312a9
commit 517d92c76f
25 changed files with 1511 additions and 12 deletions

View File

@@ -29,6 +29,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
private readonly Dictionary<string, AlarmInfo> _alarmPriorityTags = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, AlarmInfo> _alarmDescTags = new(StringComparer.OrdinalIgnoreCase);
private readonly bool _alarmTrackingEnabled;
private readonly AlarmObjectFilter? _alarmObjectFilter;
private int _alarmFilterIncludedObjectCount;
private readonly bool _anonymousCanWrite;
private readonly AutoResetEvent _dataChangeSignal = new(false);
private readonly Dictionary<int, List<string>> _gobjectToTagRefs = new();
@@ -88,6 +90,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
/// <param name="metrics">The metrics collector used to track node manager activity.</param>
/// <param name="historianDataSource">The optional historian adapter used to satisfy OPC UA history read requests.</param>
/// <param name="alarmTrackingEnabled">Enables alarm-condition state generation for Galaxy attributes modeled as alarms.</param>
/// <param name="alarmObjectFilter">Optional template-based object filter. When supplied and enabled, only Galaxy
/// objects whose template derivation chain matches a pattern (and their descendants) contribute alarm conditions.
/// A <see langword="null"/> or disabled filter preserves the current unfiltered behavior.</param>
public LmxNodeManager(
IServerInternal server,
ApplicationConfiguration configuration,
@@ -100,7 +105,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
NodeId? writeOperateRoleId = null,
NodeId? writeTuneRoleId = null,
NodeId? writeConfigureRoleId = null,
NodeId? alarmAckRoleId = null)
NodeId? alarmAckRoleId = null,
AlarmObjectFilter? alarmObjectFilter = null)
: base(server, configuration, namespaceUri)
{
_namespaceUri = namespaceUri;
@@ -108,6 +114,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
_metrics = metrics;
_historianDataSource = historianDataSource;
_alarmTrackingEnabled = alarmTrackingEnabled;
_alarmObjectFilter = alarmObjectFilter;
_anonymousCanWrite = anonymousCanWrite;
_writeOperateRoleId = writeOperateRoleId;
_writeTuneRoleId = writeTuneRoleId;
@@ -161,6 +168,21 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
/// </summary>
public bool AlarmTrackingEnabled => _alarmTrackingEnabled;
/// <summary>
/// Gets a value indicating whether the template-based alarm object filter is enabled.
/// </summary>
public bool AlarmFilterEnabled => _alarmObjectFilter?.Enabled ?? false;
/// <summary>
/// Gets the number of compiled alarm filter patterns.
/// </summary>
public int AlarmFilterPatternCount => _alarmObjectFilter?.PatternCount ?? 0;
/// <summary>
/// Gets the number of Galaxy objects included by the alarm filter during the most recent address-space build.
/// </summary>
public int AlarmFilterIncludedObjectCount => _alarmFilterIncludedObjectCount;
/// <summary>
/// Gets the number of distinct alarm conditions currently tracked (one per alarm attribute).
/// </summary>
@@ -337,9 +359,12 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
// Build alarm tracking: create AlarmConditionState for each alarm attribute
if (_alarmTrackingEnabled)
{
var includedIds = ResolveAlarmFilterIncludedIds(sorted);
foreach (var obj in sorted)
{
if (obj.IsArea) continue;
if (includedIds != null && !includedIds.Contains(obj.GobjectId)) continue;
if (!attrsByObject.TryGetValue(obj.GobjectId, out var objAttrs)) continue;
var hasAlarms = false;
@@ -419,6 +444,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
if (hasAlarms && _nodeMap.TryGetValue(obj.GobjectId, out var objNode))
EnableEventNotifierUpChain(objNode);
}
}
// Auto-subscribe to InAlarm tags so we detect alarm transitions
if (_alarmTrackingEnabled)
@@ -433,6 +459,32 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
}
}
/// <summary>
/// Resolves the alarm object filter against the given hierarchy, updates the published include count,
/// emits a one-line summary log when the filter is active, and warns about patterns that matched nothing.
/// Returns <see langword="null"/> when no filter is configured so the alarm loop continues unfiltered.
/// </summary>
private HashSet<int>? ResolveAlarmFilterIncludedIds(IReadOnlyList<GalaxyObjectInfo> sorted)
{
if (_alarmObjectFilter == null || !_alarmObjectFilter.Enabled)
{
_alarmFilterIncludedObjectCount = 0;
return null;
}
var includedIds = _alarmObjectFilter.ResolveIncludedObjects(sorted);
_alarmFilterIncludedObjectCount = includedIds?.Count ?? 0;
Log.Information(
"Alarm filter: {IncludedCount} of {TotalCount} objects included ({PatternCount} pattern(s))",
_alarmFilterIncludedObjectCount, sorted.Count, _alarmObjectFilter.PatternCount);
foreach (var unmatched in _alarmObjectFilter.UnmatchedPatterns)
Log.Warning("Alarm filter pattern matched zero objects: {Pattern}", unmatched);
return includedIds;
}
private void SubscribeAlarmTags()
{
foreach (var kvp in _alarmInAlarmTags)
@@ -863,9 +915,11 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
// Alarm tracking for the new subtree
if (_alarmTrackingEnabled)
{
var includedIds = ResolveAlarmFilterIncludedIds(sorted);
foreach (var obj in sorted)
{
if (obj.IsArea) continue;
if (includedIds != null && !includedIds.Contains(obj.GobjectId)) continue;
if (!attrsByObject.TryGetValue(obj.GobjectId, out var objAttrs)) continue;
var hasAlarms = false;