Files
UA-.NETStandard/docs/AlarmsAndConditions.md
Marc Schier c4e0721a29 Add a documentation learning path and fix issues found while verifying it (#4555)
## Summary

### Documentation
- Stage the documentation index as a learning path: a first application,
independent client, server, and PubSub paths, then data and services,
companion models, production, advanced integration, and draft or
experimental capabilities.
- Add Getting started (repository samples or NuGet packages), OPC UA
concepts, Your first information model, and a production readiness
checklist.
- Add Native AOT application publishing, a first PubSub publisher and
subscriber exchange, and the UDP transport options.
- Add or expand contents navigation throughout `docs/`, and keep
contributor documentation separate.
- Correct outdated SDK examples and capability guidance, consolidate
repeated explanations into canonical guides, and repair local links and
heading structure.
- Rewrite What's New in 2.0 as a concise feature overview with short
linked details; keep migration instructions in the Migration Guide.
- Expand the feature catalogue with specification IDs, GDS, LDS, the OPC
10000-21 onboarding registrar, ISA-95, xRegistry, OpenUSD, and
sample-local models; distinguish draft status, explain crypto
pluggability, and bring performance guidance forward.
- Address review feedback on wording, active monitored items during
shadow reload, and the `Samples.md` filename and references.
- Regenerate the `opcua-v20-migration` plugin's bundled copy of the
migration guides so it matches the edited `docs/migrate/2.0.x` guides.
- Merge the latest `master`, preserving upstream resource-isolation
guidance within the task-based documentation index.
- Retain deferred Lingity findings where appropriate; this PR does not
claim that every document meets the Lingity score thresholds.

### Fixes found while verifying the guides
Each fix has regression tests that fail without it.
- **Client:** V2 subscriptions compared their lifetime with the
operation timeout instead of the session timeout, and logged a spurious
warning. `ISubscriptionEngineContext`, which is new in 2.0, gains a
`SessionTimeout` property; implementations must add it.
- **PubSub:** a subscriber now registers the `DataSetMetaData`
configured on its readers (OPC 10000-14 6.2.9.4), and the UADP decoder
decodes DataSetMessages that carry no ConfigurationVersion. Before, a
secured subscriber of the reference sample received no DataSets.
- **Source generator:** a ModelDesign method that declares its arguments
inline, without a method type, produced code that did not compile; it
now uses `MethodState`. The generated code of existing models is
unchanged.
- **MinimalClient sample:** waits for its first data-change notification
before it disconnects.

### Known issues, not addressed here
- UDP multicast: the publisher does not send on the interface that the
subscriber joins, and `MulticastLoopback` defaults to false. The PubSub
guide uses unicast for the exchange on one computer.
- The UDP metadata announcement is sent unsecured on secured writer
groups; a secured subscriber drops it with a warning.
- A server does not start in globalization-invariant mode, because
`ResourceManager` creates `CultureInfo("en-US")`. The Native AOT guide
cautions against the mode.

## Validation
- `git diff --check` and
`./.azurepipelines/validate-migration-plugin.ps1` (15 bundled migration
docs validated, plugin manifests synchronized).
- Local Markdown links, anchors, file-link casing, and contents lists
checked across all 87 documents under `docs/`. The index links every
top-level guide and every target of the previous index.
- Both Getting started routes, Your first information model, the PubSub
exchange, and the Native AOT publish of the Getting started client and
server were run as documented. The NuGet route and the tutorial use the
public `2.0.0-preview.5` packages.
- The changed libraries build for all target frameworks in Release
without warnings.
- Tests on .NET 10: Opc.Ua.SourceGeneration.Tests 199 passed;
Opc.Ua.SourceGeneration.Core.Tests 3,977 passed, 8 skipped;
Opc.Ua.Server.Tests `Fluent` category 621 passed; Opc.Ua.Client.Tests
subscription-engine tests 70 passed; Opc.Ua.PubSub.Tests 1,374 passed, 2
skipped; PubSub fuzz replay 145 passed. The new and affected tests also
pass on .NET Framework 4.8.
- Earlier merge validation: 174 resource-isolation tests passed on .NET
10.
- The full solution suite was not run locally.

Review and validation artifacts are maintained outside the repository.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-09-28 19:19:39 +00:00

26 KiB

Alarms and Conditions (OPC UA Part 9)

This guide covers the stack's OPC UA Part 9 Alarms & Conditions support. It explains how to build alarm-aware servers and consume alarm events on clients. It also introduces the helpers for latched alarms, alarm groups, suppression, alarm metrics, and streaming alarm records.

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)
Filtered retain ConditionState.SupportsFilteredRetain n/a (per-subscription)
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?)

Create AlarmClient from any ISession and a telemetry context. It delegates each Part 9 method call to the matching source-generated *TypeClient proxy. The generated proxy is the single source for each method NodeId:

AlarmClient alarms = session.GetAlarmClient(telemetry);

Dependency-injected hosts can call the builder.AddAlarms() extension and resolve AlarmClientFactory. The factory passes the host's telemetry context to the client:

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

Fluent-created alarms have distinct per-instance NodeIds for their entire subtree, including condition Methods and state properties. Configuring WithLimits materializes the standard typed limit properties, assigns instance identifiers, and registers them for browse/read access. Repeated limit configuration updates the same property in place; omitted limits remain absent.

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 (src/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);

