Repository navigation
Server Alarms and Conditions
Milo 1.2.0-RC1, Java 17. API baseline 69cb25087804.
An event reports an occurrence once. An alarm keeps state. It becomes active, waits for an operator to acknowledge it, and stays retained until both the process and the operator are done with it. OPC UA models this as a Condition, with operator actions exposed as Methods. This page adds a high-temperature alarm to Thermostat with a high limit of 80.0, driven by the thermostat's temperature.
It builds on Events, which made Thermostat an event notifier. A client sees alarm transitions as events, so the client side is the same event subscription.
A Condition has two parts. A generated ConditionTypeNode supplies the model structure that clients browse and read. A Condition behavior manages state transitions, EventIds, Methods, and refresh. Setting attributes on the generated node does not activate alarm behavior, so you always work through the behavior object.
Concrete behaviors such as AlarmCondition, ExclusiveLimitAlarm, and NonExclusiveLimitAlarm create or wrap the node for you. Each one returns unregistered, so you register it with server.getConditionManager() before clients invoke its Methods or request refresh. From then on, your application tells the behavior what the process is doing, and the behavior decides which state transition that is and fires the matching event.
Build a server namespace and an event source hierarchy before adding Conditions, because alarm events follow the same notifier routing as any other event.
An exclusive limit alarm is in at most one limit state at a time, which suits a single high limit. Your code creates and registers the alarm once, then feeds it each new temperature reading.
This code runs in the tutorial namespace after it creates Thermostat and Temperature, so thermostat and temperature are those nodes. Imports:
-
ExclusiveLimitAlarmfromorg.eclipse.milo.opcua.sdk.server.conditions -
UaObjectNodeandUaVariableNodefromorg.eclipse.milo.opcua.sdk.server.nodes -
UaExceptionfromorg.eclipse.milo.opcua.stack.core -
DataValuefromorg.eclipse.milo.opcua.stack.core.types.builtin
ExclusiveLimitAlarm alarm =
ExclusiveLimitAlarm.create(
getNodeContext(),
builder ->
builder
.nodeId(newNodeId("HighTemperatureAlarm"))
.browseName(newQualifiedName("HighTemperatureAlarm"))
.conditionSource(thermostat)
.inputNode(temperature.getNodeId())
.highLimit(80.0));
getServer().getConditionManager().register(alarm);
DataValue currentValue = temperature.getValue();
if (currentValue.value().value() instanceof Double currentTemperature) {
alarm.evaluate(currentTemperature);
}The NodeId and BrowseName are stable application identities, which matters once you persist alarm state. conditionSource(thermostat) makes Thermostat the SourceNode and SourceName of the alarm's events and adds a HasCondition reference from Thermostat to the alarm. Thermostat is already a registered notifier, so the alarm's events reach items on Thermostat as well as on the Server Object. A source with no HasEventSource or HasNotifier parent would instead get a HasEventSource reference from the Server Object. inputNode(...) records which Variable the alarm watches, for clients to read, but the SDK does not subscribe to it.
Registration comes before the first evaluation, so the alarm can answer Methods and refresh as soon as it can become active. The tutorial's Temperature is a constant 21.5, below the limit, so this first evaluation leaves the alarm inactive and fires nothing.
The behavior does not poll Temperature or any other node, so whatever produces readings, such as a device driver callback or a polling task, must pass each one to the alarm. This helper stores a reading in Temperature and evaluates the alarm with the same value. Imports: UaVariableNode, ExclusiveLimitAlarm, and DataValue as above, plus Variant from org.eclipse.milo.opcua.stack.core.types.builtin.
static void updateTemperature(
UaVariableNode temperature, ExclusiveLimitAlarm alarm, double reading) {
DataValue value = new DataValue(new Variant(reading));
temperature.setValue(value);
alarm.evaluate(reading);
}evaluate(double) applies Milo's scalar limit and deadband policy to pick the limit state. A non-finite reading, such as NaN from a failed sensor, leaves the alarm state unchanged instead of clearing it.
Create an event item on Thermostat that selects EventId, ActiveState/Id, and Retain. A reading of 85.0 makes the alarm active, unacknowledged, and retained, and the item receives that transition. A later reading of 70.0 clears the active state, but the alarm stays retained until someone acknowledges it, and the item receives that transition too. Acknowledging with the cleared event's EventId, as the client received it, produces an inactive, non-retained notification.
Clients call Acknowledge with the relevant EventId and a comment. A successful acknowledgement marks that occurrence acknowledged. Acknowledging an active alarm does not clear the process state, because only the process feed changes it. A cleared, acknowledged alarm can stop being retained. Confirmation is a separate operator action, which you enable with withConfirm() on the builder.
Shelving suppresses alarm presentation according to the behavior's state machine, but evaluation of the process continues. withShelving() enables shelving without a MaxTimeShelved property, and withShelving(Duration maxTimeShelved) also sets that maximum. One-shot shelving lasts until the alarm goes inactive, and timed shelving lasts for the requested time. A maximum bounds both, because TimedShelve cannot exceed it and OneShotShelve releases automatically when it elapses.
Timed expiry is scheduled. If the timer runs late, the next access to the alarm's state applies the expiry lazily. Treat operation status, illegal state transitions, and requested duration limits as part of the client workflow.
Clients invoke ConditionRefresh to repopulate retained state after establishing event monitoring or recovering. The manager dispatches Condition Methods, emits refresh boundaries, and supplies retained snapshots to the appropriate event items, skipping items whose monitoring is disabled. Maintain notifier relationships so the relevant monitored subtree receives the refresh. Refresh is not a historian replay or a list of all past events, because it sends only the state that is still retained.
The manager supports copied instance Methods and shared Methods, including shelving Methods addressed through the Condition or its nested ShelvingState Object. Do not replace shared Method handlers globally to implement one Condition. Application authorization and any ConditionMethodInterceptor must match the operator policy, so test denied actions as well as allowed ones.
The main path creates a new alarm. When the instance already exists in the address space, for example from an imported model, you activate it instead. Concrete behaviors have three entry points:
| Entry point | Use |
|---|---|
create(context, builder) |
Create and initialize a new typed instance. |
attach(node, options) |
Activate a structurally complete generated instance already in the address space. |
adopt(context, nodeId, builder) |
Complete a partial generated instance in place, preserving identities and stored configuration unless explicitly overridden. |
attach and adopt accept a generated subtype at the selected behavior level, leaving the existing HasTypeDefinition, EventType, NodeIds, and subtype fields intact. Adopting generic alarm behavior for an imported specialized alarm does not add its missing numeric evaluator. A NodeSet loader, if required, remains an external integration.
evaluate(double) covers a scalar reading like the thermostat's. For compound or array data, compute the state yourself and call ExclusiveLimitAlarm.setLimitState(...) or NonExclusiveLimitAlarm.setLimitStates(...). For non-exclusive alarms, provide the set of active states and the primary state used in event presentation. Milo applies the result through its Condition state boundary, but how to reduce your data to a state is your decision.
For a custom Condition type or Java behavior, use Condition.createInstance, attachInstance, or adoptInstance. These entry points provide structure, Method discovery, and handler installation, while your behavior supplies the custom domain state machine.
| Status or event | Returned by | Meaning and response |
|---|---|---|
Bad_EventIdUnknown |
Acknowledge | The EventId does not match a known occurrence. Use the EventId from the event the client received. |
Bad_NothingToDo |
ConditionRefresh | The subscription has no event items. Create an event item first. |
Bad_MonitoredItemIdInvalid |
ConditionRefresh2 | The item is missing or is not an event item. |
Bad_SubscriptionIdInvalid |
ConditionRefresh and ConditionRefresh2 | The subscription does not exist. |
Bad_UserAccessDenied |
ConditionRefresh and ConditionRefresh2 | The subscription belongs to another Session. |
Bad_RefreshInProgress |
ConditionRefresh and ConditionRefresh2 | An overlapping refresh for the same subscription is still running. Wait for it to finish. |
RefreshRequiredEventType event |
Delivered to the client | The condition queue overflowed. Refresh again. |
The current implementation supports the trunk branch (BranchId null), not general historical Condition branches. Do not promise multiple outstanding branch identities.
Custom subtype fields cannot currently change atomically with the private stock limit transition. Writing fields before or after setLimitStates is not a guaranteed single event-state transaction.
captureSnapshot() captures mutually consistent state. The application owns serialization, storage, retention, and protection of the operator comments and identities a snapshot can contain. Keep snapshots compatible with node and model versions across application upgrades.
restoreSnapshot() works only before the behavior goes live, and it is rejected once events or refresh have exposed the Condition. For the thermostat, restore right after create(...) and before register(...) or evaluate(...), because a registered alarm can be exposed by a client's refresh and an evaluation can fire an event. Create the alarm with the same stable ConditionId each time, which is why its NodeId is a fixed string. Reconnecting clients then learn the recovered state through refresh. Missing snapshot fields take recovery defaults rather than reconstructing lost process history.
Retire an alarm in three steps. Stop the process feed, unregister the behavior, and then delete its owned node tree with alarm.getNode().delete(). Deletion recursively removes HasChild descendants.
Unregistration releases handlers, filters, and timers, but it does not stop a later application evaluate(...) or setActive(...) call from emitting events, which is why the feed stops first. Unregistering is terminal. Registering the same instance again does not restore its released resources, so construct a fresh behavior if you need the alarm again. The default manager also releases runtime resources when another behavior replaces this one under the same ConditionId, and at server shutdown. It does not own your historian, process driver, or snapshot storage.
Calls that already obtained a handler may complete after retirement, so coordinate the resources they use.
The alarm reused a standard type with a builder. To create your own typed node trees from ObjectType declarations and control their identities, continue with Types and instantiation.
Related: Server SDK · Events · Client subscriptions
User guide for Milo 1.2.0-RC1 at source 69cb25087, 2026-10-12. Home · Getting started · Examples · Troubleshooting · Release notes
- Home
- Start here
- Client guide
- Server guide
- Feature guides
- Reference
- Operations
- Examples
- Release notes
- Contributing