feat(heidenhain): Part 9 condition class from alarm class/group, ALARM: selectors in the browse picker, unclaimed alarms counted + logged (#839)

- Core: optional AlarmEventArgs.ConditionClass (AlarmConditionClass Base/Process/Maintenance/System/Safety)
  carried by NativeAlarmProjector on AlarmConditionSnapshot and written to ConditionClassId/Name by
  OtOpcUaNodeManager (kept across class-less events, carried across a #641 rebuild).
- Heidenhain: HeidenhainAlarmClasses — HEIDENHAIN DNC-interface class/group names (corroborated by the
  captured iTNC 530 record); class 5 => Safety, group 1/2 => Process, 4/5 => System, else Base.
- DiscoverAsync offers AlarmConditions/ selector leaves (IsAlarm Boolean); browse-commit seeds the alarm object.
- Alarms no selector claims: otopcua.driver.alarms.unmatched counter, /hosts diagnostics, rate-limited Warning.

Refs #839
This commit is contained in:
Joseph Doherty
2026-11-11 03:41:14 -05:00
parent 481985e20b
commit ddc1f29e3a
17 changed files with 593 additions and 39 deletions
+14 -7
View File
@@ -163,8 +163,8 @@ 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) |
| `ConditionClassId` | **`BaseConditionClassType`** at materialize; a classifying native driver may set `Process`/`Maintenance`/`System`/`SafetyConditionClassType` at runtime (#839 — today only Heidenhain) | Part 9's "no condition class modelled" value. Unset shipped `NodeId.Null` (#475) |
| `ConditionClassName` | **`"BaseConditionClass"`**, or the matching `"<X>ConditionClass"` | Matches `ConditionClassId`. Unset shipped empty text (#475) |
| `Quality` | the condition's **source-data quality** — native tracks the source's connectivity (`Good` / `Bad`); scripted takes the worst of its input tags' qualities (#478) | A pure annotation; never alters Active/Acked/Retain. Unset shipped the accidentally-Good default (#477) — see below |
**Why `BaseConditionClassType` and not `ProcessConditionClassType`.** We hold no per-alarm
@@ -173,11 +173,18 @@ classification at the materialize seam, and `ConditionClassId` is a wire contrac
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.
Safety or Diagnostics, trading a detectable null for an undetectable lie.
**Per-alarm classification arrives at runtime (#839).** A native driver whose upstream codes say where
an alarm comes from sets the optional `AlarmEventArgs.ConditionClass` (`AlarmConditionClass` —
Base/Process/Maintenance/System/Safety) on its Raise/Retrigger; `NativeAlarmProjector` carries it on the
`AlarmConditionSnapshot` (kept across events that state none — quality-only annotations, Clears) and
`WriteAlarmCondition` writes `ConditionClassId`/`ConditionClassName` over the materialize default. It is
not in the event delta-gate (a class rides along with the transition that set it) and the #641 rebuild
carry-over keeps it, normal conditions included. Only Heidenhain classifies today
(`docs/drivers/Heidenhain.md` §Alarm conditions); every other source leaves `BaseConditionClassType`.
Galaxy's free-text `AlarmCategory` is still not mapped — the `IAlarmSource` doc comment's
"maps to `ConditionClassName` downstream" remains an intent for that field.
**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