Skip to content

How-to guide

Historical data

Store historical values of nodes and provide them to clients, with your own history provider or the OpcNodeHistorian.

This content was machine translated and may contain errors. Read in German

How-to guide

Historical data ​

Types usedOpcServerOpcNodeManagerIOpcNodeOpcHistoryValueOpcHistoryModificationInfoOpcValueCollectionOpcStatusCollectionOpcDeleteHistoryOptionsOpcReadHistoryOptionsIOpcNodeHistoryProviderOpcNodeHistorianOpcNodeHistory<T>OpcModifiedNodeHistoryIOpcEventHistoryProvider

According to the OPC UA specification, every node of the Variable category can record the values of its Value attribute over time. Each time the Value attribute changes, the server stores the new value together with the timestamp of the Value attribute. These pairs of value and timestamp are called historical data. The server decides where it stores the data. The client uses the IsHistorizing attribute of a node to find out whether the server provides historical data for that node, that is, whether it stores value changes in a history. A client can read, update, replace, delete, or create historical data. Reading is the most common case.

The server manages historical data either directly in the node manager of the node, through the node historian in memory, or through a custom historian. In OPC UA, historical values always use their timestamp as the key. A timestamp is therefore unique among all historical values of a node. It identifies exactly one value and its quality information, that is, its status information. Historical data stored this way is also divided into plain and modified historical data. From a database point of view, the modified data is a kind of changelog. With this changelog, you can process historical data that was valid before the original historical data was changed. It also makes every change to the historical data traceable. For example, if a historical value is replaced, the server stores the previous value in the modified history. The server also stores a historical value that is removed from the history in the modified history. In addition, the modified history stores the kind of change, the timestamp of the change, and the name of the user who made the change.

Enable recording ​

For a client to read the (modified) historical data of a node, the node must be a variable node. In addition, recording of historical data must be enabled and access to it must be granted:

With OpcNodeHistorian: The historian enables recording of historical data and grants access to it:

csharp
// "this" points to the Node-Manager of the node.
var machineIsRunningHistorian = new OpcNodeHistorian(this, machineIsRunningNode);
vbnet
'"Me" points to the Node-Manager of the node.
Dim machineIsRunningHistorian = New OpcNodeHistorian(Me, machineIsRunningNode)

Manually: Enable recording of historical data and grant access to it:

csharp
machineIsRunningNode.AccessLevel |= OpcAccessLevel.HistoryReadOrWrite;
machineIsRunningNode.UserAccessLevel |= OpcAccessLevel.HistoryReadOrWrite;
 
machineIsRunningNode.IsHistorizing = true;
vbnet
machineIsRunningNode.AccessLevel = machineIsRunningNode.AccessLevel Or OpcAccessLevel.HistoryReadOrWrite
machineIsRunningNode.UserAccessLevel = machineIsRunningNode.UserAccessLevel Or OpcAccessLevel.HistoryReadOrWrite

machineIsRunningNode.IsHistorizing = True

Record value changes automatically ​

To have the server monitor changes to the Value attribute of a variable node and transfer them to a store for historical values:

With OpcNodeHistorian: Configure the historian to update the history automatically:

csharp
machineIsRunningHistorian.AutoUpdateHistory = true;
vbnet
machineIsRunningHistorian.AutoUpdateHistory = True

Manually: Subscribe to the BeforeApplyChanges event of the variable node to monitor changes to the Value attribute:

csharp
machineIsRunningNode.BeforeApplyChanges += HandleBeforeApplyChanges;
...
private void HandleBeforeApplyChanges(object sender, EventArgs e)
{
    // Update (modified) Node History here.
}
vbnet
machineIsRunningNode.BeforeApplyChanges = AddressOf HandleBeforeApplyChanges;
...
Private Sub HandleBeforeApplyChanges(ByVal sender As Object, ByVal e As EventArgs)
    'Update (modified) Node History here.
End Sub

Answer read requests ​

To provide the historical data to the client:

Use a history provider: If you use an IOpcNodeHistoryProvider such as the OpcNodeHistorian, register it with the server through the node manager:

csharp
protected override IOpcNodeHistoryProvider RetrieveNodeHistoryProvider(IOpcNode node)
{
    if (node == machineIsRunningNode)
        return machineIsRunningHistorian;
 
    return base.RetrieveNodeHistoryProvider(node);
}
vbnet
Protected Overrides Function RetrieveNodeHistoryProvider( _
        ByVal node As IOpcNode) As IOpcNodeHistoryProvider
    If node Is machineIsRunningNode Then
        Return machineIsRunningHistorian
    End If

    Return MyBase.RetrieveNodeHistoryProvider(node)
End Function

