How-to guide
Historical data
Read the historical values of a node over time ranges, at specific times, and aggregated, modify and delete them, and read the event history.
How-to guide
Historical data
Types usedOpcClientOpcNodeIdIOpcNodeHistoryNavigatorOpcHistoryValueOpcModifiedHistoryValueOpcAggregateType
According to the OPC UA specification, every node of the Variable node class can record the values of its Value attribute over time. On every change of the Value attribute, the server then stores the new value together with the timestamp of the Value attribute. These pairs of value and timestamp are called historical data. The server itself decides where it stores the data. The client uses the IsHistorizing attribute of the node to determine whether the server provides historical data for the node, that is, whether it stores value changes historically. The client selects the node whose historical data it accesses through the NodeId (OpcNodeId). The client can read, update, replace, delete, or create historical data. Reading is the most common case. The server determines whether and which of these operations it allows for a node. The client does not have to process all historical values, regardless of whether it reads with or without a navigator.
Read values of a time range
The client reads historical data over a time range.
Read all values within an open time range, in which StartTime or EndTime is undefined:
Read all values from a specific timestamp (StartTime) onward:
var startTime = new DateTime(2017, 2, 16, 10, 0, 0);
var history = client.ReadNodeHistory(
startTime, endTime: null, "ns=2;s=Machine/Job/Speed");Dim startTime = New DateTime(2017, 2, 16, 10, 0, 0)
Dim history = client.ReadNodeHistory( _
startTime, endTime:=Nothing, "ns=2;s=Machine/Job/Speed")Read all values up to a specific timestamp (EndTime):
var endTime = new DateTime(2017, 2, 16, 15, 0, 0);
var history = client.ReadNodeHistory(
null, endTime, "ns=2;s=Machine/Job/Speed");Dim endTime = New DateTime(2017, 2, 16, 15, 0, 0)
Dim history = client.ReadNodeHistory( _
Nothing, endTime, "ns=2;s=Machine/Job/Speed")Read all values within a closed time range, in which StartTime and EndTime are defined:
var startTime = new DateTime(2017, 2, 16, 10, 0, 0);
var endTime = new DateTime(2017, 2, 16, 15, 0, 0);
var history = client.ReadNodeHistory(
startTime, endTime, "ns=2;s=Machine/Job/Speed");Dim startTime = New DateTime(2017, 2, 16, 10, 0, 0)
Dim endTime = New DateTime(2017, 2, 16, 15, 0, 0)
Dim history = client.ReadNodeHistory( _
startTime, endTime, "ns=2;s=Machine/Job/Speed")Process the values (through an instance that implements the IEnumerable interface):
foreach (var value in history) {
Console.WriteLine(
"{0}: {1}",
value.Timestamp,
value);
}For Each value In history
Console.WriteLine( _
"{0}: {1}",
value.Timestamp,
value)
NextRead values page by page
When reading historical data page by page, the client retrieves only a specific number of values from the server at a time.
Read a specific number of values per page:
var historyNavigator = client.ReadNodeHistory(
10, "ns=2;s=Machine/Job/Speed");Dim historyNavigator = client.ReadNodeHistory( _
10, "ns=2;s=Machine/Job/Speed")Read a specific number of values per page within an open time range, in which StartTime or EndTime is undefined:
Read a specific number of values per page from a specific timestamp (StartTime) onward:
var startTime = new DateTime(2017, 2, 16, 15, 0, 0);
var historyNavigator = client.ReadNodeHistory(
startTime, 10, "ns=2;s=Machine/Job/Speed");Dim startTime = New DateTime(2017, 2, 16, 15, 0, 0)
Dim historyNavigator = client.ReadNodeHistory( _
startTime, 10, "ns=2;s=Machine/Job/Speed")Read a specific number of values per page up to a specific timestamp (EndTime):
var endTime = new DateTime(2017, 2, 16, 15, 0, 0);
var historyNavigator = client.ReadNodeHistory(
null, endTime, 10, "ns=2;s=Machine/Job/Speed");Dim endTime = New DateTime(2017, 2, 16, 15, 0, 0)
Dim historyNavigator = client.ReadNodeHistory( _
Nothing, endTime, 10, "ns=2;s=Machine/Job/Speed")Read a specific number of values per page within a closed time range:
var startTime = new DateTime(2017, 2, 16, 10, 0, 0);
var endTime = new DateTime(2017, 2, 16, 15, 0, 0);
var historyNavigator = client.ReadNodeHistory(
startTime, endTime, 10, "ns=2;s=Machine/Job/Speed");Dim startTime = New DateTime(2017, 2, 16, 10, 0, 0)
Dim endTime = New DateTime(2017, 2, 16, 15, 0, 0)
Dim historyNavigator = client.ReadNodeHistory( _
startTime, endTime, 10, "ns=2;s=Machine/Job/Speed")Process the values (through an instance that implements the IOpcNodeHistoryNavigator interface):
do {
foreach (var value in historyNavigator) {
Console.WriteLine(
"{0}: {1}",
value.Timestamp,
value);
}
} while (historyNavigator.MoveNextPage());
historyNavigator.Close();Do
For Each value In historyNavigator
Console.WriteLine( _
"{0}: {1}", _
value.Timestamp, _
value)
Next
Loop While historyNavigator.MoveNextPage()
historyNavigator.Close()Always make sure that the Close method of the navigator is called. Only then can the server discard the historical data it has buffered for this request. Instead of calling Close explicitly, you can also use the navigator in a using block:
using (historyNavigator) {
do {
foreach (var value in historyNavigator) {
Console.WriteLine(
"{0}: {1}",
value.Timestamp,
value);
}
} while (historyNavigator.MoveNextPage());
}Using historyNavigator
Do
For Each value In historyNavigator
Console.WriteLine( _
"{0}: {1}", _
value.Timestamp, _
value)
Next
Loop While historyNavigator.MoveNextPage()
End UsingThe navigator uses at most 999 requests to retrieve a page. If the server has not completed the page by then, the navigator throws an OpcException with the status BadCommunicationError. This also applies to the navigator of the event history.
Read the history of multiple nodes
Types usedOpcClientOpcNodeIdIOpcNodeHistoryNavigatorOpcHistoryValue
ReadNodesHistory reads the historical values of multiple nodes for the same time range in one call. If you specify a number of values per page, the method returns a navigator. Its indexer returns the values of a single node. The client loads further pages the same way as for a single node:
var nodeIds = new OpcNodeId[] { "ns=2;s=Machine/Speed", "ns=2;s=Machine/Temperature" };
using (var history = client.ReadNodesHistory(
DateTime.UtcNow.AddHours(-1),
DateTime.UtcNow,
numberOfValues: 100,
nodeIds)) {
foreach (OpcHistoryValue value in history[nodeIds[0]])
Console.WriteLine("{0}: {1}", value.Timestamp, value.Value);
}Dim nodeIds = New OpcNodeId() {"ns=2;s=Machine/Speed", "ns=2;s=Machine/Temperature"}
Using history = client.ReadNodesHistory( _
DateTime.UtcNow.AddHours(-1), _
DateTime.UtcNow, _
numberOfValues:=100, _
nodeIds)
For Each value As OpcHistoryValue In history(nodeIds(0))
Console.WriteLine("{0}: {1}", value.Timestamp, value.Value)
Next
End UsingWithout a number per page, ReadNodesHistory returns all values of the time range as a dictionary with the identifier of the node as the key:
IDictionary<OpcNodeId, IEnumerable<OpcHistoryValue>> history = client.ReadNodesHistory(
DateTime.UtcNow.AddHours(-1),
DateTime.UtcNow,
"ns=2;s=Machine/Speed",
"ns=2;s=Machine/Temperature");
foreach (var entry in history)
Console.WriteLine("{0}: {1} values", entry.Key, entry.Value.Count());Dim history As IDictionary(Of OpcNodeId, IEnumerable(Of OpcHistoryValue)) = _
client.ReadNodesHistory( _
DateTime.UtcNow.AddHours(-1), _
DateTime.UtcNow, _
"ns=2;s=Machine/Speed", _
"ns=2;s=Machine/Temperature")
For Each entry In history
Console.WriteLine("{0}: {1} values", entry.Key, entry.Value.Count())
NextRead aggregated values
For processed reading of historical data, you select one of several kinds of aggregation through OpcAggregateType.
Read the minimum value within a time range:
var minSpeed = client.ReadNodeHistoryProcessed(
startTime,
endTime,
OpcAggregateType.Minimum,
"ns=2;s=Machine/Job/Speed");Dim minSpeed = client.ReadNodeHistoryProcessed( _
startTime, _
endTime, _
OpcAggregateType.Minimum, _
"ns=2;s=Machine/Job/Speed")Read the average value within a time range:
var avgSpeed = client.ReadNodeHistoryProcessed(
startTime,
endTime,
OpcAggregateType.Average,
"ns=2;s=Machine/Job/Speed");Dim avgSpeed = client.ReadNodeHistoryProcessed( _
startTime, _
endTime, _
OpcAggregateType.Average, _
"ns=2;s=Machine/Job/Speed")Read the maximum value within a time range:
var maxSpeed = client.ReadNodeHistoryProcessed(
startTime,
endTime,
OpcAggregateType.Maximum,
"ns=2;s=Machine/Job/Speed");Dim maxSpeed = client.ReadNodeHistoryProcessed( _
startTime, _
endTime, _
OpcAggregateType.Maximum, _
"ns=2;s=Machine/Job/Speed")If you divide the values into intervals of equal length, the server returns one aggregated value per interval. You specify the length of the intervals in milliseconds. Without it, the server calculates one value over the entire time range:
Read the average value per hour within a time range:
// Read the average value of each hour within the time window.
var avgSpeedPerHour = client.ReadNodeHistoryProcessed(
startTime,
endTime,
OpcAggregateType.Average,
aggregateInterval: TimeSpan.FromHours(1).TotalMilliseconds,
"ns=2;s=Machine/Job/Speed");'Read the average value of each hour within the time window.
Dim avgSpeedPerHour = client.ReadNodeHistoryProcessed( _
startTime, _
endTime, _
OpcAggregateType.Average, _
aggregateInterval:=TimeSpan.FromHours(1).TotalMilliseconds, _
"ns=2;s=Machine/Job/Speed")Aggregated values carry only the source timestamp (SourceTimestamp), not a server timestamp.
Read values at specific times
When the client reads values at specific times, the server returns one value per point in time. If there is no recorded value at a point in time, the server usually determines the value from the neighboring values:
var times = new DateTime[] {
new DateTime(2017, 2, 16, 10, 0, 0),
new DateTime(2017, 2, 16, 12, 0, 0)
};
var speeds = client.ReadNodeHistoryAtTime(times, "ns=2;s=Machine/Job/Speed");
foreach (var value in speeds)
Console.WriteLine("{0}: {1}", value.Timestamp, value);Dim times = New DateTime() { _
New DateTime(2017, 2, 16, 10, 0, 0), _
New DateTime(2017, 2, 16, 12, 0, 0) _
}
Dim speeds = client.ReadNodeHistoryAtTime(times, "ns=2;s=Machine/Job/Speed")
For Each value In speeds
Console.WriteLine("{0}: {1}", value.Timestamp, value)
NextRead modified values
If a client has updated, replaced, or deleted historical values (section Modify historical data), a server can keep the previous values together with information about the modification. The ReadNodeHistoryModified method returns these values as instances of OpcModifiedHistoryValue. As with the other methods, the client can also read the values page by page:
var modifiedSpeeds = client.ReadNodeHistoryModified(
startTime,
endTime,
"ns=2;s=Machine/Job/Speed");
foreach (var value in modifiedSpeeds) {
Console.WriteLine(
"{0}: {1} ({2} at {3} by {4})",
value.Timestamp,
value,
value.ModificationType,
value.ModificationTime,
value.ModificationUserName);
}Dim modifiedSpeeds = client.ReadNodeHistoryModified( _
startTime, _
endTime, _
"ns=2;s=Machine/Job/Speed")
For Each value In modifiedSpeeds
Console.WriteLine( _
"{0}: {1} ({2} at {3} by {4})", _
value.Timestamp, _
value, _
value.ModificationType, _
value.ModificationTime, _
value.ModificationUserName)
NextAgainst a server that uses OpcNodeHistorian, ReadNodeHistoryModified and DeleteNodeHistoryModified refer to the timestamps of the values, not to the time of the modification.
If the server rejects the request for the node, ReadNodeHistoryProcessed, ReadNodeHistoryAtTime, and ReadNodeHistoryModified throw an OpcException with the status of the server, for example BadAggregateNotSupported. If the server accepts the node but returns no values, the result is empty.
Modify historical data
Types usedOpcClientOpcNodeIdOpcValueOpcStatusOpcStatusCollection
To modify historical data, the client passes the values as instances of OpcValue. Their SourceTimestamp defines the point in time of the value. The server returns an OpcStatus for each value. OPC UA defines the following rules:
CreateNodeHistoryadds values only at points in time for which no value exists yet.ReplaceNodeHistoryreplaces only existing values.UpdateNodeHistoryadds values or replaces existing values.
Add new values:
OpcStatusCollection results = client.CreateNodeHistory(
"ns=2;s=Machine/Job/Speed",
new OpcValue(1200, sourceTimestamp: new DateTime(2017, 2, 16, 10, 0, 0)),
new OpcValue(1300, sourceTimestamp: new DateTime(2017, 2, 16, 10, 5, 0)));
foreach (var result in results) {
if (result.IsBad)
Console.WriteLine("Failed to create value: {0}", result.Description);
}Dim results As OpcStatusCollection = client.CreateNodeHistory( _
"ns=2;s=Machine/Job/Speed", _
New OpcValue(1200, sourceTimestamp:=New DateTime(2017, 2, 16, 10, 0, 0)), _
New OpcValue(1300, sourceTimestamp:=New DateTime(2017, 2, 16, 10, 5, 0)))
For Each result In results
If result.IsBad Then
Console.WriteLine("Failed to create value: {0}", result.Description)
End If
NextReplace an existing value:
OpcStatus result = client.ReplaceNodeHistory(
"ns=2;s=Machine/Job/Speed",
new OpcValue(1250, sourceTimestamp: new DateTime(2017, 2, 16, 10, 0, 0)));Dim result As OpcStatus = client.ReplaceNodeHistory( _
"ns=2;s=Machine/Job/Speed", _
New OpcValue(1250, sourceTimestamp:=New DateTime(2017, 2, 16, 10, 0, 0)))Add or replace a value:
OpcStatus result = client.UpdateNodeHistory(
"ns=2;s=Machine/Job/Speed",
new OpcValue(1250, sourceTimestamp: new DateTime(2017, 2, 16, 10, 0, 0)));Dim result As OpcStatus = client.UpdateNodeHistory( _
"ns=2;s=Machine/Job/Speed", _
New OpcValue(1250, sourceTimestamp:=New DateTime(2017, 2, 16, 10, 0, 0)))Delete the values of a time range or at specific times:
// Delete all values within the time window.
OpcStatusCollection results = client.DeleteNodeHistory(
"ns=2;s=Machine/Job/Speed",
startTime,
endTime);
// Delete the value of the timestamp specified.
OpcStatus result = client.DeleteNodeHistoryAtTime(
"ns=2;s=Machine/Job/Speed",
time: new DateTime(2017, 2, 16, 10, 5, 0));'Delete all values within the time window.
Dim results As OpcStatusCollection = client.DeleteNodeHistory( _
"ns=2;s=Machine/Job/Speed", _
startTime, _
endTime)
'Delete the value of the timestamp specified.
Dim result As OpcStatus = client.DeleteNodeHistoryAtTime( _
"ns=2;s=Machine/Job/Speed", _
time:=New DateTime(2017, 2, 16, 10, 5, 0))Read the event history
Types usedOpcClientOpcNodeIdOpcEventFilterOpcEventIOpcEventHistoryNavigator
Besides the values of variables, a server can also record past events. The client reads the event history through the node that reports the events, for example, the server node or an event node (page Events). An event filter determines which events and which fields the server returns. You create it the same way as when subscribing to events (section Filter events):
var severity = new OpcSimpleAttributeOperand(OpcEventTypes.Event, "Severity");
var filter = OpcFilter.Using(client)
.FromEvents(OpcEventTypes.AlarmCondition)
.Where(severity > OpcEventSeverity.Medium)
.Select();
IEnumerable<OpcEvent> events = client.ReadEventHistory(
startTime,
endTime,
"ns=2;s=Machine",
filter);
foreach (var historicalEvent in events)
Console.WriteLine("{0}: {1}", historicalEvent.Time, historicalEvent.Message);Dim severity = New OpcSimpleAttributeOperand(OpcEventTypes.Event, "Severity")
Dim filter = OpcFilter.Using(client) _
.FromEvents(OpcEventTypes.AlarmCondition) _
.Where(severity > OpcEventSeverity.Medium) _
.Select()
Dim events As IEnumerable(Of OpcEvent) = client.ReadEventHistory( _
startTime, _
endTime, _
"ns=2;s=Machine", _
filter)
For Each historicalEvent In events
Console.WriteLine("{0}: {1}", historicalEvent.Time, historicalEvent.Message)
NextRead the events page by page: As with historical values, a navigator returns the events page by page. Here, too, make sure that the navigator is closed, for example, with a using block:
using (var eventNavigator = client.ReadEventHistory(
startTime,
endTime,
numberOfEvents: 10,
"ns=2;s=Machine",
filter)) {
do {
foreach (var historicalEvent in eventNavigator)
Console.WriteLine("{0}: {1}", historicalEvent.Time, historicalEvent.Message);
} while (eventNavigator.MoveNextPage());
}Using eventNavigator = client.ReadEventHistory( _
startTime, _
endTime, _
numberOfEvents:=10, _
"ns=2;s=Machine", _
filter)
Do
For Each historicalEvent In eventNavigator
Console.WriteLine("{0}: {1}", historicalEvent.Time, historicalEvent.Message)
Next
Loop While eventNavigator.MoveNextPage()
End UsingA server is not required to provide past values or events.
Historical data of an OPC HDA server
The client reads the historical data of an OPC HDA server (Historical Data Access) with the same methods. The section Historical data (HDA) on the OPC Classic page describes the procedure.

