Files
UA-.NETStandard/Docs/AlarmsAndConditions.md
Marc Schier 01d7162313 Part 9 Alarms & Conditions: complete server + client implementation (#3805)
Complete implementation of **OPC UA Part 9 ΓÇö Alarms & Conditions** for
both server and client, **OPC UA Part 16 ΓÇö State Machines** (generic
extensible client + server API), and the supporting infrastructure
(source-generated event records, source-generated ObjectType proxies,
`IAsyncEnumerable` streaming subscriptions, address-space model change
tracking, per-node `INodeCache` invalidation, unified DI integration).

### Server-side (Part 9 complete)

- **`AlarmConditionState`** ΓÇö every Part 9 method wired automatically
in `OnAfterCreate`: `Silence`, `Suppress`/`Suppress2`,
`Unsuppress`/`Unsuppress2`, `RemoveFromService`/`RemoveFromService2`,
`PlaceInService`/`PlaceInService2`, `Reset`/`Reset2`,
`GetGroupMemberships`. Each method generates the spec-mandated audit
event and supports an optional veto-delegate hook.
- **Latched alarms** ΓÇö `ActiveState` reflects real process state;
`LatchedState` persists until explicit `Reset` (with full precondition
validation).
- **Re-alarming** ΓÇö
`ProcessReAlarm`/`ResetReAlarmRepeatCount`/`IsReAlarmEnabled` helpers
(opt-in, no hidden timer).
- **Audible alarms** ΓÇö `UpdateAudibleState` for
`AudibleEnabled`/`AudibleSound`/`SilenceState` lifecycle.
- **Suppression + groups** ΓÇö `AlarmGroup`, `AlarmSuppressionEngine`
(with first-in-group), `AlarmRateTracker` (for `AlarmMetricsType`).
- **Spec-compliance fixes** ΓÇö `SuppressedOrShelved` now includes
`OutOfServiceState`; `GetRetainState` includes `LatchedState`;
`EffectiveDisplayName` composition includes `OutOfServiceState` +
`LatchedState`.

### Client-side Part 9 API

- **`AlarmClient`** ΓÇö typed Part 9 operations. Each method delegates
to the matching source-generated `*TypeClient` proxy
(`ConditionTypeClient`, `AcknowledgeableConditionTypeClient`,
`AlarmConditionTypeClient`, `DialogConditionTypeClient`,
`ShelvedStateMachineTypeClient`) ΓÇö one source of truth for every
method NodeId.
- **Interface hierarchy** — `IConditionOperations` →
`IAcknowledgeableConditionOperations` → `IAlarmOperations` +
`IDialogConditionOperations`.
- **Source-generated event records** ΓÇö new `EventRecordGenerator`
(alongside `ObjectTypeProxyGenerator`) emits `{Type}TypeRecord` per OPC
UA event type: `ConditionTypeRecord` →
`AcknowledgeableConditionTypeRecord` → `AlarmConditionTypeRecord` →
`LimitAlarmTypeRecord`/`ExclusiveLimitAlarmTypeRecord`/… all the way
to vendor subtypes. **Vendor alarm types get records automatically.**
- **`AlarmEventDecoder`** ΓÇö decodes `EventNotification.Fields` into
the most-specific generated record.
- **`AlarmEventFilterBuilder`** ΓÇö fluent builder for Part 9
`EventFilter`s.
- **`ISession.GetAlarmClient(telemetry)`** convenience extension.

### Part 16 state machines (generic, extensible)

- **Client**: extension methods on the source-generated
`StateMachineTypeClient` / `FiniteStateMachineTypeClient` ΓÇö
`GetCurrentStateAsync`, `ObserveFiniteTransitionsAsync`
(`IAsyncEnumerable<FiniteStateSnapshot>`), `WaitForStateAsync`,
`GetAvailableStatesAsync` / `GetAvailableTransitionsAsync`. **Vendor
proxies inherit the API automatically** through the generated
inheritance chain.
- **Server**: `StateMachineBuilder` (fluent: `AddState` /
`AddTransition` / `OnCause` / validation) +
`FluentFiniteStateMachineState` ΓÇö declare a Part 16 state machine
without subclassing the abstract base.
- **Alarms alignment**: `AlarmClient.GetShelvingStateAsync` /
`ObserveShelvingTransitionsAsync` surface the API for every alarm's
`ShelvingState` child.

### IAsyncEnumerable streaming subscription

- **`IStreamingSubscription`** with
`SubscribeDataChangesAsync`/`SubscribeEventsAsync` returning
`IAsyncEnumerable<T>` (Channel-backed).
- **`ManagedSession.DefaultStreaming`** ΓÇö lazy shared subscription;
disposed with the session.
- **Helpers** ΓÇö `TakeUntilAsync`, `WithTimeoutAsync`, `TakeAsync`,
`BufferedAsync` for state-machine waits and bounded observation windows.
- **`AlarmStreamExtensions`** ΓÇö
`SubscribeAlarmsAsync`/`SubscribeConditionsAsync`/`SubscribeDialogsAsync`
returning the source-generated record types.

### Address-space model change tracking

- **Server**: `RaiseGeneralModelChangeEvent` on both `CustomNodeManager`
and `AsyncCustomNodeManager`; `ModelChangeAggregator` (in
`Opc.Ua.Server.NodeManager`, used by every node manager) batches changes
per service call; **auto-emit** from `CreateNode`/`DeleteNode` (opt-out
via `ModelChangeEmissionEnabled = false`).
- **Client**: `IModelChangeTracker` subscribes to
`BaseModelChangeEventType` via the streaming subscription, decodes
`ModelChangeStructureDataType` payloads, invalidates the `INodeCache`
per-node.
- **`INodeCache.InvalidateNode(NodeId)`** ΓÇö new interface member (true
per-node eviction in `NodeCache`).
- **`ManagedSession.EnableModelChangeTrackingAsync()`** +
`ManagedSessionOptions.ModelChangeTracking` +
`ManagedSessionBuilder.WithModelChangeTracking()` fluent method.

### Dependency Injection integration

- **`services.AddOpcUa().AddClient(...).AddAlarms()`** ΓÇö registers
`AlarmClientFactory` so DI-hosted apps can resolve a Part 9 client per
session without `new`-ing one manually.
- Cross-link added to the unified DI surface (`AddServer` / `AddClient`
/ `AddComplexTypes` / `AddGdsServer` / `AddLdsServer` /
`AddWotConServer`).

### Quickstart server modernization

- `Quickstarts.Servers/Alarms/AlarmNodeManager.cs` ported to
`AsyncCustomNodeManager` and demonstrates the new `AlarmGroup` +
`AlarmSuppressionEngine` end-to-end via a writable `MaintenanceMode`
boolean that suppresses every member alarm.

### Documentation

- New: `Docs/AlarmsAndConditions.md`, `Docs/StreamingSubscription.md`,
`Docs/ModelChangeTracking.md`, `Docs/StateMachines.md` ΓÇö full
developer guides.
- Updated: `Docs/DependencyInjection.md` (`AddAlarms()` row + section),
`Docs/README.md` index, `Docs/MigrationGuide.md` (Part 9 / Part 16 /
model change tracking sections covering behavior changes + breaking API
surface).
- Sample: `Applications/ConsoleReferenceClient/AlarmClientSample.cs`.
2026-05-29 23:24:53 +02:00

24 KiB

Alarms and Conditions (OPC UA Part 9)

This guide describes the OPC UA Part 9 Alarms & Conditions support in this stack: how to build alarm-aware servers, how to consume alarm events on the client, and how the new helpers (latched alarms, alarm groups, suppression engine, alarm metrics, streaming alarm records) fit together.

For the formal model, see OPC UA Part 9 — Alarms and Conditions.

Quick reference

Concern Server entry point Client entry point
Create an alarm new AlarmConditionState(telemetry, parent); alarm.Create(...) n/a
Acknowledge / Confirm AcknowledgeableConditionState.OnAcknowledgeCalled (auto-wired) AlarmClient.AcknowledgeAsync / ConfirmAsync
Silence AlarmConditionState.SetSilenceState AlarmClient.SilenceAsync
Suppress / Unsuppress SetSuppressedState AlarmClient.SuppressAsync / UnsuppressAsync
Out-of-service SetOutOfServiceState AlarmClient.RemoveFromServiceAsync / PlaceInServiceAsync
Latched + Reset SetLatchedState + auto from SetActiveState AlarmClient.ResetAsync
Shelve / Unshelve SetShelvingState AlarmClient.TimedShelveAsync / OneShotShelveAsync / UnshelveAsync
Re-alarm ProcessReAlarm n/a (server timer)
Alarm groups AlarmGroup, AlarmSuppressionEngine AlarmClient.GetGroupMembershipsAsync
Alarm rate AlarmRateTracker read AlarmMetricsType attributes
Refresh state Server.ConditionRefresh (server-driven) AlarmClient.ConditionRefreshAsync / ConditionRefresh2Async
Stream alarm events n/a IStreamingSubscription.SubscribeAlarmsAsync
Decode raw event fields n/a EventRecordDecoderRegistry.Default.Decode
Build an event filter n/a {Type}Record.EventFilters.Build(registry?)

AlarmClient is obtained from any ISession and a telemetry context; internally it delegates every Part 9 method call to the matching source-generated *TypeClient proxy (so there is exactly one place that knows each method NodeId — the generator):

AlarmClient alarms = session.GetAlarmClient(telemetry);

For dependency-injected hosts use the builder.AddAlarms() extension and resolve AlarmClientFactory (which threads the host's telemetry into the client for you):

var factory = sp.GetRequiredService<AlarmClientFactory>();
AlarmClient alarms = factory.Create(session);

If you already have a typed handle to a specific condition node you can also construct a proxy directly — AlarmClient is a convenience façade over these:

await new AlarmConditionTypeClient(session, conditionId, telemetry)
    .AcknowledgeAsync(eventId, comment);

Server side

Creating an alarm

AlarmConditionState is the central server-side state type for all Part 9 alarms. It is source-generated from the standard NodeSet and extended with hand-written behavior. The behavior partial file (Stack/Opc.Ua.Core.Types/State/AlarmConditionState.Methods.cs) wires every Part 9 method handler during OnAfterCreate, so as soon as you populate the optional state nodes the corresponding methods are callable.

var alarm = new AlarmConditionState(telemetry, parent);
alarm.Create(
    context,
    nodeId: NodeId.Null,         // server-assigned
    browseName: new QualifiedName("MyAlarm"),
    displayName: null,
    assignNodeIds: true);

alarm.SetEnableState(context, enabled: true);
alarm.SetSeverity(context, EventSeverity.High);
alarm.SetActiveState(context, active: true);
alarm.ReportEvent(context, alarm);

The optional state nodes are created the way you would expect on a generated AlarmConditionState: assign to alarm.SilenceState, alarm.OutOfServiceState, alarm.LatchedState, etc. before calling Create. The quickstart reference server creates these via AlarmConditionTypeHolder.Initialize — Applications/Quickstarts.Servers/Alarms/AlarmHolders/.

Driving state from your process

All state transitions go through typed setters that:

  • update the TwoStateVariableState value + Id
  • stamp TransitionTime
  • recompute EffectiveDisplayName and the composite SuppressedOrShelved flag
  • clear ChangeMasks so the next publish cycle sees the new state
Setter Notes
SetEnableState(context, enabled) Inherited from ConditionState. Disabling clears Retain per Part 9 §5.5.2.
SetSeverity(context, severity) Records LastSeverity before updating.
SetActiveState(context, active) On true: if LatchedState is present, sets it true; if SilenceState is present and silenced, clears it. On false: if shelved as OneShotShelve, unshelves.
SetSuppressedState(context, suppressed) Updates SuppressedOrShelved taking OutOfServiceState and ShelvingState into account.
SetOutOfServiceState(context, outOfService) Sets SuppressedOrShelved true when out of service (Part 9 §5.8.2).
SetShelvingState(context, shelved, oneShot, shelvingTime) Drives the ShelvedStateMachineState, runs the unshelve timer, computes UnshelveTime.
SetLatchedState(context, latched) Direct latched-state setter; usually called automatically from SetActiveState.
SetSilenceState(context, silenced) Direct silence-state setter; usually called automatically on activation / re-alarm.

Latched alarms

If you populate alarm.LatchedState, the alarm becomes a latching alarm (Part 9 §4.8). The semantics:

  • SetActiveState(context, true) — also sets LatchedState = true.
  • SetActiveState(context, false) — ActiveState reflects the real process state, but LatchedState stays true.
  • Reset (server-side method, auto-wired) — clears LatchedState. The Reset method validates all preconditions before accepting: enabled, not active, acknowledged, confirmed (if ConfirmedState is present). Any other state returns Bad_InvalidState.

Latched alarms are retained (Retain = true) for as long as LatchedState.Id is true, so a client refresh sees them.

Re-alarming

A re-alarm reminder fires when an alarm has been active and unacknowledged longer than ReAlarmTime. The state type provides a helper rather than an automatic timer (so the host owns scheduling and event generation):

// In your re-alarm scheduler (e.g. an external Timer)
if (alarm.IsReAlarmEnabled && alarm.ActiveState.Id.Value
    && alarm.AckedState?.Id.Value != true)
{
    alarm.ProcessReAlarm(context);
}

// On deactivation / acknowledge:
alarm.ResetReAlarmRepeatCount(context);

ProcessReAlarm:

  • clears AckedState (forces re-acknowledgement)
  • clears SilenceState (audible annunciation resumes)
  • increments ReAlarmRepeatCount
  • calls ReportStateChange so the new event is published

Audible alarms and silencing

If AudibleEnabled = true and AudibleSound is populated, the UpdateAudibleState helper takes care of clearing the silence state when the alarm activates (so the next activation is audible again):

ByteString sound = LoadWavFile();
alarm.UpdateAudibleState(context, active: true, soundData: sound);

SilenceAsync from the client (or the Silence method handler on the server) sets SilenceState.Id = true.

Suppression, out-of-service, shelving

All three states contribute to the SuppressedOrShelved boolean. The state-type setters keep that flag in sync — clearing one does not clear SuppressedOrShelved while the others are still active:

alarm.SetSuppressedState(context, true);      // SuppressedOrShelved = true
alarm.SetOutOfServiceState(context, true);    // SuppressedOrShelved stays true
alarm.SetSuppressedState(context, false);     // SuppressedOrShelved stays true
alarm.SetOutOfServiceState(context, false);   // SuppressedOrShelved = false

Alarm groups and first-in-group

Libraries/Opc.Ua.Server/Alarms/AlarmGroup.cs wraps a generated AlarmGroupState and provides typed add/remove/enumerate:

var group = new AlarmGroup(motorAlarmGroupState);
group.AddMember(motorHighTempAlarm);
group.AddMember(motorLowOilAlarm);

foreach (NodeId id in group.GetMemberIds(context))
{
    // ...
}

AlarmSuppressionEngine centralizes both the AlarmSuppressionGroup pattern and the FirstInGroup pattern. Register on startup, call Evaluate from your simulation/process loop, and the engine routes suppression to the right alarm members:

using var engine = new AlarmSuppressionEngine();

engine.RegisterSuppressionGroup(
    suppressionGroup: motorShutdownGroup,
    suppressionSource: () => motorIsShutDown.Value,
    alarmMembers: new[] { motorHighTempAlarm, motorLowOilAlarm });

engine.RegisterFirstInGroupAlarm(
    firstAlarm: masterTripAlarm,
    group: tripGroup,
    otherMembers: dependentTripAlarms);

// In your periodic update:
engine.Evaluate(context);

// On master trip activation:
engine.OnFirstInGroupActiveChanged(context, masterTripAlarm, tripGroup,
    firstActive: true);

The first Evaluate call always applies the current state, so clients see a coherent suppression state immediately after registration — there is no edge required.

Live demo: The Applications/Quickstarts.Servers/Alarms/ AlarmNodeManager.cs reference implementation wires this up end-to-end. It exposes a /Alarms/AnalogGroup (AlarmGroupType) containing every analog-source alarm and a writable /Alarms/MaintenanceMode boolean. Writing true to MaintenanceMode runs AlarmSuppressionEngine.Evaluate(...) and suppresses every group member; writing false clears suppression on the next evaluation. The reference server's node manager itself derives from AsyncCustomNodeManager, so the same file is a worked example of porting a Part 9 demo to the modern async base class.

Alarm metrics (rate tracking)

AlarmRateTracker records activations into a sliding window and exposes CurrentAlarmRate / MaximumAlarmRate suitable for surfacing through an AlarmMetricsType instance:

var tracker = new AlarmRateTracker(TimeSpan.FromMinutes(1));

alarm.OnSilenceRequested = (ctx, a) =>
{
    tracker.RecordActivation();
    return ServiceResult.Good;
};

// Periodically push to AlarmMetrics:
metrics.CurrentAlarmRate.Value = tracker.CurrentAlarmRate;
metrics.MaximumAlarmRate.Value = tracker.MaximumAlarmRate;

Vetoing alarm operations

Each Part 9 alarm method has an optional delegate that runs before the default state transition. Returning a Bad status from the delegate aborts the operation; returning Good (or null) lets the default behavior run:

Delegate Triggered by
alarm.OnSilenceRequested Silence method
alarm.OnSuppressRequested(suppressing: bool) Suppress / Unsuppress
alarm.OnOutOfServiceRequested(outOfService: bool) RemoveFromService / PlaceInService
alarm.OnResetRequested Reset (latched alarms)
alarm.OnShelve OneShotShelve / TimedShelve / Unshelve (existing)
alarm.OnResetRequested = (ctx, a) =>
{
    if (!hardwareDiagnosticPassed)
    {
        return new ServiceResult(StatusCodes.BadUserAccessDenied,
            "Hardware diagnostic must pass before reset.");
    }
    return ServiceResult.Good;
};

Audit events

Every Part 9 alarm method generates the spec-mandated audit event type automatically. You do not have to call ReportEvent for the audit event — it happens inside the method handler when AreEventsMonitored is true:

Method Audit event type
Silence AuditConditionSilenceEventType
Suppress / Unsuppress / *2 AuditConditionSuppressionEventType
RemoveFromService / PlaceInService / *2 AuditConditionOutOfServiceEventType
Reset / Reset2 AuditConditionResetEventType
Existing: Acknowledge / Confirm AuditConditionAcknowledgeEventType / AuditConditionConfirmEventType
Existing: shelving AuditConditionShelvingEventType

Client side

AlarmClient — typed operations

AlarmClient is the strongly-typed client API for Part 9 methods. Each method delegates to the matching source-generated *TypeClient proxy (ConditionTypeClient, AcknowledgeableConditionTypeClient, AlarmConditionTypeClient, DialogConditionTypeClient, ShelvedStateMachineTypeClient), passing the caller-supplied conditionId as the proxy's ObjectId. This honours the Part 9 §5.5.4 idiom (ConditionId is acceptable as ObjectId) and keeps exactly one source of truth — the generated proxy — for every method NodeId and argument shape.

AlarmClient alarms = session.GetAlarmClient(telemetry);

// ConditionType methods
await alarms.EnableAsync(conditionId);
await alarms.DisableAsync(conditionId);
await alarms.AddCommentAsync(conditionId, eventId,
    new LocalizedText("en", "Looks like flow sensor drift"));
await alarms.ConditionRefreshAsync(subscriptionId);
await alarms.ConditionRefresh2Async(subscriptionId, monitoredItemId);

// AcknowledgeableConditionType
await alarms.AcknowledgeAsync(conditionId, eventId,
    new LocalizedText("en", "Operator review complete"));
await alarms.ConfirmAsync(conditionId, eventId,
    new LocalizedText("en", "Maintenance verified"));

// AlarmConditionType
await alarms.SilenceAsync(conditionId);
await alarms.SuppressAsync(conditionId,
    comment: new LocalizedText("en", "Routine maintenance"));
await alarms.UnsuppressAsync(conditionId);
await alarms.RemoveFromServiceAsync(conditionId);
await alarms.PlaceInServiceAsync(conditionId);
await alarms.ResetAsync(conditionId);                            // latched-alarm reset
await alarms.TimedShelveAsync(conditionId, shelvingTime: 30000); // 30s
await alarms.OneShotShelveAsync(conditionId);
await alarms.UnshelveAsync(conditionId);

ArrayOf<NodeId> groups = await alarms.GetGroupMembershipsAsync(conditionId);

The *Async(... comment ...) overloads automatically pick the spec-defined *2 method when a non-empty comment is supplied (Suppress2, Unsuppress2, RemoveFromService2, PlaceInService2, Reset2).

Subscribing to alarms with IAsyncEnumerable

Alarm events flow through the streaming subscription API. The AlarmStreamExtensions.SubscribeAlarmsAsync extension returns strongly-typed records:

ManagedSession session = ...;
IStreamingSubscription streaming = session.DefaultStreaming;

await foreach (ConditionTypeRecord record in streaming
    .SubscribeAlarmsAsync(notifierId: ObjectIds.Server, ct: ct)
    .ConfigureAwait(false))
{
    switch (record)
    {
        case ExclusiveLimitAlarmTypeRecord limit:
            Console.WriteLine($"{limit.SourceName} limit-state-id={limit.LimitState}");
            break;
        case AlarmConditionTypeRecord alarm when alarm.ActiveStateId == true:
            await alarms.AcknowledgeAsync(alarm.ConditionId,
                alarm.EventId,
                new LocalizedText("en", "Auto-ack")).ConfigureAwait(false);
            break;
        case DialogConditionTypeRecord dialog:
            // Pick a response index from dialog.ResponseOptionSet
            await alarms.RespondAsync(dialog.ConditionId,
                selectedResponse: 0).ConfigureAwait(false);
            break;
    }
}

State-machine waits compose naturally with TakeUntilAsync / WithTimeoutAsync:

// Wait for myAlarm to clear, or 5 minutes — whichever comes first.
await streaming.SubscribeAlarmsAsync(ObjectIds.Server)
    .TakeUntilAsync(r =>
        r is AlarmConditionTypeRecord a && a.ConditionId == myAlarmId &&
        a.ActiveStateId == false)
    .WithTimeoutAsync(TimeSpan.FromMinutes(5))
    .LastAsync(ct);

Typed alarm records

Alarm and condition records are source-generated by the Opc.Ua.SourceGeneration analyzer (see the EventRecordGenerator). For every ObjectType whose base-type chain ends at BaseEventType, the generator emits a partial record {Type}Record deriving from the record of its parent type, exposing one init-only property per declared field. The standard NodeSet produces the following hierarchy (abridged — only the most commonly observed types are shown):

EventRecord                              (anchor; hand-written)
└── BaseEventTypeRecord                  (i=2041)
    └── ConditionTypeRecord              (i=2782)
        ├── DialogConditionTypeRecord    (i=2830)
        └── AcknowledgeableConditionTypeRecord
            └── AlarmConditionTypeRecord
                ├── LimitAlarmTypeRecord
                │   ├── ExclusiveLimitAlarmTypeRecord
                │   └── NonExclusiveLimitAlarmTypeRecord
                ├── DiscreteAlarmTypeRecord
                │   └── OffNormalAlarmTypeRecord
                │       └── CertificateExpirationAlarmTypeRecord
                └── DiscrepancyAlarmTypeRecord

Vendor models that derive from any of these types automatically get their own *TypeRecord deriving from the closest standard ancestor — no checked-in code, no manual class definitions. Add a hand-written partial record VibrationAlarmTypeRecord in your project to extend the generated declaration with computed properties or custom helpers.

The decoder upgrades the record type based on which fields are populated in the event. A simple switch on the record type gives you the right field set:

EventRecord? record = EventRecordDecoderRegistry.Default.Decode(eventFields);

if (record is CertificateExpirationAlarmTypeRecord cert)
{
    Console.WriteLine($"Cert {cert.CertificateType} expires {cert.ExpirationDate}");
}

The shared ConditionTypeRecord.ConditionId property is a hand-written alias for SourceNode — Part 9 of the OPC UA specification defines the "ConditionId" as the NodeId of the condition object that fired the event, which is reported through the SourceNode event field.

Source-generated decoders + EventRecordDecoderRegistry

Every record emitted by the EventRecordGenerator also exposes a nested static class Decoder with a positional StandardFields table and a Decode(IReadOnlyList<Variant>) method that populates own + inherited init-only properties. A per-file Register{ModelPrefix}Decoders(this EventRecordDecoderRegistry) extension registers every emitted decoder with a caller-supplied registry.

The process-wide EventRecordDecoderRegistry.Default ships with the standard UA model pre-registered. It routes by the event's EventType field and walks the OPC UA event-type hierarchy through an optional SuperTypeResolver when the exact type is not registered. Vendor models register their generated extensions via Register{Prefix}Decoders or CreateChildScope().Register{Prefix}Decoders() for test isolation.

// EventType-keyed dispatch backed by source-generated decoders:
EventRecord? rec = EventRecordDecoderRegistry.Default.Decode(eventFields);

// Vendor scenario — register extra decoders on an app-scoped child:
var app = EventRecordDecoderRegistry.Default
    .CreateChildScope()
    .RegisterMyVendorDecoders();
EventRecord? vendorRec = app.Decode(eventFields);

Per-record event filters

Every generated {Type}Record exposes a nested static class EventFilters alongside its Decoder block. The Build(registry?) factory produces an EventFilter whose where clause restricts events to OfType({recordTypeId}) and whose select clauses come from the supplied registry's composed StandardFields (defaults to EventRecordDecoderRegistry.Default). The returned filter pairs cleanly with EventRecordDecoderRegistry.Decode — the registry remaps the composed positions to each decoder's own layout before invoking it, so vendor models extend transparently.

// Filter for alarm events:
EventFilter filter = AlarmConditionTypeRecord.EventFilters.Build();

// Or for any condition:
EventFilter f2 = ConditionTypeRecord.EventFilters.Build();

// Or for dialog events:
EventFilter f3 = DialogConditionTypeRecord.EventFilters.Build();

// Or for a specific subtype:
EventFilter f4 = CertificateExpirationAlarmTypeRecord.EventFilters.Build();

// Vendor scenario — pass a child registry so the filter superset
// includes the vendor model's fields:
var app = EventRecordDecoderRegistry.Default
    .CreateChildScope()
    .RegisterMyVendorDecoders();
EventFilter vendorFilter = VibrationAlarmTypeRecord.EventFilters.Build(app);

The same registry should be passed to Subscribe*Async (through the registry: parameter) so the streaming side decodes through the registry that built the filter.

Dialog conditions

A DialogConditionType event arrives as a DialogConditionTypeRecord. The Respond and Respond2 methods on IDialogConditionOperations close out the dialog. The decoded record exposes the prompt and available response option set so the caller can pick an index:

await foreach (DialogConditionTypeRecord dialog in streaming.SubscribeDialogsAsync(notifierId))
{
    Console.WriteLine($"Prompt: {dialog.Prompt}");
    LocalizedText[] options = dialog.ResponseOptionSet ?? Array.Empty<LocalizedText>();
    Console.WriteLine($"Options: {string.Join(", ", options)}");

    // Pick whichever option matches your scenario.
    int selectedIndex = 0;
    await alarms.Respond2Async(dialog.ConditionId, selectedIndex,
        new LocalizedText("en", "Approved by operator-1")).ConfigureAwait(false);
}

The OkResponse / CancelResponse / DefaultResponse properties on the server-side DialogConditionType (Part 9 §5.6.2) carry their canonical indices for clients to read separately via Read service if the application needs them; they are not surfaced in the standard DialogConditionTypeRecord (which only carries the dialog prompt + active state).

ConditionRefresh

Both Part 9 refresh methods are available:

// Refresh all conditions for the subscription
await alarms.ConditionRefreshAsync(subscriptionId);

// Refresh just one monitored item's conditions (Part 9 §5.5.8)
await alarms.ConditionRefresh2Async(subscriptionId, monitoredItemId);

The classic ISubscription.ConditionRefreshAsync on the streaming subscription is also available — they are equivalent when the AlarmClient and the IStreamingSubscription are bound to the same session.

Reference

  • OPC UA Part 9 — Alarms and Conditions
  • IEC 62682 — Management of alarm systems for the process industries
  • ISA 18.2 — Management of Alarm Systems for the Process Industries
  • Streaming Subscriptions — IStreamingSubscription
  • State Machines — generic Part 16 state-machine API used by AlarmClient.GetShelvingStateAsync / ObserveShelvingTransitionsAsync
  • Model Change Tracking — client cache invalidation on address-space changes
  • Source: Libraries/Opc.Ua.Server/Alarms/, Libraries/Opc.Ua.Client/Alarms/, Stack/Opc.Ua.Core.Types/State/AlarmConditionState.Methods.cs
  • Reference client sample: Applications/ConsoleReferenceClient/AlarmClientSample.cs
  • Conformance tests: Tests/Opc.Ua.History.Tests/AlarmsAndConditions*.cs