Use your own history provider: A custom IOpcNodeHistoryProvider answers the request with its ReadHistory method. If the timeFlowsBackward parameter is set, the client expects the values in reverse chronological order. The maxCount parameter states the maximum number of values the client requests (null if it sets no limit). The server delivers the values to the client page by page. The method can therefore also provide them gradually (for example, with yield return):

csharp
public IEnumerable<OpcHistoryValue> ReadHistory(
        OpcContext context,
        DateTime? startTime,
        DateTime? endTime,
        bool timeFlowsBackward,
        uint? maxCount,
        OpcReadHistoryOptions options)
{
    // Read (modified) Node History here.
}
vbnet
Public Function ReadHistory( _
        ByVal context As OpcContext, _
        ByVal startTime As DateTime?, _
        ByVal endTime As DateTime?, _
        ByVal timeFlowsBackward As Boolean, _
        ByVal maxCount As UInteger?, _
        ByVal options As OpcReadHistoryOptions) As IEnumerable(Of OpcHistoryValue) _
        Implements IOpcNodeHistoryProvider.ReadHistory
    'Read (modified) Node History here.
End Function

As of version 2.59, the server calculates values at specific times and aggregated values from the bounding values of the requested time range. Raw reads with ReturnBounds also return these bounding values. Therefore also return the value directly before and the value directly after the requested time range; otherwise the client receives BadNoData.

Manage the history in the node manager: If the node manager handles the history of its nodes itself, implement the ReadHistory method:

csharp
protected override IEnumerable<OpcHistoryValue> ReadHistory(
        OpcContext context,
        IOpcNode node,
        DateTime? startTime,
        DateTime? endTime,
        bool timeFlowsBackward,
        uint? maxCount,
        OpcReadHistoryOptions options)
{
    // Read (modified) Node History here.
}
vbnet
Protected Overrides Function ReadHistory( _
        ByVal context As OpcContext, _
        ByVal node As IOpcNode, _
        ByVal startTime As DateTime?, _
        ByVal endTime As DateTime?, _
        ByVal timeFlowsBackward As Boolean, _
        ByVal maxCount As UInteger?, _
        ByVal options As OpcReadHistoryOptions) As IEnumerable(Of OpcHistoryValue)
    'Read (modified) Node History here.
End Function

Aggregated values and values at specific times ​

Types usedOpcNodeManagerIOpcNodeHistoryProviderOpcNodeHistorianOpcAggregateTypeOpcHistoryValue

A server that provides the raw values of a node also answers requests for aggregated values and for values at specific times, without any further code. This applies to an IOpcNodeHistoryProvider, the OpcNodeHistorian, and the ReadHistory method of the node manager. The OPC UA .NET SDK reads the raw values of the requested time range through the same ReadHistory method and calculates the result itself:

  • Aggregated values (Processed): For each interval of the time range, the server calculates the requested aggregate, for example, average, minimum, maximum, or count (see OpcAggregateType). If a client requests an aggregate that the server does not support, it receives the status BadAggregateNotSupported for this node.
  • Values at specific times (AtTime): If a raw value exists at the requested time, the server returns it. If the time lies between two raw values, the server interpolates linearly between them. After the last raw value, it returns the last value. If no raw value exists before the time, the status is BadNoData. If the server skipped raw values with a bad status during the calculation, it marks the result as uncertain (UncertainDataSubNormal).

So you only implement reading the raw values, as shown in Answer read requests. A client then requests the calculated values like this, for example (see also Historical data in the client chapter):

csharp
DateTime endTime = DateTime.UtcNow;
DateTime startTime = endTime.AddHours(-8);
 
var averagePerHour = client.ReadNodeHistoryProcessed(
        startTime,
        endTime,
        OpcAggregateType.Average,
        aggregateInterval: 3600000,
        nodeId: "ns=2;s=Machine/Temperature");
 
OpcHistoryValue atNoon = client.ReadNodeHistoryAtTime(
        time: DateTime.UtcNow.Date.AddHours(12),
        nodeId: "ns=2;s=Machine/Temperature");
vbnet
Dim endTime As DateTime = DateTime.UtcNow
Dim startTime As DateTime = endTime.AddHours(-8)

Dim averagePerHour = client.ReadNodeHistoryProcessed( _
        startTime, _
        endTime, _
        OpcAggregateType.Average, _
        aggregateInterval:=3600000, _
        nodeId:="ns=2;s=Machine/Temperature")

Dim atNoon As OpcHistoryValue = client.ReadNodeHistoryAtTime( _
        time:=DateTime.UtcNow.Date.AddHours(12), _
        nodeId:="ns=2;s=Machine/Temperature")

Insert values ​

If a client wants to create historical data of a node, the server must store the new values both in the history and in the modified history:

