Repository navigation
Server Events
Milo 1.2.0-SNAPSHOT, Java 17. API baseline 7faaf4f0516c.
Some things a client needs to hear about once, when they happen, rather than read as a current value. OPC UA reports those as events. This page extends the thermostat so it fires a "setpoint changed" event each time Setpoint changes, whether a client writes the value or calls AdjustSetpoint from Methods.
You need the tutorial server from Your first server, and a client that can create an event MonitoredItem as shown in Client subscriptions.
An event carries typed field values instead of a Variable Value notification. The server builds a short-lived event node from an event type, fills in its fields, fires it, and then deletes it. A client receives it through an event MonitoredItem, which monitors a node's EventNotifier attribute and supplies an EventFilter. The filter's select clauses choose the fields to return, and its optional where clause restricts which events match.
SourceNode says where the occurrence happened, but setting it does not decide who hears about it. That is the job of a separate notifier hierarchy built from HasNotifier and HasEventSource references. For a monitored Object M, an event reaches the item when its source is M or is reachable from M through forward HasNotifier or HasEventSource references, including subtypes. Milo resolves this at fire time by walking inverse references up from the source, so both directions of each reference must exist. Each Object that clients monitor also needs the SubscribeToEvents bit in its EventNotifier attribute.
The standard Server Object is the root of that hierarchy, so an item on it receives every fired event before its filter applies. An event without a usable source hierarchy reaches only items on the Server Object. Application listeners registered directly with the event notifier, not through a MonitoredItem, receive every event regardless of scope.
The thermostat's events have Thermostat as their source, so these client items receive them:
| The client's event item monitors | It receives the setpoint event when |
|---|---|
The Server Object (NodeIds.Server) |
Always, with no extra wiring. |
| Thermostat | Thermostat has the SubscribeToEvents bit, which registerEventNotifier(thermostat) sets below. The source is the monitored node itself. |
| Another Object, such as an area folder | Thermostat is reachable from that Object through forward HasNotifier or HasEventSource references. See Group sources under an area. |
A helper builds and fires one event, and an observer in the tutorial namespace calls it whenever Setpoint's value changes.
server.getEventInstantiator() creates an event instance and its fields from the event type, and sets EventType to the requested type. The standard fields include EventId, EventType, SourceNode, SourceName, Time, ReceiveTime, Message, and Severity. This helper fills them in for a BaseEventType event whose source is Thermostat, fires it, and deletes the temporary nodes. Imports:
-
BaseEventTypeNodefromorg.eclipse.milo.opcua.sdk.server.model.objects -
OpcUaServerfromorg.eclipse.milo.opcua.sdk.server -
UaObjectNodefromorg.eclipse.milo.opcua.sdk.server.nodes -
NodeIdsandUaExceptionfromorg.eclipse.milo.opcua.stack.core -
ByteString,DateTime,LocalizedText, andNodeIdfromorg.eclipse.milo.opcua.stack.core.types.builtin -
UShortfromorg.eclipse.milo.opcua.stack.core.types.builtin.unsigned -
java.util.UUIDandjava.nio.charset.StandardCharsets - static
org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.ushort
static void fireSetpointChanged(OpcUaServer server, UaObjectNode thermostat, double setpoint)
throws UaException {
UShort namespaceIndex = thermostat.getNodeId().getNamespaceIndex();
NodeId eventNodeId = new NodeId(namespaceIndex, UUID.randomUUID());
BaseEventTypeNode event =
server.getEventInstantiator().createEvent(eventNodeId, NodeIds.BaseEventType);
try {
byte[] eventId = UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8);
DateTime now = DateTime.now();
event.setEventId(ByteString.of(eventId));
event.setSourceNode(thermostat.getNodeId());
event.setSourceName(thermostat.getDisplayName().text());
event.setTime(now);
event.setReceiveTime(now);
event.setMessage(LocalizedText.english("Setpoint changed to " + setpoint));
event.setSeverity(ushort(100));
server.getEventNotifier().fire(event);
} finally {
event.delete();
}
}The event node needs a fresh NodeId in the application's registered namespace, so the helper uses Thermostat's namespace index with a random UUID. EventId is separate. It identifies the occurrence to clients, so every occurrence needs a unique value. Time is when the event occurred and ReceiveTime is when this server received it, which is the same moment for an event the server produces itself. Severity runs from 1 to 1000. Keep it within your application's OPC UA event policy.
Firing is synchronous, so every listener has extracted its fields before the finally block deletes the temporary event nodes. A listener that retains information must copy the values it needs, because the nodes do not survive. The try/finally keeps a failure while populating or firing from leaking nodes.
Setpoint changes when a client writes it or when the AdjustSetpoint handler stores a new value. Both paths end by storing the value on the node, which then calls its attribute observers. One observer on Setpoint therefore sees every successful change, while a write rejected by a filter or by SDK checks never reaches it.
Add this to the tutorial namespace from Your first server, after it creates Thermostat and Setpoint. registerEventNotifier(...) is a protected method of ManagedNamespace, so it runs inside the namespace class. Imports, beyond the helper's:
-
UaVariableNodefromorg.eclipse.milo.opcua.sdk.server.nodes -
AttributeIdfromorg.eclipse.milo.opcua.stack.core -
DataValuefromorg.eclipse.milo.opcua.stack.core.types.builtin -
LoggerandLoggerFactoryfromorg.slf4j, for the namespace'sloggerfield
registerEventNotifier(thermostat);
setpoint.addAttributeObserver(
(node, attributeId, value) -> {
if (attributeId != AttributeId.Value) {
return;
}
DataValue dataValue = (DataValue) value;
if (!(dataValue.value().value() instanceof Double newSetpoint)) {
return;
}
try {
fireSetpointChanged(getServer(), thermostat, newSetpoint);
} catch (UaException e) {
logger.warn("Failed to fire setpoint changed event", e);
}
});registerEventNotifier(thermostat) sets the SubscribeToEvents bit on Thermostat and, unless Thermostat already has a HasNotifier parent, connects it to the Server Object with a HasNotifier reference pair. Calling it again has no further effect.
The observer runs on the thread that stored the value, before that Write or Call returns, and inside Setpoint's synchronized setAttribute. Setpoint therefore stays locked while the event is built and delivered, and any EventListener registered directly with the event notifier runs under that lock too. Other threads that read or write Setpoint wait until it is released, so keep the observer and such listeners short. The value is already stored when the observer runs, which is why a failure to build the event is logged rather than thrown back into the write. The observer also runs for every stored value, so writing the value Setpoint already holds fires another event.
Create an event item on Thermostat, or on the Server Object, that selects EventId, SourceNode, Message, and Severity. Writing 23.5 to Setpoint delivers one event whose Message is Setpoint changed to 23.5 and whose SourceNode is the Thermostat NodeId. Calling AdjustSetpoint with a Delta of 1.0 delivers Setpoint changed to 24.5. A write that fails, such as a String value returning Bad_TypeMismatch, delivers nothing.
registerEventNotifier(...) connects a notifier to the Server Object, and you wire the hierarchy below it. For a server with several thermostats in one area, register the area Object as the notifier and add a HasEventSource or HasNotifier reference from the area to each thermostat. A client monitoring the area then receives events from every thermostat in it. Organizes and HasComponent references do not establish event scope. UaNode.addReference and NodeManager.addReferences add the inverse reference for you, but NodeManager.addReference adds only the one you pass. The notifier subtree setup in the alarm example does this for a Boiler area and its Motor.
For a custom event type, instantiate the actual subtype and keep the EventType the instantiator set. Use createEvent(Class<T>, NodeId, NodeId) when you need the generated Java subtype. Publish the type hierarchy and its field declarations so clients can form correct select clauses, and populate every field clients select.
Notifier routing limits the candidate recipients, and the client's filter then chooses events and projects their fields. Check monitored-item creation results and filter diagnostics before you debug event generation.
| What you see | Cause and response |
|---|---|
Bad_AttributeIdInvalid when creating the item |
The Object lacks the SubscribeToEvents bit. Register it with registerEventNotifier(...). |
Bad_MonitoredItemFilterInvalid when creating the item |
The event filter is missing or cannot be decoded. Supply a valid EventFilter. |
| The access controller's status when creating the item | Access was denied. Unlike a data item, an event item cannot carry the denial in a later DataValue, so creation fails. |
| The item exists but receives nothing | The source is not reachable from the monitored node. Wire the hierarchy in both directions, or monitor the Server Object. |
| Some events are missing | A where clause can validly exclude an event even though routing and firing succeeded. Queue limits and publish timing still apply. |
| A selected field is null | The event type declares the field but the event left it unpopulated, or the select clause names a field the type does not have. |
UaException from createEvent(...)
|
An unknown type, a type that cannot be instantiated, or a NodeId collision is a construction error. No event is delivered. |
Do not treat client where clauses as authorization. If an event contains data that some Sessions must not see, design the event access policy and the exposure of sources and fields explicitly. The data-value UserAccessLevel sampling cache is not a per-event permission system.
Repeatedly firing BaseEventType nodes does not implement retained state, acknowledgement, shelving, or refresh. Use Alarms and Conditions for those. Event history and durable persistence are also separate application work.
Delete each temporary event instance after firing it, as the helper's finally block does. Unregister application event listeners when their owner stops. Retire notifier and source nodes only after their producers stop. For the thermostat, keep a reference to the Setpoint observer so you can remove it with removeAttributeObserver(...) before deleting Thermostat. Condition behavior owns longer-lived state and must be unregistered through its manager before its nodes are retired.
EventFactory is deprecated in favor of EventInstantiator. The instantiation package is experimental for one minor release. When migrating, compare optional fields and persisted derived IDs, and keep event-node lifetime separate from persistent source identity.
The linked GenerateEventMethod is an older example. Do not copy these choices from it:
- It repeats the fixed EventId
{0,1,2,3}. - It overwrites EventType with BaseEventType even when another type was requested.
- It deletes nodes without
finally.
Events report occurrences once. To keep state that operators acknowledge and that clients can refresh after reconnecting, continue with Alarms and Conditions, which adds a high-temperature alarm to Thermostat.
Related: Server SDK · Client subscriptions · Methods · Event routing changes in 1.2
User guide for Milo 1.2.0-SNAPSHOT at source 7faaf4f05, 2026-10-04. Snapshot builds can change after this baseline. Home · Getting started · Examples · Troubleshooting · Release notes
- Home
- Start here
- Client guide
- Server guide
- Feature guides
- Reference
- Operations
- Examples
- Release notes
- Contributing