The aahClientManaged SDK is now isolated in ZB.MOM.WW.LmxOpcUa.Historian.Aveva and loaded via HistorianPluginLoader from a Historian/ subfolder only when enabled, removing the SDK from Host's compile-time and deploy-time surface. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
10 KiB
Historical Data Access
LmxNodeManager exposes OPC UA historical data access (HDA) through an abstract IHistorianDataSource interface (Historian/IHistorianDataSource.cs). The Wonderware Historian implementation lives in a separate assembly, ZB.MOM.WW.LmxOpcUa.Historian.Aveva, which is loaded at runtime only when Historian.Enabled=true. This keeps the aahClientManaged SDK out of the core Host so deployments that do not need history do not need the SDK installed.
Plugin Architecture
The historian surface is split across two assemblies:
ZB.MOM.WW.LmxOpcUa.Host(core) owns only OPC UA / BCL types:IHistorianDataSource-- the interfaceLmxNodeManagerdepends onHistorianEventDto-- SDK-free representation of a historian event recordHistorianAggregateMap-- maps OPC UA aggregate NodeIds to AnalogSummary column namesHistorianPluginLoader-- loads the plugin viaAssembly.LoadFromat startupHistoryContinuationPointManager-- paginates HistoryRead results
ZB.MOM.WW.LmxOpcUa.Historian.Aveva(plugin) owns everything SDK-bound:HistorianDataSource-- implementsIHistorianDataSource, wrapsaahClientManagedIHistorianConnectionFactory/SdkHistorianConnectionFactory-- opens and pollsArchestrA.HistorianAccessconnectionsAvevaHistorianPluginEntry.Create(HistorianConfiguration)-- the static factory invoked by the loader
The plugin assembly and its SDK dependencies (aahClientManaged.dll, aahClient.dll, aahClientCommon.dll, Historian.CBE.dll, Historian.DPAPI.dll, ArchestrA.CloudHistorian.Contract.dll) deploy to a Historian/ subfolder next to ZB.MOM.WW.LmxOpcUa.Host.exe. See Service Hosting for the full layout and deployment matrix.
Plugin Loading
When the service starts with Historian.Enabled=true, OpcUaService calls HistorianPluginLoader.TryLoad(config). The loader:
- Probes
AppDomain.CurrentDomain.BaseDirectory\Historian\ZB.MOM.WW.LmxOpcUa.Historian.Aveva.dll. - Installs a one-shot
AppDomain.AssemblyResolvehandler that redirects anyaahClientManaged/aahClientCommon/Historian.*lookups to the same subfolder, so the CLR can resolve SDK dependencies when the plugin first JITs. - Calls the plugin's
AvevaHistorianPluginEntry.Create(HistorianConfiguration)via reflection and returns the resultingIHistorianDataSource. - On any failure (plugin missing, entry type not found, SDK assembly unresolvable, bad image), logs a warning with the expected plugin path and returns
null. The server starts normally andLmxNodeManagerreturnsBadHistoryOperationUnsupportedfor every history call.
Wonderware Historian SDK
The plugin 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\. Only the plugin project (src/ZB.MOM.WW.LmxOpcUa.Historian.Aveva/) references them at build time; the core Host project does not.
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, HistorianPluginLoader.TryLoad is not called, no plugin is loaded, and the node manager returns BadHistoryOperationUnsupported for history read requests. When Enabled is true but the plugin cannot be loaded (missing Historian/ subfolder, SDK assembly resolve failure, etc.), the server still starts and returns the same BadHistoryOperationUnsupported status with a warning in the log.
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 (in the plugin assembly) maintains a persistent connection to the Historian server via ArchestrA.HistorianAccess:
- Lazy connect -- The connection is established on the first query via
EnsureConnected(). - Connection reuse -- Subsequent queries reuse the same connection.
- Auto-reconnect -- On connection failure, the connection is disposed and re-established on the next query.
- Clean shutdown --
Dispose()closes the connection when the service stops.
The connection is opened with ReadOnly = true and ConnectionType = Process.
Raw Reads
IHistorianDataSource.ReadRawAsync (plugin implementation) uses a HistoryQuery to retrieve individual samples within a time range:
- Create a
HistoryQueryvia_connection.CreateHistoryQuery() - Configure
HistoryQueryArgswithTagNames,StartDateTime,EndDateTime, andRetrievalMode = Full - Iterate:
StartQuery->MoveNextloop ->EndQuery
Each result row is converted to an OPC UA DataValue:
QueryResult.Value(double) takes priority;QueryResult.StringValueis used as fallback for string-typed tags.SourceTimestampandServerTimestampare both set toQueryResult.StartDateTime.StatusCodeis mapped from theQueryResult.OpcQuality(UInt16) viaQualityMapper(the same OPC DA quality byte mapping used for live MXAccess data).
Aggregate Reads
IHistorianDataSource.ReadAggregateAsync (plugin implementation) uses an AnalogSummaryQuery to retrieve pre-computed aggregates:
- Create an
AnalogSummaryQueryvia_connection.CreateAnalogSummaryQuery() - Configure
AnalogSummaryQueryArgswithTagNames,StartDateTime,EndDateTime, andResolution(milliseconds) - Iterate the same
StartQuery->MoveNext->EndQuerypattern - 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
HistorianAggregateMap.MapAggregateToColumn (in the core Host assembly, so the node manager can validate aggregate support without requiring the plugin to be loaded) 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:
- 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.
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 result property via
MapAggregateToColumn. ReturnBadAggregateNotSupportedif unmapped. - Call
ReadAggregateAsyncwith the time range,ProcessingInterval, and property 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.