- Remove ConfigUserAuthenticationProvider and Users property — LDAP is the only auth mechanism - Fix historian quality mapping to use existing QualityMapper (OPC DA quality bytes, not custom mapping) - Add AppRoles constants, unify HasWritePermission/HasAlarmAckPermission into shared HasRole helper - Hoist write permission check out of per-item loop, eliminate redundant _ldapRolesEnabled field - Update docs (Configuration.md, Security.md, OpcUaServer.md, HistoricalDataAccess.md) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
6.7 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 columnsDateTime,Value(numeric),vValue(string), andQuality.Runtime.dbo.AnalogSummaryHistory-- Pre-computed aggregates bucketed bywwResolution(milliseconds), with columns likeAverage,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:
Valuecolumn (double) takes priority overvValue(string). If both are null, the value is null.SourceTimestampandServerTimestampare both set to theDateTimecolumn.StatusCodeis mapped from the HistorianQualitybyte viaQualityMapper(the same OPC DA quality byte mapping used for live MXAccess data).
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
The Historian stores standard OPC DA quality bytes, the same format used by MXAccess at runtime. The quality byte is passed through the shared QualityMapper pipeline (MapFromMxAccessQuality → MapToOpcUaStatusCode), which maps the OPC DA quality families to OPC UA status codes:
| Historian 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 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:
- Resolve the
NodeHandleto a tag reference via_nodeIdToTagReference. ReturnBadNodeIdUnknownif not found. - Check that
_historianDataSourceis not null. ReturnBadHistoryOperationUnsupportedif historian is disabled. - Call
ReadRawAsyncwith the time range andNumValuesPerNodefrom theReadRawModifiedDetails. - Pack the resulting
DataValuelist into aHistoryDataobject and wrap it in anExtensionObjectfor theHistoryReadResult.
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:
- Resolve the node and check historian availability (same as raw).
- Validate that
AggregateTypeis present in theReadProcessedDetails. ReturnBadAggregateListMismatchif empty. - Map the requested aggregate to a Historian column via
MapAggregateToColumn. ReturnBadAggregateNotSupportedif unmapped. - Call
ReadAggregateAsyncwith the time range,ProcessingInterval, and column name. - Return results in the same
HistoryData/ExtensionObjectformat.
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 theHistoryReadaccess 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 reachingHistoryReadRawModifiedorHistoryReadProcessed.
The IsHistorized flag originates from the Galaxy repository database query, which checks whether the attribute has Historian logging configured.