# OtOpcUa v3 native-alarm B/C live-gate (Gitea #14) — PASS **Date:** 2026-07-23 · **Scope:** items **B** (native-alarm dedup) + **C** (UNS-bound alarm routing) of #14, against a UNS structure + scripted Part-9 alarm authored on the live `otopcua-dev` v3 cluster (see `2026-07-23-otopcua-v3-raw-path-live-gate.md` for the connection + `SiteAOnly` substrate). Instrument: ScadaBridge itself (its DCL A&C client is the thing under test). ## Headline **Both B and C PASS, and item C's original premise turned out to be already-solved code.** No ScadaBridge code change is needed for B/C. The scope-doc worry — "a UNS-bound source won't `StartsWith`-match a condition whose `SourceName` is the RawPath/ScriptedAlarmId" — is **obsolete**: ScadaBridge routes native alarms by the **subscribed node reference**, not the event `SourceName` (deliberate fix, Gitea #17). The only remaining #14 work is the item-A data re-author (greenfield, operational). ## Substrate (on OtOpcUa, SITE-A cluster) - UNS: `area-1/line-1/pump01` → signal `SiteAOnly` (equipment folder node `ns=3;s=EQ-54f7711e160d`, which has **`EventNotifier=1`**; signal `ns=3;s=area-1/line-1/pump01/SiteAOnly` mirrors the raw value). - Scripted Part-9 condition `ns=3;s=SA-siteahigh` (a `HasComponent` child of the equipment folder), `AlarmConditionType`, severity 700, predicate driven by `SiteAOnly` (set always-active for the gate). - `Server` (i=2253) `EventNotifier=1` (the connection-wide aggregate). ## What ScadaBridge did (bindings on template 2148, instance 98, connection `OtOpcUa-v3-raw`) Two `TemplateNativeAlarmSource` bindings, deliberately overlapping to stress dedup: - `ServerAlarms` → `--source-ref i=2253` (Server aggregate) - `UnsEquipAlarms` → `--source-ref nsu=https://zb.com/otopcua/uns;s=EQ-54f7711e160d` (UNS equipment node) ## Findings / checks | # | Check | Result | |---|-------|--------| | C1 | A **UNS-node-scoped** binding receives + routes the native alarm | **PASS** — `UnsEquipAlarms` mirrored `SA-siteahigh.SiteA High` active=True, sev 700, `kind=NativeOpcUa` | | C2 | The condition's `SourceName` = **ScriptedAlarmId** (`SA-siteahigh`), NOT RawPath | **Confirmed** (captured live) — and it does **not** break routing (routing keys off the subscribed node, Gitea #17), only feeds the alarm's identity (`alarmName = SA-siteahigh.SiteA High`) | | C3 | Item C's original "SourceName mismatch drops UNS-bound transitions" premise | **Obsolete** — `OpcUaAlarmMapper.BuildIdentity` stamps `SourceObjectReference` = the subscription ref; `DataConnectionActor` routes on that, not `SourceName` | | B1 | Fanned condition (Server aggregate + equipment-folder notifier) mirrored **once**, not duplicated | **PASS** — active condition appears exactly once (on the more-specific equipment feed); the Server binding stays an inactive placeholder. Stable across repeated snapshots | | B2 | Mechanism | ScadaBridge opens **one** OPC UA `Subscription` per connection with one `MonitoredItem` per source-ref; `ConditionRefresh` delivers each retained condition **once per subscription**, to the most-specific monitored notifier (the equipment folder) — so overlapping bindings do not double-mirror the fan-out | | B3 | Residual double-mirror risk | Latent, **not triggered** here: it needs two bindings whose `SourceReference` strings are literal prefixes of one another AND a delivery matching both (`DataConnectionActor` has no cross-binding, per-ConditionId dedup — `InstanceActor._latestAlarmEvents` is last-writer-wins by `alarmName`). Disjoint node refs (as here) don't collide | ## Captured real payload (what §5 phase 3 wanted) `kind=NativeOpcUa`, `alarmName='SA-siteahigh.SiteA High'` (`SourceName`=`SA-siteahigh` = ScriptedAlarmId, `.` + `ConditionName`=`SiteA High`), `alarmTypeName='AlarmConditionType'`, `severity=700`, `condition.active=True`. A&C state is delivered via events/`ConditionRefresh` (ScadaBridge triggers a refresh on every subscribe), not a plain attribute read. ## Bottom line for #14 - **A** — raw/UNS binding re-author: validated end-to-end (raw-path gate proved `nsu=` bind→read; this gate proved UNS bind→alarm). Remaining work is data re-authoring (greenfield), **no code**. - **B** — dedup: **PASS**, no double-mirror across the v3 fan-out (one subscription + `ConditionRefresh`). - **C** — UNS-bound routing: **PASS**; the original concern was already solved by Gitea #17. - **D/E/F** — shipped in phase 2 (`2026-07-23-otopcua-v3-nsu-hardening-and-browse-ux.md`). **#14 needs no further ScadaBridge code for B/C.** The only shipped code from this whole effort was the phase-2 `nsu=` hardening + the `RealOpcUaClient` host-rewrite fix (surfaced by the raw gate). #14 can close once item A's re-author is done in the target environment(s). ## Rig artifacts (removable on next `docker/deploy.sh`) OtOpcUa ConfigDb: `UnsArea SITEA-area1` / `UnsLine SITEA-line1` / `Equipment EQ-54f7711e160d` / `UnsTagReference UTR-siteapump01-siteaonly` / `Script SC-siteahigh` / `ScriptedAlarm SA-siteahigh`. ScadaBridge: template 2148 native-alarm-sources `ServerAlarms` + `UnsEquipAlarms`; instance 98; connection 3044.