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:
+22
-5
@@ -57,7 +57,8 @@ Every condition event — native and scripted — carries the mandatory `BaseEve
|
||||
fields, assigned at materialize time in `OtOpcUaNodeManager.MaterialiseAlarmCondition`. The SDK
|
||||
does **not** synthesize them on this path (`Create` builds the children from the type definition
|
||||
but leaves them unset; `ReportEvent` / `InstanceStateSnapshot` copy children verbatim), so they
|
||||
are set explicitly. Leaving them unset shipped them as **null** on every event — see issue #473.
|
||||
are set explicitly. Leaving them unset shipped them as **null** on every event — see issues #473
|
||||
(the `BaseEventType` trio) and #475 (the `ConditionType` classification pair).
|
||||
|
||||
| Field | Value | Notes |
|
||||
|---|---|---|
|
||||
@@ -65,6 +66,20 @@ are set explicitly. Leaving them unset shipped them as **null** on every event
|
||||
| `SourceNode` | the condition's **own NodeId** — equal to `ConditionId` | The condition **is** the source: an alarm-bearing raw tag materializes only the condition, with no sibling value variable, so there is no other node to point at |
|
||||
| `SourceName` | the same identifying id string: **RawPath** (native) / **ScriptedAlarmId** (scripted) | Deliberately the *unique* id, **not** the leaf name |
|
||||
| `ConditionName` | the leaf / display name (e.g. `HR200`) | Where the short human-readable name lives |
|
||||
| `ConditionClassId` | always **`BaseConditionClassType`** | Part 9's "no condition class modelled" value. Unset shipped `NodeId.Null` (#475) |
|
||||
| `ConditionClassName` | always **`"BaseConditionClass"`** | Matches `ConditionClassId`. Unset shipped empty text (#475) |
|
||||
|
||||
**Why `BaseConditionClassType` and not `ProcessConditionClassType`.** We hold no per-alarm
|
||||
classification at the materialize seam, and `ConditionClassId` is a wire contract clients bucket on.
|
||||
`BaseConditionClassType` is the honest, spec-conformant report of *"this server does not model
|
||||
condition classes"* — it fixes the real defect (a null that breaks conformant clients) without
|
||||
asserting a classification we cannot back. `ProcessConditionClassType` — the SDK sample's pick —
|
||||
was rejected deliberately: it would be *actively wrong* for a Galaxy alarm whose upstream category is
|
||||
Safety or Diagnostics, trading a detectable null for an undetectable lie. Real per-alarm
|
||||
classification is a separate future feature: it needs the driver's alarm category, which today lives
|
||||
only on the runtime `AlarmEventArgs` transition, carried to the deploy-time authored composition that
|
||||
`MaterialiseAlarmCondition` sees. Until then the `IAlarmSource` doc comment claiming the category
|
||||
"maps to `ConditionClassName` downstream" describes an intent, not the implementation.
|
||||
|
||||
**Why `SourceName` is the id, not the leaf name.** The leaf is ambiguous across devices (`HR200` on
|
||||
two PLCs collides) and is already carried by `ConditionName`, so the leaf-name option would add no
|
||||
@@ -79,10 +94,12 @@ identifier and is unique, so it is safe to key on *by itself* — but do **not**
|
||||
`ConditionName` (`$"{SourceName}.{ConditionName}"`), because `SourceName` already ends in the
|
||||
condition's leaf name and the result stutters (`pymodbus/plc/HR200.HR200`).
|
||||
|
||||
Wire-level guard: `NativeAlarmEventIdentityFieldDeliveryTests` asserts the three fields arrive
|
||||
populated on a real subscription using the standard `[EventType, SourceNode, SourceName, Time,
|
||||
Message, Severity]` select clause; `NodeManagerAlarmSourceFieldsTests` guards the node itself
|
||||
across both realms.
|
||||
Wire-level guard: `NativeAlarmEventIdentityFieldDeliveryTests` asserts the three `BaseEventType`
|
||||
fields arrive populated on a real subscription using the standard `[EventType, SourceNode,
|
||||
SourceName, Time, Message, Severity]` select clause, and — in a second test with its own clause —
|
||||
that `ConditionClassId` / `ConditionClassName` do too. The two class fields are declared on
|
||||
`ConditionType`, **not** `BaseEventType`, so a client must select them against that type.
|
||||
`NodeManagerAlarmSourceFieldsTests` guards the node itself across both realms.
|
||||
|
||||
> **Do not correlate live events to HistoryRead on `SourceName` — the two paths disagree.**
|
||||
> The HistoryRead *events* projection (`OtOpcUaNodeManager.ProjectEventField`) returns
|
||||
|
||||
Reference in New Issue
Block a user