Before calling Create, assign any optional state nodes to properties such as alarm.SilenceState, alarm.OutOfServiceState, and alarm.LatchedState. The quickstart reference server creates these nodes through AlarmConditionTypeHolder.Initialize in samples/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. Reset accepts the request only when the alarm is enabled, inactive, and acknowledged, and when it is confirmed if ConfirmedState is present. Otherwise, it returns Bad_InvalidState.

The server retains latched alarms (Retain = true) while LatchedState.Id is true, so clients can see them during a refresh.

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 performs these actions:

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

Audible alarms and silencing

When AudibleEnabled = true and AudibleSound has a value, UpdateAudibleState clears the silence state on activation. The next activation is then audible:

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 the flag in sync. Clearing one state does not clear SuppressedOrShelved while another state remains 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

Filtered retain

Part 9 B.1.4 lets a server set a client-specific Retain flag based on that client's event filter. When a condition no longer matches a client's where clause, the server sends that client a final event with Retain = false. The alarm remains active for everyone else.

Opt a condition in by setting SupportsFilteredRetain:

alarm.SupportsFilteredRetain =
    PropertyState<bool>.With<VariantBuilder>(alarm, value: true);

Notes on the shape of that property:

  • Part 9 provides SupportsFilteredRetain on the ConditionType only — the standard nodeset declares no modelling rule for it, so condition instances do not carry it as a child. The C# property is therefore the server side switch, not an address space node: FindChild(BrowseNames.SupportsFilteredRetain) on an instance returns null by design. Clients read the flag from the type node, which the generated address space already builds.
  • Branches inherit the parent's setting — CreateBranch copies it, since nothing that walks the instance children would.
  • Each condition and branch is tracked separately, so one branch leaving filter scope does not affect its siblings or its parent.
  • ConditionRefresh reevaluates refreshed conditions against the where clause and primes the tracking again.
  • Changing a monitored item's where clause with ModifyMonitoredItems discards tracking for the previous filter. Durable subscriptions preserve tracking across a restart.
  • Each client receives a trailing event with Retain = false (Part 9, 5.5.2, Figure 11), regardless of the server's Retain value. Otherwise, the client would keep an alarm that it should drop. The override applies only to that client's event fields; it leaves the shared event snapshot unchanged. Other clients whose filters still include the condition keep receiving the server's actual value.

Alarm groups and first-in-group

src/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 applies the current state. Clients therefore see a coherent suppression state immediately after registration; no state transition is required.

Live demo: The reference implementation in samples/Quickstarts.Servers/Alarms/AlarmNodeManager.cs exposes an /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 node manager derives from AsyncCustomNodeManager, so this file also demonstrates 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 proxy: ConditionTypeClient, AcknowledgeableConditionTypeClient, AlarmConditionTypeClient, DialogConditionTypeClient, or ShelvedStateMachineTypeClient. The proxy receives the caller-supplied conditionId as its ObjectId, following the Part 9 §5.5.4 idiom that accepts ConditionId as ObjectId. The generated proxy remains the single source for each 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 call the spec-defined *2 method when the comment is non-empty: Suppress2, Unsuppress2, RemoveFromService2, PlaceInService2, and 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 derived from BaseEventType, the generator emits a partial record {Type}Record. Each generated record derives from its parent record and exposes one init-only property for each declared field. The standard NodeSet produces the following hierarchy, abridged to show the most commonly observed types:

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 these types automatically get their own *TypeRecord, which derives from the closest standard ancestor. No checked-in code or manual class definition is needed. Add a hand-written partial record VibrationAlarmTypeRecord to 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 defines ConditionId as the NodeId of the condition object that raised the event. The event reports this NodeId in the SourceNode field.

Source-generated decoders + EventRecordDecoderRegistry

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

The process-wide EventRecordDecoderRegistry.Default registers the standard UA model. It routes by the event's EventType field. If the exact type is not registered, it walks the OPC UA event-type hierarchy through an optional SuperTypeResolver. Vendor models can register their generated extensions with 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 returns an EventFilter. Its where clause restricts events to OfType({recordTypeId}), and its select clauses come from the supplied registry's composed StandardFields (by default, EventRecordDecoderRegistry.Default). Use the filter with EventRecordDecoderRegistry.Decode. The registry remaps composed field positions to each decoder's layout before decoding, 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);

Pass the same registry to Subscribe*Async through its registry: parameter. This ensures the streaming side uses 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 server-side DialogConditionType properties OkResponse, CancelResponse, and DefaultResponse carry canonical indices (Part 9 §5.6.2). Applications can read these indices separately through the Read service. The standard DialogConditionTypeRecord does not include them; it contains only the dialog prompt and 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 streaming subscription also provides ISubscription.ConditionRefreshAsync. Both refresh methods are equivalent when AlarmClient and IStreamingSubscription use 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
  • Subscriptions and Monitored Items Service Set — IStreamingSubscription and the V2 subscription engine
  • State Machines — generic Part 16 state-machine API used by AlarmClient.GetShelvingStateAsync / ObserveShelvingTransitionsAsync
  • Model Change Tracking — client cache invalidation on address-space changes
  • Source: src/Opc.Ua.Server/Alarms/, src/Opc.Ua.Client/Alarms/, src/Opc.Ua.Core.Types/State/AlarmConditionState.Methods.cs
  • Reference client sample: samples/Reference/ConsoleReferenceClient/AlarmClientSample.cs
  • Conformance tests: tests/Opc.Ua.History.Tests/AlarmsAndConditions*.cs