Files
lmxopcua/docs/HistoricalDataAccess.md
Joseph Doherty 965e430f48 Add component-level documentation for all 14 server subsystems
Provides technical documentation covering OPC UA server, address space,
Galaxy repository, MXAccess bridge, data types, read/write, subscriptions,
alarms, historian, incremental sync, configuration, dashboard, service
hosting, and CLI tool. Updates README with component documentation table.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 15:47:59 -04:00

6.3 KiB

Historical Data Access

LmxNodeManager exposes OPC UA historical data access (HDA) by querying the Wonderware Historian Runtime database. The HistorianDataSource class translates OPC UA history requests into SQL queries against the Historian's History and AnalogSummaryHistory views, and the node manager overrides wire the results back into the OPC UA response.

Wonderware Historian Runtime Database

The Historian stores time-series data in a SQL Server database named Runtime. Two views are relevant:

  • Runtime.dbo.History -- Raw historical samples with columns DateTime, Value (numeric), vValue (string), and Quality.
  • Runtime.dbo.AnalogSummaryHistory -- Pre-computed aggregates bucketed by wwResolution (milliseconds), with columns like Average, Minimum, Maximum, ValueCount, First, Last, StdDev.

Both views require TagName in the WHERE clause. This is a Historian constraint -- the views are optimized for tag-scoped queries and do not support efficient cross-tag scans.

Configuration

HistorianConfiguration controls the historian connection:

public class HistorianConfiguration
{
    public bool Enabled { get; set; } = false;
    public string ConnectionString { get; set; } =
        "Server=localhost;Database=Runtime;Integrated Security=true;";
    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.

Raw Reads

HistorianDataSource.ReadRawAsync queries the History view for individual samples within a time range:

SELECT TOP (@MaxValues) DateTime, Value, vValue, Quality
FROM Runtime.dbo.History
WHERE TagName = @TagName
  AND DateTime >= @StartTime AND DateTime <= @EndTime
ORDER BY DateTime

The TOP clause is included only when maxValues > 0 (the OPC UA client specified NumValuesPerNode). Each row is converted to an OPC UA DataValue:

  • Value column (double) takes priority over vValue (string). If both are null, the value is null.
  • SourceTimestamp and ServerTimestamp are both set to the DateTime column.
  • StatusCode is mapped from the Historian Quality byte via MapQuality.

Aggregate Reads

HistorianDataSource.ReadAggregateAsync queries the AnalogSummaryHistory view for pre-computed aggregates:

SELECT StartDateTime, [{aggregateColumn}]
FROM Runtime.dbo.AnalogSummaryHistory
WHERE TagName = @TagName
  AND StartDateTime >= @StartTime AND StartDateTime <= @EndTime
  AND wwResolution = @Resolution
ORDER BY StartDateTime

The aggregateColumn is interpolated directly into the SQL (it comes from the controlled MapAggregateToColumn mapping, not from user input). The wwResolution parameter maps from the OPC UA ProcessingInterval in milliseconds.

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

Quality Mapping

MapQuality converts Wonderware Historian quality bytes to OPC UA status codes:

Historian Quality OPC UA StatusCode
0 Good
1 Bad
2-127 Bad
128+ Uncertain

This follows the Wonderware convention where quality 0 indicates a good sample, 1 indicates explicitly bad data, and values at or above 128 represent uncertain quality (e.g., interpolated or suspect values).

Aggregate Function Mapping

MapAggregateToColumn translates OPC UA aggregate NodeIds to Historian column names:

OPC UA Aggregate Historian Column
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.
var dataValues = _historianDataSource.ReadRawAsync(
    tagRef, details.StartTime, details.EndTime, maxValues)
    .GetAwaiter().GetResult();

var historyData = new HistoryData();
historyData.DataValues.AddRange(dataValues);
results[idx] = new HistoryReadResult
{
    StatusCode = StatusCodes.Good,
    HistoryData = new ExtensionObject(historyData)
};

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 Historian column via MapAggregateToColumn. Return BadAggregateNotSupported if unmapped.
  4. Call ReadAggregateAsync with the time range, ProcessingInterval, and column 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.