Use a history provider: If you use an IOpcNodeHistoryProvider such as the OpcNodeHistorian, register it with the server through the node manager:

csharp
protected override IOpcNodeHistoryProvider RetrieveNodeHistoryProvider(IOpcNode node)
{
    if (node == machineIsRunningNode)
        return machineIsRunningHistorian;
 
    return base.RetrieveNodeHistoryProvider(node);
}
vbnet
Protected Overrides Function RetrieveNodeHistoryProvider( _
        ByVal node As IOpcNode) As IOpcNodeHistoryProvider
    If node Is machineIsRunningNode Then
        Return machineIsRunningHistorian
    End If

    Return MyBase.RetrieveNodeHistoryProvider(node)
End Function

Use your own history provider: A custom IOpcNodeHistoryProvider uses its CreateHistory method for this:

csharp
public OpcStatusCollection CreateHistory(
        OpcContext context,
        OpcHistoryModificationInfo modificationInfo,
        OpcValueCollection values)
{
    // Create (modified) Node History here.
}
vbnet
Public Function CreateHistory( _
        ByVal context As OpcContext, _
        ByVal modificationInfo As OpcHistoryModificationInfo, _
        ByVal values As OpcValueCollection) As OpcStatusCollection
    'Create (modified) Node History here.
End Function

Manage the history in the node manager: If the node manager handles the history of its nodes itself, implement the CreateHistory method:

csharp
protected override OpcStatusCollection CreateHistory(
        IOpcNode node,
        OpcHistoryModificationInfo modificationInfo,
        OpcValueCollection values)
{
    // Create (modified) Node History here.
}
vbnet
Protected Overrides Function CreateHistory( _
        ByVal node As IOpcNode, _
        ByVal modificationInfo As OpcHistoryModificationInfo, _
        ByVal values As OpcValueCollection) As OpcStatusCollection
    'Create (modified) Node History here.
End Function

Delete values ​

If a client wants to delete historical data of a node, the server must move the values to delete into the modified history and delete them from the underlying history. The server can remove modified history values that should be deleted directly from the modified history:

Use a history provider: If you use an IOpcNodeHistoryProvider such as the OpcNodeHistorian, register it with the server through the node manager:

csharp
protected override IOpcNodeHistoryProvider RetrieveNodeHistoryProvider(IOpcNode node)
{
    if (node == machineIsRunningNode)
        return machineIsRunningHistorian;
 
    return base.RetrieveNodeHistoryProvider(node);
}
vbnet
Protected Overrides Function RetrieveNodeHistoryProvider( _
        ByVal node As IOpcNode) As IOpcNodeHistoryProvider
    If node Is machineIsRunningNode Then
        Return machineIsRunningHistorian
    End If

    Return MyBase.RetrieveNodeHistoryProvider(node)
End Function

Use your own history provider: A custom IOpcNodeHistoryProvider uses one of its DeleteHistory methods for this:

csharp
public OpcStatusCollection DeleteHistory(
        OpcContext context,
        OpcHistoryModificationInfo modificationInfo,
        IEnumerable<DateTime> times)
{
    // Delete Node History entries and add them to the modified Node History here.
}
 
public OpcStatusCollection DeleteHistory(
        OpcContext context,
        OpcHistoryModificationInfo modificationInfo,
        OpcValueCollection values)
{
    // Delete Node History entries and add them to the modified Node History here.
}
 
public OpcStatusCollection DeleteHistory(
        OpcContext context,
        OpcHistoryModificationInfo modificationInfo,
        DateTime? startTime,
        DateTime? endTime,
        OpcDeleteHistoryOptions options)
{
    // Delete Node History entries and add them to the modified Node History here.
}
vbnet
Public Function DeleteHistory( _
        ByVal context As OpcContext, _
        ByVal modificationInfo As OpcHistoryModificationInfo, _
        ByVal times As IEnumerable(Of DateTime)) As OpcStatusCollection
    'Delete Node History entries and add them to the modified Node History here.
End Function
 
Public Function DeleteHistory( _
        ByVal context As OpcContext, _
        ByVal modificationInfo As OpcHistoryModificationInfo, _
        ByVal values As OpcValueCollection) As OpcStatusCollection
    'Delete Node History entries and add them to the modified Node History here.
End Function
 
Public Function DeleteHistory( _
        ByVal context As OpcContext, _
        ByVal modificationInfo As OpcHistoryModificationInfo, _
        ByVal startTime As DateTime?, _
        ByVal endTime As DateTime?, _
        ByVal options As OpcDeleteHistoryOptions) As OpcStatusCollection
    'Delete Node History entries and add them to the modified Node History here.
End Function

Manage the history in the node manager: If the node manager handles the history of its nodes itself, implement the DeleteHistory methods:

