History reads
Three flavours of history read: raw samples, aggregated buckets, and resolved-at-time. All three target servers that store historical data — a feature OPC UA marks as optional.
History reads target servers that retain historical data. The OPC UA
spec defines them in Part 11 as an optional service set — many servers
do not implement it and will respond with BadServiceUnsupported or
BadHistoryOperationUnsupported. Test against your target server
before designing a feature around the call.
This library exposes three call shapes:
| Method | Use |
|---|---|
historyReadRaw() |
Every stored sample in [startTime, endTime] |
historyReadProcessed() |
Aggregates (avg, min, max, …) bucketed by interval |
historyReadAtTime() |
Values resolved at specific timestamps |
All three return DataValue[] (or DataValue[][] for the multi-node
forms). Each entry carries the historical value, its sourceTimestamp,
and the status the server attached at storage time.
historyReadRaw
$samples = $client->historyReadRaw(
nodeId: 'ns=2;s=Devices/PLC/Temperature',
startTime: new DateTimeImmutable('-15 minutes'),
endTime: new DateTimeImmutable(),
);
foreach ($samples as $dv) {
echo $dv->sourceTimestamp->format('c') . " " . $dv->getValue() . "\n";
}
$nodeId
The Variable node to query. Only Variable nodes with historicizing
enabled return history.
$startTime
Inclusive lower bound, or null for open-ended. OPC UA history works
with server-side timestamps; if the storage was wall-clock at
sample time, that is the clock you're querying.
$endTime
Inclusive upper bound, or null for open-ended. Reverse the order
(startTime > endTime) to read history in reverse-chronological order.
$numValuesPerNode
Max samples to return. 0 = unlimited (subject to server-side caps).
$returnBounds
When true, the server may synthesise interpolated values at
startTime and endTime if no stored sample sits exactly there.
Pagination
History responses can carry a continuation point when the result set
exceeds numValuesPerNode or the server's internal cap. The library
follows continuation points transparently until the server reports
done — the returned array is the full set.
historyReadProcessed
Aggregated reads bucket history into fixed-width intervals and apply a standard OPC UA aggregate to each bucket — average, min, max, total, count, time-weighted variants, and so on.
Info
When the server doesn't implement HistoryRead Processed
(Bad_HistoryOperationUnsupported) or you already have raw samples
in memory, use the client-side AggregateModule instead —
$client->historyAggregate(...) fetches raw history + aggregates in
one call, $client->aggregate(...) runs against an in-memory buffer.
See Client-side aggregates.
use PhpOpcua\Client\Types\NodeId;
$hourly = $client->historyReadProcessed(
nodeId: 'ns=2;s=Tank42/Level',
startTime: new DateTimeImmutable('-1 day'),
endTime: new DateTimeImmutable(),
processingInterval: 3_600_000.0, // 1 hour in ms
aggregateType: NodeId::numeric(0, 2342), // Average
);
The aggregate is itself a NodeId in namespace 0 — pass a NodeId
instance (the string shorthand is not accepted here). The well-known
set includes:
| NodeId | Aggregate |
|---|---|
i=2342 |
Average |
i=2345 |
Maximum |
i=2346 |
Minimum |
i=2352 |
Count |
i=2350 |
Total |
i=2347 |
TimeAverage |
i=2348 |
TimeAverage2 |
Refer to OPC UA Part 13 for the complete catalogue and the precise
mathematical definitions. Not every server supports every aggregate —
an unsupported aggregate returns BadAggregateNotSupported per result
entry.
historyReadAtTime
Resolves one value per requested timestamp — either the stored sample
at exactly that time, or, when no sample sits there, whatever value
the server's HistoryReadAtTime implementation returns (typically an
interpolated value flagged with Uncertain per Part 11).
$values = $client->historyReadAtTime(
nodeId: 'ns=2;s=Tank42/Level',
timestamps: [
new DateTimeImmutable('2026-05-15 08:00:00'),
new DateTimeImmutable('2026-05-15 12:00:00'),
new DateTimeImmutable('2026-05-15 16:00:00'),
new DateTimeImmutable('2026-05-15 20:00:00'),
],
);
The exact resolution behaviour (interpolation vs. simple bounds vs.
exact-match-only) is configured on the server, not from this client.
Inspect the returned DataValue::$statusCode to distinguish stored
samples (Good) from server-resolved ones (Uncertain* variants).
Capability detection
Before designing a feature around history reads, verify the server supports the relevant service. The cheapest probe is one of:
- Call
historyReadRaw()for a recent 10-second window on a known historicizing node, withnumValuesPerNode: 1. AGoodor empty reply means the service is supported. - Browse the
HistoryServerCapabilitiesobject (i=2330) and read itsAccessHistoryDataCapabilityproperty.
See Recipes · Detecting server capabilities and Recipes · Handling unsupported services.
Failure modes
| StatusCode | Meaning |
|---|---|
BadServiceUnsupported |
The server does not implement HistoryRead at all |
BadHistoryOperationUnsupported |
HistoryRead exists but this operation flavour is not supported |
BadHistoryOperationInvalid |
The request is malformed (start > end with wrong direction, etc.) |
BadNoDataAvailable |
Node is historicizing but has no data in the range |
BadAggregateNotSupported |
The aggregate NodeId is unknown to the server |
BadInvalidTimestampArgument |
Timestamps in historyReadAtTime are out of order |
For BadServiceUnsupported, the library raises
ServiceUnsupportedException rather than letting the bad status pass
through as DataValue[]. See Reference ·
Exceptions.