Files
lmxopcua/.claude/rules/alarms.md
T
Joseph Doherty 8c6067b62e docs: move subsystem gotchas out of CLAUDE.md into path-scoped .claude/rules
Keeps the root CLAUDE.md lean by lazy-loading per-area detail (drivers, topology,
data-plane auth, alarms, historian) only when matching files are touched.
2026-10-15 06:14:58 -04:00

5.0 KiB

paths
paths
src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/**
src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ScriptedAlarms/**
src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/**
docs/ScriptedAlarms.md
docs/AlarmTracking.md

Scripted alarm ack/shelve and Part 9 condition method behaviour

Moved verbatim from the root CLAUDE.md (2026-10-13) so it loads only when working in this area. Root CLAUDE.md § "Subsystem guides" indexes it. Edit it here, not in the root file.

Scripted Alarm Ack/Shelve

Inbound operator acknowledge/shelve for scripted alarms is fully implemented. Two surfaces converge on the alarm-commands DPS topic: (1) OPC UA Part 9 condition methods (Acknowledge / Confirm / AddComment / OneShotShelve / TimedShelve / Unshelve) wired in OtOpcUaNodeManager, gated on the AlarmAck role — conferred by the session's API-key principal's NodeAcl grants (or the anonymous principal's), carried by RoleCarryingUserIdentity; LDAP is AdminUI-only; (2) AdminUI /alerts per-row buttons routed through the AdminOperationsActor singleton (gated by DriverOperator policy). ScriptedAlarmHostActor dispatches from the topic to the engine. Client.CLI supports ack, confirm, shelve commands. See docs/ScriptedAlarms.md §"Inbound operator ack/shelve" and docs/AlarmTracking.md.

⚠️ One condition event per accepted method call is OURS, not the SDK's (Gitea #587). The SDK's own report is gated on AutoReportStateChanges (left false), so before #587 a client Acknowledge emitted no condition event — the old "SDK auto-fires its own event (E2)" comments were wrong. WireMethodTransitionReporting wraps every method's SDK handler (Ack/Confirm/AddComment/Enable/Disable/OneShotShelve/TimedShelve/Unshelve) and, on Good, reports once via ReportConditionEvent — under Lock, so the engine's/driver's re-projection (routed by a fire-and-forget Tell before the SDK applies state) waits and then hits the delta-gate with no delta. Things to keep: the SDK's shelve handlers never apply shelving, so the wrapper does; only a real before/after change is reported (a re-ack is silent and restores the Confirmed/Retain the SDK flips); MaxTimeShelved is set to int.MaxValue ms at materialise because Create() left it 0 and every TimedShelve failed. ⚠️ Native alarms support Acknowledge and AddComment only — Confirm, the shelve methods and Enable/Disable return BadNotSupported (no upstream path, no pair replication, and NativeAlarmProjector's Confirmed/Unshelved constants own those fields); a native ack re-asserts ConfirmedState = true because the SDK's ack unconfirms any confirm-capable condition and ScadaBridge maps that field. Pinned over the wire by ConditionMethodEventTests (exactly-once after a sentinel, folder + i=2253).

Behaviours that were wrong or missing and are now fixed — keep them: the SDK's OnTimedUnshelve is a no-op (the engine's AutoUnshelve owns timed-shelve expiry, and UnshelveTime reports the real remaining time from AlarmConditionSnapshot.UnshelveAtUtc); a commented re-ack is stored as an AddComment (durable, replicated) and reports one event, an uncommented re-ack stays silent; a native ack is refused BadInvalidState off-Primary via NativeAlarmAckPrimaryGate (fed by IDriverPrimaryRoleSource; DriverHostActor publishes its Primary verdict at host start and on member changes, so the boot window before the first redundancy snapshot is closed — the same fix covers the MQTT Sparkplug primary-host claim). A native re-ack is still re-sent to the driver (the retry path, by design).

Condition Severity is carried verbatim, clamped to 1..1000 — ToPart9Severity; the node manager used to re-bucket onto the five EventSeverity points (so an MTConnect Fault read 900, scripted Critical 1000 read 900, native Low 200 read 300). ScadaBridge only clamps, so exact values pass through. ⚠️ Never select a two-state id with AddSelectClause(type, "X/Id") — the string becomes ONE QualifiedName named X/Id, which no event carries; the Client.CLI/Client.UI filter did this since a2883b82 and every alarm read inactive+unacked. Use new SimpleAttributeOperand(type, [X, Id]) (ScadaBridge already does).

Condition children are instance-scoped (#629). Every child of a materialised condition carries {ConditionId}/{browse path} in the condition's namespace (e.g. …/temp_hi/ShelvingState/UnshelveTime), assigned by the OtOpcUaNodeManager.New override — the SDK default kept each child on the TYPE's ns=0 id, so a direct Read hit the type (or BadNodeIdUnknown), translate failed, and children sat outside the ACL (the slash join now puts them under their condition's scope). ⚠️ The shelve methods resolve by the AlarmConditionType_ShelvingState_* ids only because New adopts those (a fixed set) as their MethodDeclarationId; calls by the ShelvedStateMachineType_* id are no longer supported (pinned by a test). Condition teardown must forget the whole PredefinedNodes subtree (ForgetPredefinedSubtree) or a removed condition's children keep answering.