csharp
protected override OpcStatusCollection DeleteHistory(
        IOpcNode node,
        OpcHistoryModificationInfo modificationInfo,
        IEnumerable<DateTime> times)
{
    // Delete Node History entries and add them to the modified Node History here.
}
 
protected override OpcStatusCollection DeleteHistory(
        IOpcNode node,
        OpcHistoryModificationInfo modificationInfo,
        OpcValueCollection values)
{
    // Delete Node History entries and add them to the modified Node History here.
}
 
protected override OpcStatusCollection DeleteHistory(
        IOpcNode node,
        OpcHistoryModificationInfo modificationInfo,
        DateTime? startTime,
        DateTime? endTime,
        OpcDeleteHistoryOptions options)
{
    // Delete Node History entries and add them to the modified Node History here.
}
vbnet
Protected Overrides Function DeleteHistory( _
        ByVal node As IOpcNode, _
        ByVal modificationInfo As OpcHistoryModificationInfo, _
        ByVal times As IEnumerable(Of DateTime)) As OpcStatusCollection
    'Delete Node History entries and add them to the modified Node History here.
End Function
 
Protected Overrides Function DeleteHistory( _
        ByVal node As IOpcNode, _
        ByVal modificationInfo As OpcHistoryModificationInfo, _
        ByVal values As OpcValueCollection) As OpcStatusCollection
    'Delete Node History entries and add them to the modified Node History here.
End Function
 
Protected Overrides Function DeleteHistory( _
        ByVal node As IOpcNode, _
        ByVal modificationInfo As OpcHistoryModificationInfo, _
        ByVal startTime As DateTime?, _
        ByVal endTime As DateTime?, _
        ByVal options As OpcDeleteHistoryOptions) As OpcStatusCollection
    'Delete Node History entries and add them to the modified Node History here.
End Function

Replace values ​

If a client wants to replace historical data of a node, the server must move the values to replace into the modified history and replace them in the underlying history:

Use a history provider: If you use an IOpcNodeHistoryProvider such as the OpcNodeHistorian, register it with the server through the node manager:

csharp
protected override IOpcNodeHistoryProvider RetrieveNodeHistoryProvider(IOpcNode node)
{
    if (node == machineIsRunningNode)
        return machineIsRunningHistorian;
 
    return base.RetrieveNodeHistoryProvider(node);
}
vbnet
Protected Overrides Function RetrieveNodeHistoryProvider( _
        ByVal node As IOpcNode) As IOpcNodeHistoryProvider
    If node Is machineIsRunningNode Then
        Return machineIsRunningHistorian
    End If

    Return MyBase.RetrieveNodeHistoryProvider(node)
End Function

Use your own history provider: A custom IOpcNodeHistoryProvider uses its ReplaceHistory method for this:

csharp
public OpcStatusCollection ReplaceHistory(
        OpcContext context,
        OpcHistoryModificationInfo modificationInfo,
        OpcValueCollection values)
{
    // Replace Node History entries and add them to the modified Node History here.
}
vbnet
Public Function ReplaceHistory( _
        ByVal context As OpcContext, _
        ByVal modificationInfo As OpcHistoryModificationInfo, _
        ByVal values As OpcValueCollection) As OpcStatusCollection
    'Replace Node History entries and add them to the modified Node History here.
End Function

Manage the history in the node manager: If the node manager handles the history of its nodes itself, implement the ReplaceHistory method:

csharp
protected override OpcStatusCollection ReplaceHistory(
        IOpcNode node,
        OpcHistoryModificationInfo modificationInfo,
        OpcValueCollection values)
{
    // Replace Node History entries and add them to the modified Node History here.
}
vbnet
Protected Overrides Function ReplaceHistory( _
        ByVal node As IOpcNode, _
        ByVal modificationInfo As OpcHistoryModificationInfo, _
        ByVal values As OpcValueCollection) As OpcStatusCollection
    'Replace Node History entries and add them to the modified Node History here.
End Function

Insert or replace values (Update) ​

A client can create historical data of a node if it does not exist yet, or replace it if it already exists. In OPC UA, this is called an update. The server must add entries that do not exist to the history and to the modified history. It must replace existing entries in the history and add them to the modified history:

Use a history provider: If you use an IOpcNodeHistoryProvider such as the OpcNodeHistorian, register it with the server through the node manager:

csharp
protected override IOpcNodeHistoryProvider RetrieveNodeHistoryProvider(IOpcNode node)
{
    if (node == machineIsRunningNode)
        return machineIsRunningHistorian;
 
    return base.RetrieveNodeHistoryProvider(node);
}
vbnet
Protected Overrides Function RetrieveNodeHistoryProvider( _
        ByVal node As IOpcNode) As IOpcNodeHistoryProvider
    If node Is machineIsRunningNode Then
        Return machineIsRunningHistorian
    End If

    Return MyBase.RetrieveNodeHistoryProvider(node)
