fix(alarms): populate ConditionClassId/ConditionClassName on conditions (#475)
MaterialiseAlarmCondition never assigned the mandatory Part 9 ConditionType classification fields, so every condition event — native and scripted — shipped ConditionClassId = NodeId.Null (i=0) and ConditionClassName = empty text. Same mechanism as #473: Create() builds the mandatory children from the type's embedded definition but leaves them unset, and nothing downstream synthesises them (ReportEvent / InstanceStateSnapshot copy children verbatim). An HMI bucketing alarms by condition class dropped every OtOpcUa alarm as unclassified. Report BaseConditionClassType — Part 9's "no condition class modelled" value. This is the honest report: we hold no classification at the materialise seam. Deliberately NOT ProcessConditionClassType (the SDK sample's pick), which would assert a classification we cannot back and would be actively wrong for a Galaxy alarm whose upstream category is Safety/Diagnostics — trading a detectable null for an undetectable lie. Real per-alarm classification needs the driver's AlarmCategory carried to this deploy-time seam (it lives only on the runtime AlarmEventArgs transition today) and is a separate feature. Guards, both observed RED against the pre-fix server: - NativeAlarmEventIdentityFieldDeliveryTests: wire-level, its own select clause (the #473 test's clause mirrors ScadaBridge's exactly and its indices are load-bearing, so it is left untouched). The class fields are declared on ConditionType, not BaseEventType, so they are selected against that type. - NodeManagerAlarmSourceFieldsTests: node-level, native (Raw) + scripted (Uns). Stacked on #473 (PR #474) — merge after it.
This commit is contained in:
@@ -815,6 +815,18 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
alarm.SourceNode.Value = alarm.NodeId; // Create() assigned this above; do not rebuild it
|
||||
alarm.SourceName.Value = alarmNodeId;
|
||||
|
||||
// #475 — the mandatory ConditionType classification fields, unset by Create() for the same reason as
|
||||
// the fields above (mandatory, no default, nothing downstream synthesises them) ⇒ NodeId.Null + empty
|
||||
// text on the wire, which buckets every alarm as unclassified in a Part 9 HMI.
|
||||
// BaseConditionClassType is Part 9's "no class modelled" value and is the honest report: we hold no
|
||||
// classification at this seam. Deliberately NOT ProcessConditionClassType (the SDK sample's pick) — it
|
||||
// would assert a classification we cannot back, and would be actively wrong for a Galaxy alarm whose
|
||||
// upstream category is Safety/Diagnostics. Real per-alarm classification needs the driver's
|
||||
// AlarmCategory, which today exists only on the runtime AlarmEventArgs transition and not on the
|
||||
// authored composition this deploy-time seam sees — a separate feature, not a default picked here.
|
||||
if (alarm.ConditionClassId is not null) alarm.ConditionClassId.Value = ObjectTypeIds.BaseConditionClassType;
|
||||
if (alarm.ConditionClassName is not null) alarm.ConditionClassName.Value = new LocalizedText("BaseConditionClass");
|
||||
|
||||
// Initial state via the SDK setters (T14: basic state only, NO event firing).
|
||||
alarm.SetEnableState(SystemContext, true);
|
||||
alarm.SetActiveState(SystemContext, false);
|
||||
|
||||
Reference in New Issue
Block a user