Files
lmxopcua/docs/HistoricalDataAccess.md
Joseph Doherty 41f0e9ec4c Migrate historian from SQL to aahClientManaged SDK and resolve all OPC UA Part 11 gaps
Replace direct SQL queries against Historian Runtime database with the Wonderware
Historian managed SDK (ArchestrA.HistorianAccess). Add HistoryServerCapabilities node,
AggregateFunctions folder, continuation points, ReadAtTime interpolation, ReturnBounds,
ReadModified rejection, HistoricalDataConfiguration per node, historical event access,
and client-side StandardDeviation aggregate support. Remove screenshot tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 16:38:00 -04:00

7.4 KiB

Historical Data Access

LmxNodeManager exposes OPC UA historical data access (HDA) by querying the Wonderware Historian via the aahClientManaged SDK. The HistorianDataSource class translates OPC UA history requests into SDK queries using the ArchestrA.HistorianAccess API, and the node manager overrides wire the results back into the OPC UA response.

Wonderware Historian SDK

The server uses the AVEVA Historian managed SDK (aahClientManaged.dll) to query historical data. The SDK provides a cursor-based query API through ArchestrA.HistorianAccess, replacing direct SQL queries against the Historian Runtime database. Two query types are used:

  • HistoryQuery -- Raw historical samples with timestamp, value (numeric or string), and OPC quality.
  • AnalogSummaryQuery -- Pre-computed aggregates with properties for Average, Minimum, Maximum, ValueCount, First, Last, StdDev, and more.

The SDK DLLs are located in lib/ and originate from C:\Program Files (x86)\Wonderware\Historian\.

Configuration

HistorianConfiguration controls the SDK connection:

public class HistorianConfiguration
{
    public bool Enabled { get; set; } = false;
    public string ServerName { get; set; } = "localhost";
    public bool IntegratedSecurity { get; set; } = true;
    public string? UserName { get; set; }
    public string? Password { get; set; }
    public int Port { get; set; } = 32568;
    public int CommandTimeoutSeconds { get; set; } = 30;
    public int MaxValuesPerRead { get; set; } = 10000;
}

When Enabled is false, the HistorianDataSource is not instantiated and the node manager returns BadHistoryOperationUnsupported for history read requests.

Connection Properties

Property Default Description
ServerName localhost Historian server hostname
IntegratedSecurity true Use Windows authentication
UserName null Username when IntegratedSecurity is false
Password null Password when IntegratedSecurity is false
Port 32568 Historian TCP port
CommandTimeoutSeconds 30 SDK packet timeout in seconds
MaxValuesPerRead 10000 Maximum values per history read request

Connection Lifecycle

HistorianDataSource maintains a persistent connection to the Historian server via ArchestrA.HistorianAccess:

  1. Lazy connect -- The connection is established on the first query via EnsureConnected().
  2. Connection reuse -- Subsequent queries reuse the same connection.
  3. Auto-reconnect -- On connection failure, the connection is disposed and re-established on the next query.
  4. Clean shutdown -- Dispose() closes the connection when the service stops.

The connection is opened with ReadOnly = true and ConnectionType = Process.

Raw Reads

HistorianDataSource.ReadRawAsync uses a HistoryQuery to retrieve individual samples within a time range:

  1. Create a HistoryQuery via _connection.CreateHistoryQuery()
  2. Configure HistoryQueryArgs with TagNames, StartDateTime, EndDateTime, and RetrievalMode = Full
  3. Iterate: StartQuery -> MoveNext loop -> EndQuery

Each result row is converted to an OPC UA DataValue:

  • QueryResult.Value (double) takes priority; QueryResult.StringValue is used as fallback for string-typed tags.
  • SourceTimestamp and ServerTimestamp are both set to QueryResult.StartDateTime.
  • StatusCode is mapped from the QueryResult.OpcQuality (UInt16) via QualityMapper (the same OPC DA quality byte mapping used for live MXAccess data).

Aggregate Reads

HistorianDataSource.ReadAggregateAsync uses an AnalogSummaryQuery to retrieve pre-computed aggregates:

  1. Create an AnalogSummaryQuery via _connection.CreateAnalogSummaryQuery()
  2. Configure AnalogSummaryQueryArgs with TagNames, StartDateTime, EndDateTime, and Resolution (milliseconds)
  3. Iterate the same StartQuery -> MoveNext -> EndQuery pattern
  4. Extract the requested aggregate from named properties on AnalogSummaryQueryResult

Null aggregate values return BadNoData status rather than Good with a null variant.

Quality Mapping

The Historian SDK returns standard OPC DA quality values in QueryResult.OpcQuality (UInt16). The low byte is passed through the shared QualityMapper pipeline (MapFromMxAccessQuality -> MapToOpcUaStatusCode), which maps the OPC DA quality families to OPC UA status codes:

OPC Quality Byte OPC DA Family OPC UA StatusCode
0-63 Bad Bad (with sub-code when an exact enum match exists)
64-191 Uncertain Uncertain (with sub-code when an exact enum match exists)
192+ Good Good (with sub-code when an exact enum match exists)

See Domain/QualityMapper.cs and Domain/Quality.cs for the full mapping table and sub-code definitions.

Aggregate Function Mapping

MapAggregateToColumn translates OPC UA aggregate NodeIds to AnalogSummaryQueryResult property names:

OPC UA Aggregate Result Property
AggregateFunction_Average Average
AggregateFunction_Minimum Minimum
AggregateFunction_Maximum Maximum
AggregateFunction_Count ValueCount
AggregateFunction_Start First
AggregateFunction_End Last
AggregateFunction_StandardDeviationPopulation StdDev

Unsupported aggregates return null, which causes the node manager to return BadAggregateNotSupported.

HistoryReadRawModified Override

LmxNodeManager overrides HistoryReadRawModified to handle raw history read requests:

  1. Resolve the NodeHandle to a tag reference via _nodeIdToTagReference. Return BadNodeIdUnknown if not found.
  2. Check that _historianDataSource is not null. Return BadHistoryOperationUnsupported if historian is disabled.
  3. Call ReadRawAsync with the time range and NumValuesPerNode from the ReadRawModifiedDetails.
  4. Pack the resulting DataValue list into a HistoryData object and wrap it in an ExtensionObject for the HistoryReadResult.

HistoryReadProcessed Override

HistoryReadProcessed handles aggregate history requests with additional validation:

  1. Resolve the node and check historian availability (same as raw).
  2. Validate that AggregateType is present in the ReadProcessedDetails. Return BadAggregateListMismatch if empty.
  3. Map the requested aggregate to a result property via MapAggregateToColumn. Return BadAggregateNotSupported if unmapped.
  4. Call ReadAggregateAsync with the time range, ProcessingInterval, and property name.
  5. Return results in the same HistoryData / ExtensionObject format.

Historizing Flag and AccessLevel

During variable node creation in CreateAttributeVariable, attributes with IsHistorized == true receive two additional settings:

if (attr.IsHistorized)
    accessLevel |= AccessLevels.HistoryRead;
variable.Historizing = attr.IsHistorized;
  • Historizing = true -- Tells OPC UA clients that this node has historical data available.
  • AccessLevels.HistoryRead -- Enables the HistoryRead access bit on the node, which the OPC UA stack checks before routing history requests to the node manager override. Nodes without this bit set will be rejected by the framework before reaching HistoryReadRawModified or HistoryReadProcessed.

The IsHistorized flag originates from the Galaxy repository database query, which checks whether the attribute has Historian logging configured.