End Function

Use your own history provider: A custom IOpcNodeHistoryProvider uses its UpdateHistory method for this:

csharp
public OpcStatusCollection UpdateHistory(
        OpcContext context,
        OpcHistoryModificationInfo modificationInfo,
        OpcValueCollection values)
{
    // Update (modified) Node History entries here.
}
vbnet
Public Function UpdateHistory( _
        ByVal context As OpcContext, _
        ByVal modificationInfo As OpcHistoryModificationInfo, _
        ByVal values As OpcValueCollection) As OpcStatusCollection
    'Update (modified) Node History entries here.
End Function

Manage the history in the node manager: If the node manager handles the history of its nodes itself, implement the UpdateHistory method:

csharp
protected override OpcStatusCollection UpdateHistory(
        IOpcNode node,
        OpcHistoryModificationInfo modificationInfo,
        OpcValueCollection values)
{
    // Update (modified) Node History entries here.
}
vbnet
Protected Overrides Function UpdateHistory( _
        ByVal node As IOpcNode, _
        ByVal modificationInfo As OpcHistoryModificationInfo, _
        ByVal values As OpcValueCollection) As OpcStatusCollection
    'Update (modified) Node History entries here.
End Function

Manage the history in memory ​

The OpcNodeHistory<T> class manages the data of the history in memory, and up to version 2.58 also the data of the modified history. As of version 2.59, the OpcModifiedNodeHistory class manages the modified history. The OpcNodeHistorian also uses these classes. The OpcNodeHistory<T> class provides several methods for the common scenarios of accessing historical data. Its constructors set the size of the history, that is, its capacity; this also applies to the OpcNodeHistorian. By default, the capacity is -1, that is, the history is unlimited. As of version 2.59, a capacity of 0 or less than -1 throws an ArgumentOutOfRangeException. This also applies to the modifiedCapacity parameter of the OpcNodeHistorian constructors. You can also preload the history and monitor it through several events.

Define a history depending on the kind of historical data:

Simple history: Use the OpcHistoryValue class as the type parameter:

csharp
var history = new OpcNodeHistory<OpcHistoryValue>();
vbnet
Dim history = New OpcNodeHistory(Of OpcHistoryValue)()

Modified history: As of version 2.59, use the OpcModifiedNodeHistory class. It holds entries of type OpcModifiedHistoryValue and also accepts several modifications of a value with the same timestamp. The entries are sorted by timestamp; for the same timestamp, the newest modification comes first. When the capacity is reached, the class removes the oldest modification. The OpcNodeHistorian.ModifiedHistory property is of this type. Up to version 2.58, use OpcNodeHistory<T> with the type parameter OpcModifiedHistoryValue:

csharp
var modifiedHistory = new OpcNodeHistory<OpcModifiedHistoryValue>();
vbnet
Dim modifiedHistory = New OpcNodeHistory(Of OpcModifiedHistoryValue)()

Scenarios with the in-memory node history ​

With the OpcNodeHistory<T> class, you implement the common history scenarios Read, Create, Delete, Replace, and Update as follows:

Scenario: Create History:

csharp
var results = OpcStatusCollection.Create(OpcStatusCode.Good, values.Count);
 
for (int index = 0; index < values.Count; index++) {
    var result = results[index];
    var value = OpcHistoryValue.Create(values[index]);
 
    if (MatchesValueType(value)) {
        if (history.Contains(value.Timestamp)) {
            result.Update(OpcStatusCode.BadEntryExists);
        }
        else {
            history.Add(value);
 
            var modifiedValue = value.CreateModified(modificationInfo);
            modifiedHistory.Add(modifiedValue);
 
            result.Update(OpcStatusCode.GoodEntryInserted);
        }
    }
    else {
        result.Update(OpcStatusCode.BadTypeMismatch);
    }
}
 
return results;
vbnet
Dim results = OpcStatusCollection.Create(OpcStatusCode.Good, values.Count)

For index As Integer = 0 To values.Count - 1
    Dim result = results(index)
    Dim value = OpcHistoryValue.Create(values(index))

    If MatchesValueType(value) Then
        If history.Contains(value.Timestamp) Then
            result.Update(OpcStatusCode.BadEntryExists)
        Else
            history.Add(value)

            Dim modifiedValue = value.CreateModified(modificationInfo)
            modifiedHistory.Add(modifiedValue)

            result.Update(OpcStatusCode.GoodEntryInserted)
        End If
    Else
        result.Update(OpcStatusCode.BadTypeMismatch)
    End If
