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.
5.0 KiB
paths
| paths | |||||
|---|---|---|---|---|---|
|
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. RootCLAUDE.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.