Fix alarm acknowledge EventId validation and add auth plan

Set a new EventId (GUID) on AlarmConditionState each time an alarm event
is reported so the framework can match it when clients call Acknowledge.
Without this, the framework rejected all ack attempts with BadEventIdUnknown.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-03-27 01:39:21 -04:00
parent 9368767b1b
commit b27d355763
2 changed files with 323 additions and 6 deletions

View File

@@ -72,6 +72,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
// Alarm tracking: maps InAlarm tag reference → alarm source info
private readonly Dictionary<string, AlarmInfo> _alarmInAlarmTags = new Dictionary<string, AlarmInfo>(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, AlarmInfo> _alarmAckedTags = new Dictionary<string, AlarmInfo>(StringComparer.OrdinalIgnoreCase);
// Incremental sync: persistent node map and reverse lookup
private readonly Dictionary<int, NodeState> _nodeMap = new Dictionary<int, NodeState>();
@@ -125,6 +126,16 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
/// Gets or sets the cached alarm message used when emitting active and cleared events.
/// </summary>
public string CachedMessage { get; set; } = "";
/// <summary>
/// Gets or sets the Galaxy tag reference for the alarm acknowledged state.
/// </summary>
public string AckedTagReference { get; set; } = "";
/// <summary>
/// Gets or sets the Galaxy tag reference for the acknowledge message that triggers acknowledgment.
/// </summary>
public string AckMsgTagReference { get; set; } = "";
}
/// <summary>
@@ -218,6 +229,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
_tagToVariableNode.Clear();
_tagMetadata.Clear();
_alarmInAlarmTags.Clear();
_alarmAckedTags.Clear();
_nodeMap.Clear();
_gobjectToTagRefs.Clear();
VariableNodeCount = 0;
@@ -377,6 +389,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
condition.SetSeverity(SystemContext, EventSeverity.Medium);
condition.Retain.Value = false;
condition.OnReportEvent = (context, node, e) => Server.ReportEvent(context, e);
condition.OnAcknowledge = OnAlarmAcknowledge;
// Add HasCondition reference from source to condition
if (sourceVariable != null)
@@ -388,15 +401,19 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
AddPredefinedNode(SystemContext, condition);
var baseTagRef = alarmAttr.FullTagReference.TrimEnd('[', ']');
_alarmInAlarmTags[inAlarmTagRef] = new AlarmInfo
var alarmInfo = new AlarmInfo
{
SourceTagReference = alarmAttr.FullTagReference,
SourceNodeId = sourceNodeId,
SourceName = alarmAttr.AttributeName,
ConditionNode = condition,
PriorityTagReference = baseTagRef + ".Priority",
DescAttrNameTagReference = baseTagRef + ".DescAttrName"
DescAttrNameTagReference = baseTagRef + ".DescAttrName",
AckedTagReference = baseTagRef + ".Acked",
AckMsgTagReference = baseTagRef + ".AckMsg"
};
_alarmInAlarmTags[inAlarmTagRef] = alarmInfo;
_alarmAckedTags[alarmInfo.AckedTagReference] = alarmInfo;
hasAlarms = true;
}
@@ -427,7 +444,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
foreach (var kvp in _alarmInAlarmTags)
{
// Subscribe to InAlarm, Priority, and DescAttrName for each alarm
var tagsToSubscribe = new[] { kvp.Key, kvp.Value.PriorityTagReference, kvp.Value.DescAttrNameTagReference };
var tagsToSubscribe = new[] { kvp.Key, kvp.Value.PriorityTagReference, kvp.Value.DescAttrNameTagReference, kvp.Value.AckedTagReference };
foreach (var tag in tagsToSubscribe)
{
if (string.IsNullOrEmpty(tag) || !_tagToVariableNode.ContainsKey(tag))
@@ -444,6 +461,30 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
}
}
private ServiceResult OnAlarmAcknowledge(
ISystemContext context, ConditionState condition, byte[] eventId, LocalizedText comment)
{
var alarmInfo = _alarmInAlarmTags.Values
.FirstOrDefault(a => a.ConditionNode == condition);
if (alarmInfo == null)
return new ServiceResult(StatusCodes.BadNodeIdUnknown);
try
{
var ackMessage = comment?.Text ?? "";
_mxAccessClient.WriteAsync(alarmInfo.AckMsgTagReference, ackMessage)
.GetAwaiter().GetResult();
Log.Information("Alarm acknowledge sent: {Source} (Message={AckMsg})",
alarmInfo.SourceName, ackMessage);
return ServiceResult.Good;
}
catch (Exception ex)
{
Log.Warning(ex, "Failed to write AckMsg for {Source}", alarmInfo.SourceName);
return new ServiceResult(StatusCodes.BadInternalError);
}
}
private void ReportAlarmEvent(AlarmInfo info, bool active)
{
var condition = info.ConditionNode;
@@ -455,6 +496,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
? (!string.IsNullOrEmpty(info.CachedMessage) ? info.CachedMessage : $"Alarm active: {info.SourceName}")
: $"Alarm cleared: {info.SourceName}";
// Set a new EventId so clients can reference this event for acknowledge
condition.EventId.Value = Guid.NewGuid().ToByteArray();
condition.SetActiveState(SystemContext, active);
condition.Message.Value = new LocalizedText("en", message);
condition.SetSeverity(SystemContext, (EventSeverity)severity);
@@ -605,6 +649,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
}
}
_alarmInAlarmTags.Remove(alarmKey);
if (!string.IsNullOrEmpty(info.AckedTagReference))
_alarmAckedTags.Remove(info.AckedTagReference);
}
// Delete variable node
@@ -796,6 +842,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
condition.SetSeverity(SystemContext, EventSeverity.Medium);
condition.Retain.Value = false;
condition.OnReportEvent = (context, n, e) => Server.ReportEvent(context, e);
condition.OnAcknowledge = OnAlarmAcknowledge;
if (sourceVariable != null)
{
@@ -806,15 +853,19 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
AddPredefinedNode(SystemContext, condition);
var baseTagRef = alarmAttr.FullTagReference.TrimEnd('[', ']');
_alarmInAlarmTags[inAlarmTagRef] = new AlarmInfo
var alarmInfo = new AlarmInfo
{
SourceTagReference = alarmAttr.FullTagReference,
SourceNodeId = sourceNodeId,
SourceName = alarmAttr.AttributeName,
ConditionNode = condition,
PriorityTagReference = baseTagRef + ".Priority",
DescAttrNameTagReference = baseTagRef + ".DescAttrName"
DescAttrNameTagReference = baseTagRef + ".DescAttrName",
AckedTagReference = baseTagRef + ".Acked",
AckMsgTagReference = baseTagRef + ".AckMsg"
};
_alarmInAlarmTags[inAlarmTagRef] = alarmInfo;
_alarmAckedTags[alarmInfo.AckedTagReference] = alarmInfo;
hasAlarms = true;
}
@@ -1532,6 +1583,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
// Prepare updates outside the Lock. Shared-state lookups stay inside the Lock.
var updates = new List<(string address, BaseDataVariableState variable, DataValue dataValue)>(keys.Count);
var pendingAlarmEvents = new List<(string address, AlarmInfo info, bool active, ushort? severity, string? message)>();
var pendingAckedEvents = new List<(AlarmInfo info, bool acked)>();
foreach (var address in keys)
{
@@ -1539,7 +1591,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
continue;
AlarmInfo? alarmInfo = null;
AlarmInfo? ackedAlarmInfo = null;
bool newInAlarm = false;
bool newAcked = false;
lock (Lock)
{
@@ -1562,6 +1616,14 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
if (newInAlarm == alarmInfo.LastInAlarm)
alarmInfo = null;
}
// Check for Acked transitions
if (_alarmAckedTags.TryGetValue(address, out ackedAlarmInfo))
{
newAcked = vtq.Value is true || vtq.Value is 1 || (vtq.Value is int ackedIntVal && ackedIntVal != 0);
pendingAckedEvents.Add((ackedAlarmInfo, newAcked));
ackedAlarmInfo = null; // handled
}
}
if (alarmInfo == null)
@@ -1601,7 +1663,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
}
// Apply under Lock so ClearChangeMasks propagates to monitored items.
if (updates.Count > 0 || pendingAlarmEvents.Count > 0)
if (updates.Count > 0 || pendingAlarmEvents.Count > 0 || pendingAckedEvents.Count > 0)
{
lock (Lock)
{
@@ -1639,6 +1701,30 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
Log.Warning(ex, "Error reporting alarm event for {Source}", currentInfo.SourceName);
}
}
// Apply Acked state changes
foreach (var (info, acked) in pendingAckedEvents)
{
var condition = info.ConditionNode;
if (condition == null) continue;
try
{
condition.SetAcknowledgedState(SystemContext, acked);
condition.Retain.Value = (condition.ActiveState?.Id?.Value == true) || !acked;
if (_tagToVariableNode.TryGetValue(info.SourceTagReference, out var src) && src.Parent != null)
src.Parent.ReportEvent(SystemContext, condition);
Server.ReportEvent(SystemContext, condition);
Log.Information("Alarm {AckState}: {Source}",
acked ? "ACKNOWLEDGED" : "UNACKNOWLEDGED", info.SourceName);
}
catch (Exception ex)
{
Log.Warning(ex, "Error updating acked state for {Source}", info.SourceName);
}
}
}
}