Next

Return results

Scenario: Delete History:

By timestamp:

csharp
var results = OpcStatusCollection.Create(OpcStatusCode.Good, times.Count());
 
int index = 0;
 
foreach (var time in times) {
    var result = results[index++];
 
    if (this.history.Contains(time)) {
        var value = this.history[time];
        this.history.RemoveAt(time);
 
        var modifiedValue = value.CreateModified(modificationInfo);
        this.modifiedHistory.Add(modifiedValue);
    }
    else {
        result.Update(OpcStatusCode.BadNoEntryExists);
    }
}
 
return results;
vbnet
Dim results = OpcStatusCollection.Create(OpcStatusCode.Good, times.Count())

Dim index As Integer = 0

For Each time In times
    Dim result = results(Math.Min(System.Threading.Interlocked.Increment(index), index - 1))

    If Me.history.Contains(time) Then
        Dim value = Me.history(time)
        Me.history.RemoveAt(time)

        Dim modifiedValue = value.CreateModified(modificationInfo)
        Me.modifiedHistory.Add(modifiedValue)
    Else
        result.Update(OpcStatusCode.BadNoEntryExists)
    End If
Next

Return results

By values:

csharp
var results = OpcStatusCollection.Create(OpcStatusCode.Good, values.Count);
 
for (int index = 0; index < values.Count; index++) {
    var timestamp = OpcHistoryValue.Create(values[index]).Timestamp;
    var result = results[index];
 
    if (history.Contains(timestamp)) {
        var value = history[timestamp];
        history.RemoveAt(timestamp);
 
        var modifiedValue = value.CreateModified(modificationInfo);
        modifiedHistory.Add(modifiedValue);
    }
    else {
        result.Update(OpcStatusCode.BadNoEntryExists);
    }
}
 
return results;
vbnet
Dim results = OpcStatusCollection.Create(OpcStatusCode.Good, values.Count)

For index As Integer = 0 To values.Count - 1
    Dim timestamp = OpcHistoryValue.Create(values(index)).Timestamp
    Dim result = results(index)

    If history.Contains(timestamp) Then
        Dim value = history(timestamp)
        history.RemoveAt(timestamp)

        Dim modifiedValue = value.CreateModified(modificationInfo)
        modifiedHistory.Add(modifiedValue)
    Else
        result.Update(OpcStatusCode.BadNoEntryExists)
    End If
Next

Return results

By time range:

csharp
var results = new OpcStatusCollection();
 
bool isModified = (options & OpcDeleteHistoryOptions.Modified)
        == OpcDeleteHistoryOptions.Modified;
 
if (isModified) {
    modifiedHistory.RemoveRange(startTime, endTime);
}
else {
    var values = history.Enumerate(startTime, endTime).ToArray();
    history.RemoveRange(startTime, endTime);
 
    for (int index = 0; index < values.Length; index++) {
        var value = values[index];
        modifiedHistory.Add(value.CreateModified(modificationInfo));
 
        results.Add(OpcStatusCode.Good);
    }
}
 
return results;
vbnet
Dim results = New OpcStatusCollection()
Dim isModified As Boolean = (options And OpcDeleteHistoryOptions.Modified) _
        = OpcDeleteHistoryOptions.Modified

If isModified Then
    modifiedHistory.RemoveRange(startTime, endTime)
Else
    Dim values = history.Enumerate(startTime, endTime).ToArray()
    history.RemoveRange(startTime, endTime)

    For index As Integer = 0 To values.Length - 1
        Dim value = values(index)
        modifiedHistory.Add(value.CreateModified(modificationInfo))

        results.Add(OpcStatusCode.Good)
    Next
End If

Return results

Scenario: Replace History:

csharp
var results = OpcStatusCollection.Create(OpcStatusCode.Good, values.Count);
 
for (int index = 0; index < values.Count; index++) {
    var result = results[index];
    var value = OpcHistoryValue.Create(values[index]);
 
    if (this.MatchesNodeValueType(value)) {
        if (this.history.Contains(value.Timestamp)) {
            var oldValue = this.history[value.Timestamp];
            history.Replace(value);
 
            var modifiedValue = oldValue.CreateModified(modificationInfo);
            modifiedHistory.Add(modifiedValue);
 
            result.Update(OpcStatusCode.GoodEntryReplaced);
        }
        else {
            result.Update(OpcStatusCode.BadNoEntryExists);
        }
    }
    else {
        result.Update(OpcStatusCode.BadTypeMismatch);
    }
}
 
return results;
vbnet
Dim results = OpcStatusCollection.Create(OpcStatusCode.Good, values.Count)

