Alarms and Conditions (OPC UA Part 9)
July 16, 2026 · View on GitHub
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
(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);
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 —
samples/Quickstarts.Servers/Alarms/AlarmHolders/.
Driving state from your process
All state transitions go through typed setters that:
- update the
TwoStateVariableStatevalue +Id - stamp
TransitionTime - recompute
EffectiveDisplayNameand the compositeSuppressedOrShelvedflag - clear
ChangeMasksso 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 setsLatchedState = true.SetActiveState(context, false)—ActiveStatereflects the real process state, butLatchedStatestaystrue.Reset(server-side method, auto-wired) — clearsLatchedState. TheResetmethod validates all preconditions before accepting: enabled, not active, acknowledged, confirmed (ifConfirmedStateis present). Any other state returnsBad_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
ReportStateChangeso 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
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 always applies the current state, so
clients see a coherent suppression state immediately after
registration — there is no edge required.
Live demo: The
samples/Quickstarts.Servers/Alarms/ AlarmNodeManager.csreference implementation wires this up end-to-end. It exposes a/Alarms/AnalogGroup(AlarmGroupType) containing every analog-source alarm and a writable/Alarms/MaintenanceModeboolean. Writingtrueto MaintenanceMode runsAlarmSuppressionEngine.Evaluate(...)and suppresses every group member; writingfalseclears suppression on the next evaluation. The reference server's node manager itself derives fromAsyncCustomNodeManager, 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
- Subscriptions and Monitored Items Service Set —
IStreamingSubscriptionand 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/ConsoleReferenceClient/AlarmClientSample.cs - Conformance tests:
tests/Opc.Ua.History.Tests/AlarmsAndConditions*.cs