Files
ScadaBridge/docs/plans/2026-07-23-otopcua-v3-alarm-bc-live-gate.md
T

5.1 KiB

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 PASSUnsEquipAlarms 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 ObsoleteOpcUaAlarmMapper.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.