For index As Integer = 0 To values.Count - 1
    Dim result = results(index)
    Dim value = OpcHistoryValue.Create(values(index))

    If Me.MatchesNodeValueType(value) Then
        If Me.history.Contains(value.Timestamp) Then
            Dim oldValue = Me.history(value.Timestamp)
            history.Replace(value)

            Dim modifiedValue = oldValue.CreateModified(modificationInfo)
            modifiedHistory.Add(modifiedValue)

            result.Update(OpcStatusCode.GoodEntryReplaced)
        Else
            result.Update(OpcStatusCode.BadNoEntryExists)
        End If
    Else
        result.Update(OpcStatusCode.BadTypeMismatch)
    End If
Next

Return results

Scenario: Update History:

csharp
var results = OpcStatusCollection.Create(OpcStatusCode.Good, values.Count);
 
for (int index = 0; index < values.Count; index++) {
    var result = results[index];
    var value = OpcHistoryValue.Create(values[index]);
 
    if (MatchesValueType(value)) {
        if (history.Contains(value.Timestamp)) {
            var oldValue = this.history[value.Timestamp];
            history.Replace(value);
 
            var modifiedValue = oldValue.CreateModified(modificationInfo);
            modifiedHistory.Add(modifiedValue);
 
            result.Update(OpcStatusCode.GoodEntryReplaced);
        }
        else {
            history.Add(value);
 
            var modifiedValue = value.CreateModified(modificationInfo);
            modifiedHistory.Add(modifiedValue);
 
            result.Update(OpcStatusCode.GoodEntryInserted);
        }
    }
    else {
        result.Update(OpcStatusCode.BadTypeMismatch);
    }
}
 
return results;
vbnet
Dim results = OpcStatusCollection.Create(OpcStatusCode.Good, values.Count)

For index As Integer = 0 To values.Count - 1
    Dim result = results(index)
    Dim value = OpcHistoryValue.Create(values(index))

    If MatchesValueType(value) Then
        If history.Contains(value.Timestamp) Then
            Dim oldValue = Me.history(value.Timestamp)
            history.Replace(value)

            Dim modifiedValue = oldValue.CreateModified(modificationInfo)
            modifiedHistory.Add(modifiedValue)

            result.Update(OpcStatusCode.GoodEntryReplaced)
        Else
            history.Add(value)

            Dim modifiedValue = value.CreateModified(modificationInfo)            
            modifiedHistory.Add(modifiedValue)

            result.Update(OpcStatusCode.GoodEntryInserted)
        End If
    Else
        result.Update(OpcStatusCode.BadTypeMismatch)
    End If
Next

Return results

Scenario: Read History:

csharp
bool isModified = (options & OpcReadHistoryOptions.Modified)
        == OpcReadHistoryOptions.Modified;
 
OpcHistoryValue[] values;
 
if (isModified) {
    values = modifiedHistory
            .Enumerate(startTime, endTime)
            .Cast<OpcHistoryValue>()
            .ToArray();
}
else {
    values = history
            .Enumerate(startTime, endTime)
            .ToArray();
}
 
// Return the values in reverse order in case the time flows backward.
if (timeFlowsBackward)
    Array.Reverse(values);
 
return values;
vbnet
Dim isModified As Boolean = (options And OpcReadHistoryOptions.Modified) _
        = OpcReadHistoryOptions.Modified

Dim values As OpcHistoryValue()

If isModified Then
    values = modifiedHistory _
            .Enumerate(startTime, endTime) _
            .Cast(Of OpcHistoryValue)() _
            .ToArray()
Else
    values = history _
            .Enumerate(startTime, endTime) _
            .ToArray()
End If

'Return the values in reverse order in case the time flows backward.
If timeFlowsBackward Then
    Array.Reverse(values)
End If

Return values

Provide the event history ​

Types usedOpcNodeManagerOpcObjectNodeIOpcEventHistoryProviderOpcEventOpcEventFilter

Besides the values of a variable, a server can also provide the events of a node over time. Clients query them with a HistoryRead for events. To do this, set the IsHistorizingEvents property on the node that reports the events. This is usually an OpcObjectNode (see Provide events on a node). Also provide an implementation of the IOpcEventHistoryProvider interface. Its ReadEventHistory method returns the events of a time range. The timeFlowsBackward and maxCount parameters have the same meaning as when reading values. If a bound of the time range is not specified, the parameter contains null or DateTime.MinValue. From the returned events, the SDK takes only the fields the client requested (SelectClause of the filter). It does not evaluate a condition of the filter (WhereClause). A simple in-memory event history can look like this:

csharp
public class MachineEventHistory : IOpcEventHistoryProvider
{
    private readonly List<OpcEvent> events = new List<OpcEvent>();
 
