Repository navigation
Client Historical Access
Milo 1.2.0-RC1, Java 17. Source baseline 69cb25087804.
The pages so far worked with current values. HistoryRead returns values a server stored in the past, which an ordinary Read or a live subscription cannot give you. Read history only from a server that implements the historical mode you request and grants your Session user access to it. The tutorial server keeps no history, so the examples target a history-enabled server, such as a historian or a vendor simulation server, and take the NodeId of a node it records. You need a connected client from Connecting, and the DataValue handling from Reading applies to every stored sample.
A history read is a paged query. You describe what you want in a details object, such as raw values between two times, and send it for one or more nodes. For each node the server returns a status, a page of data, and possibly a continuation point, an opaque token meaning the server has more. You send it back with the same request to get the next page, and you release it if you stop early, because the server holds resources for it on behalf of your Session. A complete read is therefore a loop that sends the request, decodes the page, keeps each sample with its quality, and repeats while a continuation point comes back.
Raw history is the common case. Before you write the request, choose the time interval, the maximum number of values per node, and whether you need bounding values. A zero result limit does not mean everything arrives in one response, so continuation points can still occur.
This helper requests unmodified raw data with Both timestamps, at most 1,000 values per page, and no bounding values. The caller supplies a connected client, the NodeId of a node with history, explicit start and end times, and a page budget.
static List<DataValue> readRawHistory(
OpcUaClient client, NodeId nodeId, DateTime start, DateTime end, int maxPages)
throws UaException {
if (maxPages < 1) {
throw new IllegalArgumentException("maxPages must be positive");
}
// Unmodified raw values, at most 1,000 per page, without bounding values.
var details = new ReadRawModifiedDetails(false, start, end, uint(1_000), false);
List<DataValue> values = new ArrayList<>();
ByteString continuationPoint = ByteString.NULL_VALUE;
try {
for (int page = 0; page < maxPages; page++) {
HistoryReadResult result =
sendHistoryRead(client, details, nodeId, continuationPoint, false);
continuationPoint = result.getContinuationPoint();
if (!result.getStatusCode().isGood()) {
throw new UaException(result.getStatusCode());
}
values.addAll(decodeHistoryData(client, result));
if (continuationPoint == null || continuationPoint.isNullOrEmpty()) {
break;
}
}
return values;
} finally {
// Release the continuation point if the loop stopped before the last page.
if (continuationPoint != null && !continuationPoint.isNullOrEmpty()) {
HistoryReadResult released =
sendHistoryRead(client, details, nodeId, continuationPoint, true);
if (!released.getStatusCode().isGood()) {
throw new UaException(released.getStatusCode());
}
}
}
}The loop keeps the latest continuation point and sends it back with the same details, for at most maxPages pages. The finally block runs whether the loop reached the last page, used up its page budget, or threw, so the helper tries to release any continuation point it abandons, and a release failure surfaces as an exception. The helper rejects a nonpositive maxPages.
sendHistoryRead builds and sends the one-node request for both page reads and the release, and decodeHistoryData turns a Good result into samples.
static HistoryReadResult sendHistoryRead(
OpcUaClient client,
ReadRawModifiedDetails details,
NodeId nodeId,
ByteString continuationPoint,
boolean releaseContinuationPoints)
throws UaException {
var nodeToRead =
new HistoryReadValueId(nodeId, null, QualifiedName.NULL_VALUE, continuationPoint);
HistoryReadResponse response =
client.historyRead(
details, TimestampsToReturn.Both, releaseContinuationPoints, List.of(nodeToRead));
return requireNonNull(response.getResults())[0];
}
static List<DataValue> decodeHistoryData(OpcUaClient client, HistoryReadResult result) {
// A Good_NoData result can omit the HistoryData body.
ExtensionObject historyData = result.getHistoryData();
if (historyData == null || historyData.isNull()) {
return List.of();
}
var data = (HistoryData) historyData.decode(client.getStaticEncodingContext());
DataValue[] dataValues = data.getDataValues();
if (dataValues == null) {
return List.of();
}
return Arrays.asList(dataValues);
}When a node's HistoryReadResult is Good, decode its HistoryData envelope if one is present, and treat a null or body-less envelope as an empty page. A Good_NoData result can legitimately omit the body. The static encoding context decodes the standard history envelope. Custom structures inside its Variants can require dynamic type discovery and separate ExtensionObject decoding, as described in Data types.
A successful operation does not make every historical sample Good. The helper keeps each stored DataValue with its own status and timestamps, so a stored sample with Bad_SensorFailure, for example, stays in the list with that status.
When you stitch adjacent intervals together, direction and boundary semantics decide which samples appear in both. Deduplicate using the server's documented identity or timestamp policy instead of blindly dropping equal values.
client.historyRead(details, timestamps, releaseContinuationPoints, nodes) sends any HistoryRead request, and the details object selects the operation:
-
ReadRawModifiedDetailsrequests raw or modified data. ItsisReadModifiedflag changes both the request semantics and the result decoding. -
ReadProcessedDetailsrequests aggregates. -
ReadAtTimeDetailsrequests values at selected instants. - Event-history details request event fields.
The result body type depends on the requested mode. Modified data arrives as HistoryModifiedData, and event history uses the matching event-history result structure, so do not cast every successful body to HistoryData. historyReadAsync(...) has the same split between service and per-node results, and the same resource obligations.
Support is server-specific, and a generated request class does not imply it. Check the server's historical capabilities, the node's metadata, your access permissions, and the actual operation results.
Keep the latest opaque continuation point for each node and resubmit it with the matching request. A continuation point belongs to its Session and query. Do not persist it as a restart token, share it between clients, or confuse it with a Browse continuation point.
A server can reclaim an older point from your Session to make room for a new request, and resuming a reclaimed point returns Bad_ContinuationPointInvalid. When possible, finish or release paused reads before starting more.
To release an abandoned point, send the HistoryRead request with releaseContinuationPoints=true and inspect the release result, as the helper's finally block does. If your application already holds an earlier processing exception when the release fails, add the cleanup failure as a suppressed exception so neither diagnostic is lost.
Inspect the service result, the result count you expect, and every node's status, and store diagnostics with the affected node and interval.
| Status | Meaning | What to do |
|---|---|---|
Bad_HistoryOperationUnsupported |
The node or its provider does not implement the requested operation | Treat it as permanent for that node and mode |
Bad_UserAccessDenied |
The Session user lacks access to the node's history | Correct the user's permissions instead of retrying |
Good_NoData |
No data exists in the requested interval, and the body can be missing | Treat it as an empty page, and still check for a continuation point |
Bad_NoData |
In OPC UA Part 11, an action on an interval, such as a processed read or a delete, found no data for the time range or event filter | Handle it separately from Good_NoData, which a plain read of an empty interval returns |
Bad_ContinuationPointInvalid |
The continuation point is invalid or the server reclaimed it | Restart the affected interval |
Bad_TimestampNotSupported |
The provider cannot return the requested timestamps. The helper asks for Both, which a provider that stores only source timestamps can reject. | Request timestamps the provider stores |
An empty successful page does not prove that no continuation point exists, so the loop checks the point on every page.
When you restart an interval after an invalid or reclaimed point, reconcile duplicates and gaps, because data may have changed between queries. Do not retry permanent capability or permission errors indefinitely. Bound pages, memory, elapsed time, and concurrency for every history job.
At this baseline, the default server service adapter does not pass the release flag into AddressSpace.historyRead. This affects Milo servers rather than the Milo client. A conforming custom server provider that needs explicit release must handle it at the service boundary. Server historical access shows how, and covers the implementation boundary and resource limits.
Milo's default server address-space history hooks return Bad_HistoryOperationUnsupported, so the client API alone does not mean a Milo server has a historian. Vendor-specific endpoints, supported aggregates, event-history behavior, and retention policies need separate interoperability testing.
-
ProsysHistoryReadExample: Targets an external Prosys Simulation Server. Endpoint setup and vendor interoperability remain external requirements.
That completes the client guide. To build the other side, including a server that stores and serves history of its own, continue with the Server SDK.
Related: Client SDK · Server historical access · Data types · Recovery and limits
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