bug(alarms): native alarm conditions emit null SourceNode/SourceName/EventType #473
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
MaterialiseAlarmConditionnever assignsSourceNode,SourceName, orEventTypeon native alarm conditions, so all three arrive null over the wire on every condition event — native and scripted. These are mandatoryBaseEventTypefields that essentially every OPC UA client selects and keys on, so a conforming client cannot identify the source of an alarm.Found while planning ScadaBridge's v3.0 cutover (ScadaBridge issue #14): ScadaBridge's Data Connection Layer selects exactly
[EventType, SourceNode, SourceName, Time, Message, Severity]and derives its alarm identity fromSourceName, falling back toSourceNode. Against v3 both are null, so its key degrades to".HR200"and no alarm can be routed. ScadaBridge has its own fix to make (it should key onConditionId), but the null fields are a server-side defect independent of any one client.Detail
Construction site:
src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs:781-809(MaterialiseAlarmCondition), called for raw tags fromAddressSpaceApplier.cs:702.The full field initialisation is:
There is no
alarm.SourceNode.Value = ..., noalarm.SourceName.Value = ..., noalarm.EventType.Value = ....The SDK does not fill them either.
NodeState.Create→Initialize(context)→ the generatedInitialize(context, InitializationString); theConditionTypeInstanceinit string declaresSourceNode/SourceName/EventTypeas mandatory children with no default value, so they staynull. The auto-filling overloadBaseEventState.Initialize(context, source, severity, message)— which would setSourceNode = source.NodeIdandSourceName = source.BrowseName.Name— is never called on this path (it is only used for transient events).NodeState.ReportEventandInstanceStateSnapshot.CreateChildNodedo no field synthesis; the snapshot copiesPropertyState.Valueverbatim.The SDK's own
AlarmConditionServersample doesalarm.EventType.Value = alarm.TypeDefinitionId;— this code does not.Worked example
Raw tag
pymodbus/plc/HR200,TagConfig {"alarm":{"alarmType":"OffNormalAlarm","severity":700}}, driver raises withAlarmSeverity.High, message"HR200 over limit". Client subscribes for events at the Server object (ns=0;i=2253) with a standard select clause:SourceTimestampUtcLocalizedText("HR200 over limit")"HR200"(leaf name, not the RawPath)ns=2;s=pymodbus/plc/HR200— correctConditionIdis correct because the server resolves it from the condition node's own NodeId rather than from a populated field.TypeDefinitionIdis likewise set, so aWhereClauseOfTypefilter works — but a client reading theEventTypefield gets null.Impact
SourceName/SourceNode(the common pattern) cannot attribute an alarm to its source.EventTypefield for type resolution / client-side filtering get null; onlyOfTypewhere-clauses or aHasTypeDefinitionbrowse work.ConditionNameis the leaf tag name, so it is ambiguous across devices (HR200on two PLCs collides) and is not a usable identity on its own.Suggested fix
In
MaterialiseAlarmCondition, right afteralarm.Create(...), follow the standard SDK pattern:Open question worth deciding deliberately: should
SourceNamebe the leafName("HR200") or the full RawPath ("pymodbus/plc/HR200")? The leaf name matches the usual OPC UA convention andConditionName, but is ambiguous across devices; the RawPath is unique and matchesConditionId. Clients should key onConditionIdregardless, so this is mainly a human-readability call — but it is worth being explicit since clients will read it.Why this survived
No test asserts these field values.
tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/NativeAlarmMultiNotifierEventDeliveryTests.csis the only wire-level event test, and its filter (:173-179) selects onlyEventIdandMessage— it counts delivery copies, it does not validate fields.NodeManagerHistoryReadEventsTests.cs:159-169assertsEventType => Variant.Null, but that is the HistoryRead projection (OtOpcUaNodeManager.cs:2658-2673), which nulls those fields by design — a separate path from live condition events.A fix should come with a wire-level test asserting
SourceNode/SourceName/EventTypeare populated on a live condition event.References
RealOpcUaClient.cs:464(select clause),:749-751(identity derivation).docs/AlarmTracking.md:47— ack/confirm/shelve already key on ConditionId = RawPath, consistent with the suggested client guidance.