    public MachineEventHistory(OpcObjectNode node)
    {
        this.Node = node;
        this.Node.IsHistorizingEvents = true;
    }
 
    public OpcObjectNode Node { get; }
 
    public void Add(OpcEvent historyEvent)
    {
        lock (this.events)
            this.events.Add(historyEvent);
    }
 
    public IEnumerable<OpcEvent> ReadEventHistory(
            OpcContext context,
            OpcEventFilter filter,
            DateTime? startTime,
            DateTime? endTime,
            bool timeFlowsBackward,
            uint? maxCount)
    {
        // In case the time flows backward the start time is the later one.
        DateTime? from = timeFlowsBackward ? endTime : startTime;
        DateTime? to = timeFlowsBackward ? startTime : endTime;
 
        var result = new List<OpcEvent>();
 
        lock (this.events) {
            foreach (var item in this.events) {
                if (IsSet(from) && item.Time < from)
                    continue;
 
                if (IsSet(to) && item.Time > to)
                    continue;
 
                result.Add(item);
            }
        }
 
        if (timeFlowsBackward)
            result.Reverse();
 
        if (maxCount != null && result.Count > maxCount.Value)
            result.RemoveRange((int)maxCount.Value, result.Count - (int)maxCount.Value);
 
        return result;
    }
 
    private static bool IsSet(DateTime? time)
    {
        return time != null && time != DateTime.MinValue;
    }
}
vbnet
Public Class MachineEventHistory
    Implements IOpcEventHistoryProvider

    Private ReadOnly events As New List(Of OpcEvent)()

    Public Sub New(ByVal node As OpcObjectNode)
        Me.Node = node
        Me.Node.IsHistorizingEvents = True
    End Sub

    Public ReadOnly Property Node As OpcObjectNode Implements IOpcEventHistoryProvider.Node

    Public Sub Add(ByVal historyEvent As OpcEvent)
        SyncLock Me.events
            Me.events.Add(historyEvent)
        End SyncLock
    End Sub

    Public Function ReadEventHistory( _
            ByVal context As OpcContext, _
            ByVal filter As OpcEventFilter, _
            ByVal startTime As DateTime?, _
            ByVal endTime As DateTime?, _
            ByVal timeFlowsBackward As Boolean, _
            ByVal maxCount As UInteger?) As IEnumerable(Of OpcEvent) _
            Implements IOpcEventHistoryProvider.ReadEventHistory
        'In case the time flows backward the start time is the later one.
        Dim from As DateTime? = If(timeFlowsBackward, endTime, startTime)
        Dim [to] As DateTime? = If(timeFlowsBackward, startTime, endTime)

        Dim result = New List(Of OpcEvent)()

        SyncLock Me.events
            For Each item In Me.events
                If IsSet(from) AndAlso item.Time < from.Value Then Continue For
                If IsSet([to]) AndAlso item.Time > [to].Value Then Continue For

                result.Add(item)
            Next
        End SyncLock

        If timeFlowsBackward Then
            result.Reverse()
        End If

        If maxCount.HasValue AndAlso result.Count > maxCount.Value Then
            result.RemoveRange(CInt(maxCount.Value), result.Count - CInt(maxCount.Value))
        End If

        Return result
    End Function

    Private Shared Function IsSet(ByVal time As DateTime?) As Boolean
        Return time.HasValue AndAlso time.Value <> DateTime.MinValue
    End Function
End Class

Register the event history with the server through the node manager. If the node itself implements the IOpcEventHistoryProvider interface, the server uses it even without this method:

csharp
protected override IOpcEventHistoryProvider RetrieveEventHistoryProvider(IOpcNode node)
{
    if (node == this.machineEventHistory.Node)
        return this.machineEventHistory;
 
    return base.RetrieveEventHistoryProvider(node);
}
vbnet
Protected Overrides Function RetrieveEventHistoryProvider( _
        ByVal node As IOpcNode) As IOpcEventHistoryProvider
    If node Is Me.machineEventHistory.Node Then
        Return Me.machineEventHistory
    End If

    Return MyBase.RetrieveEventHistoryProvider(node)
End Function

To record an event, create a snapshot with the CreateEvent method of the event node when you report it, and store the snapshot in the history:

csharp
activatedEvent.Message = "Machine started";
activatedEvent.ReportEvent(this.SystemContext);
 
// Stores a snapshot of the event in the event history.
this.machineEventHistory.Add(activatedEvent.CreateEvent(this.SystemContext));
vbnet
activatedEvent.Message = "Machine started"
activatedEvent.ReportEvent(Me.SystemContext)

'Stores a snapshot of the event in the event history.
Me.machineEventHistory.Add(activatedEvent.CreateEvent(Me.SystemContext))

The Read the event history section in the client chapter describes how a client reads the event history.