Extract historian into a runtime-loaded plugin so hosts without the Wonderware SDK can run with Historian.Enabled=false

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>
This commit is contained in:
Joseph Doherty
2026-04-12 15:16:07 -04:00
parent 9e1a180ce3
commit 9b42b61eb6
40 changed files with 739 additions and 15855 deletions

View File

@@ -1,11 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ZB.MOM.WW.LmxOpcUa.Client.UI.Controls.DateTimePicker">
<StackPanel Orientation="Horizontal" Spacing="6">
<CalendarDatePicker Name="DatePart" Width="130" />
<TimePicker Name="TimePart"
ClockIdentifier="24HourClock"
MinuteIncrement="1"
Width="100" />
</StackPanel>
</UserControl>

View File

@@ -1,169 +0,0 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace ZB.MOM.WW.LmxOpcUa.Client.UI.Controls;
/// <summary>
/// A combined date + time picker that exposes a single DateTimeOffset value.
/// Bridges between CalendarDatePicker (DateTime?) and TimePicker (TimeSpan?)
/// and the public SelectedDateTime (DateTimeOffset?) property.
/// </summary>
public partial class DateTimePicker : UserControl
{
public static readonly StyledProperty<DateTimeOffset?> SelectedDateTimeProperty =
AvaloniaProperty.Register<DateTimePicker, DateTimeOffset?>(
nameof(SelectedDateTime), defaultValue: DateTimeOffset.Now);
public static readonly StyledProperty<DateTimeOffset?> MinDateTimeProperty =
AvaloniaProperty.Register<DateTimePicker, DateTimeOffset?>(
nameof(MinDateTime));
public static readonly StyledProperty<DateTimeOffset?> MaxDateTimeProperty =
AvaloniaProperty.Register<DateTimePicker, DateTimeOffset?>(
nameof(MaxDateTime));
private bool _isUpdating;
public DateTimePicker()
{
InitializeComponent();
}
/// <summary>The combined date and time value.</summary>
public DateTimeOffset? SelectedDateTime
{
get => GetValue(SelectedDateTimeProperty);
set => SetValue(SelectedDateTimeProperty, value);
}
/// <summary>Optional minimum allowed date/time.</summary>
public DateTimeOffset? MinDateTime
{
get => GetValue(MinDateTimeProperty);
set => SetValue(MinDateTimeProperty, value);
}
/// <summary>Optional maximum allowed date/time.</summary>
public DateTimeOffset? MaxDateTime
{
get => GetValue(MaxDateTimeProperty);
set => SetValue(MaxDateTimeProperty, value);
}
protected override void OnLoaded(RoutedEventArgs e)
{
base.OnLoaded(e);
var datePart = this.FindControl<CalendarDatePicker>("DatePart");
var timePart = this.FindControl<TimePicker>("TimePart");
if (datePart != null)
datePart.SelectedDateChanged += OnDatePartChanged;
if (timePart != null)
timePart.SelectedTimeChanged += OnTimePartChanged;
// Push initial value to the sub-controls
SyncFromDateTime();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (_isUpdating) return;
if (change.Property == SelectedDateTimeProperty)
SyncFromDateTime();
else if (change.Property == MinDateTimeProperty || change.Property == MaxDateTimeProperty)
{
ClampDateTime();
UpdateCalendarBounds();
}
}
private void OnDatePartChanged(object? sender, SelectionChangedEventArgs e)
{
if (_isUpdating) return;
SyncToDateTime();
}
private void OnTimePartChanged(object? sender, TimePickerSelectedValueChangedEventArgs e)
{
if (_isUpdating) return;
SyncToDateTime();
}
private void SyncFromDateTime()
{
var dt = SelectedDateTime;
if (dt == null) return;
_isUpdating = true;
try
{
var datePart = this.FindControl<CalendarDatePicker>("DatePart");
var timePart = this.FindControl<TimePicker>("TimePart");
if (datePart != null)
datePart.SelectedDate = dt.Value.DateTime.Date;
if (timePart != null)
timePart.SelectedTime = dt.Value.TimeOfDay;
}
finally
{
_isUpdating = false;
}
}
private void SyncToDateTime()
{
var datePart = this.FindControl<CalendarDatePicker>("DatePart");
var timePart = this.FindControl<TimePicker>("TimePart");
var date = datePart?.SelectedDate ?? DateTime.Now.Date;
var time = timePart?.SelectedTime ?? TimeSpan.Zero;
_isUpdating = true;
try
{
var combined = new DateTimeOffset(
date.Year, date.Month, date.Day,
time.Hours, time.Minutes, time.Seconds,
DateTimeOffset.Now.Offset);
SelectedDateTime = Clamp(combined);
}
finally
{
_isUpdating = false;
}
}
private void ClampDateTime()
{
if (SelectedDateTime == null) return;
var clamped = Clamp(SelectedDateTime.Value);
if (clamped != SelectedDateTime)
SelectedDateTime = clamped;
}
private DateTimeOffset Clamp(DateTimeOffset value)
{
if (MinDateTime.HasValue && value < MinDateTime.Value)
return MinDateTime.Value;
if (MaxDateTime.HasValue && value > MaxDateTime.Value)
return MaxDateTime.Value;
return value;
}
private void UpdateCalendarBounds()
{
var datePart = this.FindControl<CalendarDatePicker>("DatePart");
if (datePart == null) return;
datePart.DisplayDateStart = MinDateTime?.DateTime;
datePart.DisplayDateEnd = MaxDateTime?.DateTime;
}
}

View File

@@ -0,0 +1,15 @@
using ZB.MOM.WW.LmxOpcUa.Host.Configuration;
using ZB.MOM.WW.LmxOpcUa.Host.Historian;
namespace ZB.MOM.WW.LmxOpcUa.Historian.Aveva
{
/// <summary>
/// Reflection entry point invoked by <c>HistorianPluginLoader</c> in the Host. Kept
/// deliberately simple so the plugin contract is a single static factory method.
/// </summary>
public static class AvevaHistorianPluginEntry
{
public static IHistorianDataSource Create(HistorianConfiguration config)
=> new HistorianDataSource(config);
}
}

View File

@@ -8,13 +8,14 @@ using Opc.Ua;
using Serilog;
using ZB.MOM.WW.LmxOpcUa.Host.Configuration;
using ZB.MOM.WW.LmxOpcUa.Host.Domain;
using ZB.MOM.WW.LmxOpcUa.Host.Historian;
namespace ZB.MOM.WW.LmxOpcUa.Host.Historian
namespace ZB.MOM.WW.LmxOpcUa.Historian.Aveva
{
/// <summary>
/// Reads historical data from the Wonderware Historian via the aahClientManaged SDK.
/// </summary>
public class HistorianDataSource : IDisposable
public sealed class HistorianDataSource : IHistorianDataSource
{
private static readonly ILogger Log = Serilog.Log.ForContext<HistorianDataSource>();
@@ -154,14 +155,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Historian
}
/// <summary>
/// Reads raw historical values for a tag from the Historian.
/// </summary>
/// <param name="tagName">The Wonderware tag name backing the OPC UA node whose raw history is being requested.</param>
/// <param name="startTime">The inclusive start of the client-requested history window.</param>
/// <param name="endTime">The inclusive end of the client-requested history window.</param>
/// <param name="maxValues">The maximum number of samples to return when the OPC UA client limits the result set.</param>
/// <param name="ct">The cancellation token that aborts the query when the OPC UA request is cancelled.</param>
/// <inheritdoc />
public Task<List<DataValue>> ReadRawAsync(
string tagName, DateTime startTime, DateTime endTime, int maxValues,
CancellationToken ct = default)
@@ -246,15 +240,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Historian
return Task.FromResult(results);
}
/// <summary>
/// Reads aggregate historical values for a tag from the Historian.
/// </summary>
/// <param name="tagName">The Wonderware tag name backing the OPC UA node whose aggregate history is being requested.</param>
/// <param name="startTime">The inclusive start of the aggregate history window requested by the OPC UA client.</param>
/// <param name="endTime">The inclusive end of the aggregate history window requested by the OPC UA client.</param>
/// <param name="intervalMs">The Wonderware summary resolution, in milliseconds, used to bucket aggregate values.</param>
/// <param name="aggregateColumn">The Historian summary column that matches the OPC UA aggregate function being requested.</param>
/// <param name="ct">The cancellation token that aborts the aggregate query when the client request is cancelled.</param>
/// <inheritdoc />
public Task<List<DataValue>> ReadAggregateAsync(
string tagName, DateTime startTime, DateTime endTime,
double intervalMs, string aggregateColumn,
@@ -322,12 +308,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Historian
return Task.FromResult(results);
}
/// <summary>
/// Reads interpolated values for a tag at specific timestamps from the Historian.
/// </summary>
/// <param name="tagName">The Wonderware tag name backing the OPC UA node.</param>
/// <param name="timestamps">The specific timestamps at which interpolated values are requested.</param>
/// <param name="ct">The cancellation token.</param>
/// <inheritdoc />
public Task<List<DataValue>> ReadAtTimeAsync(
string tagName, DateTime[] timestamps,
CancellationToken ct = default)
@@ -420,19 +401,12 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Historian
return Task.FromResult(results);
}
/// <summary>
/// Reads historical alarm/event records from the Historian event store.
/// </summary>
/// <param name="sourceName">Optional source name filter. Null returns all events.</param>
/// <param name="startTime">The inclusive start of the event history window.</param>
/// <param name="endTime">The inclusive end of the event history window.</param>
/// <param name="maxEvents">The maximum number of events to return.</param>
/// <param name="ct">The cancellation token.</param>
public Task<List<HistorianEvent>> ReadEventsAsync(
/// <inheritdoc />
public Task<List<HistorianEventDto>> ReadEventsAsync(
string? sourceName, DateTime startTime, DateTime endTime, int maxEvents,
CancellationToken ct = default)
{
var results = new List<HistorianEvent>();
var results = new List<HistorianEventDto>();
try
{
@@ -464,7 +438,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Historian
while (query.MoveNext(out error))
{
ct.ThrowIfCancellationRequested();
results.Add(query.QueryResult);
results.Add(ToDto(query.QueryResult));
count++;
if (maxEvents > 0 && count >= maxEvents)
break;
@@ -492,6 +466,19 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Historian
return Task.FromResult(results);
}
private static HistorianEventDto ToDto(HistorianEvent evt)
{
return new HistorianEventDto
{
Id = evt.Id,
Source = evt.Source,
EventTime = evt.EventTime,
ReceivedTime = evt.ReceivedTime,
DisplayText = evt.DisplayText,
Severity = (ushort)evt.Severity
};
}
/// <summary>
/// Extracts the requested aggregate value from an <see cref="AnalogSummaryQueryResult"/> by column name.
/// </summary>
@@ -510,30 +497,6 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Historian
}
}
/// <summary>
/// Maps an OPC UA aggregate NodeId to the corresponding Historian column name.
/// Returns null if the aggregate is not supported.
/// </summary>
/// <param name="aggregateId">The OPC UA aggregate identifier requested by the history client.</param>
public static string? MapAggregateToColumn(NodeId aggregateId)
{
if (aggregateId == ObjectIds.AggregateFunction_Average)
return "Average";
if (aggregateId == ObjectIds.AggregateFunction_Minimum)
return "Minimum";
if (aggregateId == ObjectIds.AggregateFunction_Maximum)
return "Maximum";
if (aggregateId == ObjectIds.AggregateFunction_Count)
return "ValueCount";
if (aggregateId == ObjectIds.AggregateFunction_Start)
return "First";
if (aggregateId == ObjectIds.AggregateFunction_End)
return "Last";
if (aggregateId == ObjectIds.AggregateFunction_StandardDeviationPopulation)
return "StdDev";
return null;
}
/// <summary>
/// Closes the Historian SDK connection and releases resources.
/// </summary>

View File

@@ -3,7 +3,7 @@ using System.Threading;
using ArchestrA;
using ZB.MOM.WW.LmxOpcUa.Host.Configuration;
namespace ZB.MOM.WW.LmxOpcUa.Host.Historian
namespace ZB.MOM.WW.LmxOpcUa.Historian.Aveva
{
/// <summary>
/// Creates and opens Historian SDK connections. Extracted so tests can inject

View File

@@ -0,0 +1,87 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net48</TargetFramework>
<PlatformTarget>x86</PlatformTarget>
<LangVersion>9.0</LangVersion>
<Nullable>enable</Nullable>
<RootNamespace>ZB.MOM.WW.LmxOpcUa.Historian.Aveva</RootNamespace>
<AssemblyName>ZB.MOM.WW.LmxOpcUa.Historian.Aveva</AssemblyName>
<!-- Plugin is loaded at runtime via Assembly.LoadFrom; never copy it as a CopyLocal dep. -->
<CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies>
<!-- Deploy next to Host.exe under bin/<cfg>/Historian/ so F5 works without a manual copy. -->
<HistorianPluginOutputDir>$(MSBuildThisFileDirectory)..\ZB.MOM.WW.LmxOpcUa.Host\bin\$(Configuration)\net48\Historian\</HistorianPluginOutputDir>
</PropertyGroup>
<ItemGroup>
<InternalsVisibleTo Include="ZB.MOM.WW.LmxOpcUa.Historian.Aveva.Tests"/>
</ItemGroup>
<ItemGroup>
<!-- Logging -->
<PackageReference Include="Serilog" Version="2.10.0"/>
<!-- OPC UA (for DataValue/StatusCodes used by the IHistorianDataSource surface) -->
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Server" Version="1.5.374.126"/>
</ItemGroup>
<ItemGroup>
<!-- Private=false: the plugin binds to Host types at compile time but Host.exe must not be
copied into the plugin's output folder (it is already in the process). -->
<ProjectReference Include="..\ZB.MOM.WW.LmxOpcUa.Host\ZB.MOM.WW.LmxOpcUa.Host.csproj">
<Private>false</Private>
<ReferenceOutputAssembly>true</ReferenceOutputAssembly>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<!-- Wonderware Historian SDK -->
<Reference Include="aahClientManaged">
<HintPath>..\..\lib\aahClientManaged.dll</HintPath>
<EmbedInteropTypes>false</EmbedInteropTypes>
</Reference>
<Reference Include="aahClientCommon">
<HintPath>..\..\lib\aahClientCommon.dll</HintPath>
<EmbedInteropTypes>false</EmbedInteropTypes>
</Reference>
</ItemGroup>
<ItemGroup>
<!-- Historian SDK native dependencies — copied beside the plugin DLL so the AssemblyResolve
handler in HistorianPluginLoader can find them when the plugin first JITs. -->
<None Include="..\..\lib\aahClient.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="..\..\lib\aahClientCommon.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="..\..\lib\aahClientManaged.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="..\..\lib\Historian.CBE.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="..\..\lib\Historian.DPAPI.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="..\..\lib\ArchestrA.CloudHistorian.Contract.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<Target Name="StageHistorianPluginForHost" AfterTargets="Build">
<ItemGroup>
<_HistorianStageFiles Include="$(OutDir)aahClient.dll"/>
<_HistorianStageFiles Include="$(OutDir)aahClientCommon.dll"/>
<_HistorianStageFiles Include="$(OutDir)aahClientManaged.dll"/>
<_HistorianStageFiles Include="$(OutDir)Historian.CBE.dll"/>
<_HistorianStageFiles Include="$(OutDir)Historian.DPAPI.dll"/>
<_HistorianStageFiles Include="$(OutDir)ArchestrA.CloudHistorian.Contract.dll"/>
<_HistorianStageFiles Include="$(OutDir)$(AssemblyName).dll"/>
<_HistorianStageFiles Include="$(OutDir)$(AssemblyName).pdb" Condition="Exists('$(OutDir)$(AssemblyName).pdb')"/>
</ItemGroup>
<MakeDir Directories="$(HistorianPluginOutputDir)"/>
<Copy SourceFiles="@(_HistorianStageFiles)" DestinationFolder="$(HistorianPluginOutputDir)" SkipUnchangedFiles="true"/>
</Target>
</Project>

View File

@@ -45,5 +45,6 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Configuration
/// Gets or sets the maximum number of values returned per HistoryRead request.
/// </summary>
public int MaxValuesPerRead { get; set; } = 10000;
}
}

View File

@@ -2,12 +2,6 @@
<Costura>
<ExcludeAssemblies>
ArchestrA.MxAccess
aahClientManaged
aahClientCommon
aahClient
Historian.CBE
Historian.DPAPI
ArchestrA.CloudHistorian.Contract
</ExcludeAssemblies>
</Costura>
</Weavers>

View File

@@ -0,0 +1,31 @@
using Opc.Ua;
namespace ZB.MOM.WW.LmxOpcUa.Host.Historian
{
/// <summary>
/// Maps OPC UA aggregate NodeIds to the Wonderware Historian AnalogSummary column names
/// consumed by the historian plugin. Kept in Host so HistoryReadProcessed can validate
/// aggregate support without requiring the plugin to be loaded.
/// </summary>
public static class HistorianAggregateMap
{
public static string? MapAggregateToColumn(NodeId aggregateId)
{
if (aggregateId == ObjectIds.AggregateFunction_Average)
return "Average";
if (aggregateId == ObjectIds.AggregateFunction_Minimum)
return "Minimum";
if (aggregateId == ObjectIds.AggregateFunction_Maximum)
return "Maximum";
if (aggregateId == ObjectIds.AggregateFunction_Count)
return "ValueCount";
if (aggregateId == ObjectIds.AggregateFunction_Start)
return "First";
if (aggregateId == ObjectIds.AggregateFunction_End)
return "Last";
if (aggregateId == ObjectIds.AggregateFunction_StandardDeviationPopulation)
return "StdDev";
return null;
}
}
}

View File

@@ -0,0 +1,18 @@
using System;
namespace ZB.MOM.WW.LmxOpcUa.Host.Historian
{
/// <summary>
/// SDK-free representation of a Historian event record exposed by the historian plugin.
/// Prevents ArchestrA types from leaking into the Host assembly.
/// </summary>
public sealed class HistorianEventDto
{
public Guid Id { get; set; }
public string? Source { get; set; }
public DateTime EventTime { get; set; }
public DateTime ReceivedTime { get; set; }
public string? DisplayText { get; set; }
public ushort Severity { get; set; }
}
}

View File

@@ -0,0 +1,114 @@
using System;
using System.IO;
using System.Reflection;
using Serilog;
using ZB.MOM.WW.LmxOpcUa.Host.Configuration;
namespace ZB.MOM.WW.LmxOpcUa.Host.Historian
{
/// <summary>
/// Loads the Wonderware historian plugin assembly from the Historian/ subfolder next to
/// the host executable. Used so the aahClientManaged SDK is not needed on hosts that run
/// with Historian.Enabled=false.
/// </summary>
public static class HistorianPluginLoader
{
private const string PluginSubfolder = "Historian";
private const string PluginAssemblyName = "ZB.MOM.WW.LmxOpcUa.Historian.Aveva";
private const string PluginEntryType = "ZB.MOM.WW.LmxOpcUa.Historian.Aveva.AvevaHistorianPluginEntry";
private const string PluginEntryMethod = "Create";
private static readonly ILogger Log = Serilog.Log.ForContext(typeof(HistorianPluginLoader));
private static readonly object ResolverGate = new object();
private static bool _resolverInstalled;
private static string? _resolvedProbeDirectory;
/// <summary>
/// Attempts to load the historian plugin and construct an <see cref="IHistorianDataSource"/>.
/// Returns null on any failure so the server can continue with history unsupported.
/// </summary>
public static IHistorianDataSource? TryLoad(HistorianConfiguration config)
{
var pluginDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, PluginSubfolder);
var pluginPath = Path.Combine(pluginDirectory, PluginAssemblyName + ".dll");
if (!File.Exists(pluginPath))
{
Log.Warning(
"Historian plugin not found at {PluginPath} — history read operations will return BadHistoryOperationUnsupported",
pluginPath);
return null;
}
EnsureAssemblyResolverInstalled(pluginDirectory);
try
{
var assembly = Assembly.LoadFrom(pluginPath);
var entryType = assembly.GetType(PluginEntryType, throwOnError: false);
if (entryType == null)
{
Log.Warning("Historian plugin {PluginPath} does not expose {EntryType}", pluginPath, PluginEntryType);
return null;
}
var create = entryType.GetMethod(PluginEntryMethod, BindingFlags.Public | BindingFlags.Static);
if (create == null)
{
Log.Warning("Historian plugin entry type {EntryType} missing static {Method}", PluginEntryType, PluginEntryMethod);
return null;
}
var result = create.Invoke(null, new object[] { config });
if (result is IHistorianDataSource dataSource)
{
Log.Information("Historian plugin loaded from {PluginPath}", pluginPath);
return dataSource;
}
Log.Warning("Historian plugin {PluginPath} returned an object that does not implement IHistorianDataSource", pluginPath);
return null;
}
catch (Exception ex)
{
Log.Warning(ex, "Failed to load historian plugin from {PluginPath} — history disabled", pluginPath);
return null;
}
}
private static void EnsureAssemblyResolverInstalled(string pluginDirectory)
{
lock (ResolverGate)
{
_resolvedProbeDirectory = pluginDirectory;
if (_resolverInstalled)
return;
AppDomain.CurrentDomain.AssemblyResolve += ResolveFromPluginDirectory;
_resolverInstalled = true;
}
}
private static Assembly? ResolveFromPluginDirectory(object? sender, ResolveEventArgs args)
{
var probeDirectory = _resolvedProbeDirectory;
if (string.IsNullOrEmpty(probeDirectory))
return null;
var requested = new AssemblyName(args.Name);
var candidate = Path.Combine(probeDirectory!, requested.Name + ".dll");
if (!File.Exists(candidate))
return null;
try
{
return Assembly.LoadFrom(candidate);
}
catch (Exception ex)
{
Log.Debug(ex, "Historian plugin resolver failed to load {Candidate}", candidate);
return null;
}
}
}
}

View File

@@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Opc.Ua;
namespace ZB.MOM.WW.LmxOpcUa.Host.Historian
{
/// <summary>
/// OPC UA-typed surface for the historian plugin. Host consumers depend only on this
/// interface so the Wonderware Historian SDK assemblies are not required unless the
/// plugin is loaded at runtime.
/// </summary>
public interface IHistorianDataSource : IDisposable
{
Task<List<DataValue>> ReadRawAsync(
string tagName, DateTime startTime, DateTime endTime, int maxValues,
CancellationToken ct = default);
Task<List<DataValue>> ReadAggregateAsync(
string tagName, DateTime startTime, DateTime endTime,
double intervalMs, string aggregateColumn,
CancellationToken ct = default);
Task<List<DataValue>> ReadAtTimeAsync(
string tagName, DateTime[] timestamps,
CancellationToken ct = default);
Task<List<HistorianEventDto>> ReadEventsAsync(
string? sourceName, DateTime startTime, DateTime endTime, int maxEvents,
CancellationToken ct = default);
}
}

View File

@@ -33,7 +33,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
private readonly AutoResetEvent _dataChangeSignal = new(false);
private readonly Dictionary<int, List<string>> _gobjectToTagRefs = new();
private readonly HistoryContinuationPointManager _historyContinuations = new();
private readonly HistorianDataSource? _historianDataSource;
private readonly IHistorianDataSource? _historianDataSource;
private readonly PerformanceMetrics _metrics;
private readonly IMxAccessClient _mxAccessClient;
@@ -89,7 +89,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
string namespaceUri,
IMxAccessClient mxAccessClient,
PerformanceMetrics metrics,
HistorianDataSource? historianDataSource = null,
IHistorianDataSource? historianDataSource = null,
bool alarmTrackingEnabled = false,
bool anonymousCanWrite = true,
NodeId? writeOperateRoleId = null,
@@ -1591,7 +1591,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
}
var aggregateId = details.AggregateType[idx < details.AggregateType.Count ? idx : 0];
var column = HistorianDataSource.MapAggregateToColumn(aggregateId);
var column = HistorianAggregateMap.MapAggregateToColumn(aggregateId);
if (column == null)
{
errors[idx] = new ServiceResult(StatusCodes.BadAggregateNotSupported);

View File

@@ -23,7 +23,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
private readonly IUserAuthenticationProvider? _authProvider;
private readonly string _galaxyName;
private readonly HistorianDataSource? _historianDataSource;
private readonly IHistorianDataSource? _historianDataSource;
private readonly PerformanceMetrics _metrics;
private readonly IMxAccessClient _mxAccessClient;
private readonly RedundancyConfiguration _redundancyConfig;
@@ -37,7 +37,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
private NodeId? _writeTuneRoleId;
public LmxOpcUaServer(string galaxyName, IMxAccessClient mxAccessClient, PerformanceMetrics metrics,
HistorianDataSource? historianDataSource = null, bool alarmTrackingEnabled = false,
IHistorianDataSource? historianDataSource = null, bool alarmTrackingEnabled = false,
AuthenticationConfiguration? authConfig = null, IUserAuthenticationProvider? authProvider = null,
RedundancyConfiguration? redundancyConfig = null, string? applicationUri = null)
{

View File

@@ -22,7 +22,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
private readonly IUserAuthenticationProvider? _authProvider;
private readonly OpcUaConfiguration _config;
private readonly HistorianDataSource? _historianDataSource;
private readonly IHistorianDataSource? _historianDataSource;
private readonly PerformanceMetrics _metrics;
private readonly IMxAccessClient _mxAccessClient;
private readonly RedundancyConfiguration _redundancyConfig;
@@ -38,7 +38,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
/// <param name="metrics">The metrics collector shared with the node manager and runtime bridge.</param>
/// <param name="historianDataSource">The optional historian adapter that enables OPC UA history read support.</param>
public OpcUaServerHost(OpcUaConfiguration config, IMxAccessClient mxAccessClient, PerformanceMetrics metrics,
HistorianDataSource? historianDataSource = null,
IHistorianDataSource? historianDataSource = null,
AuthenticationConfiguration? authConfig = null,
IUserAuthenticationProvider? authProvider = null,
SecurityProfileConfiguration? securityConfig = null,

View File

@@ -31,7 +31,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host
private CancellationTokenSource? _cts;
private HealthCheckService? _healthCheck;
private HistorianDataSource? _historianDataSource;
private IHistorianDataSource? _historianDataSource;
private MxAccessClient? _mxAccessClient;
private IMxAccessClient? _mxAccessClientForWiring;
private StaComThread? _staThread;
@@ -216,7 +216,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host
var effectiveMxClient = (IMxAccessClient?)_mxAccessClient ??
_mxAccessClientForWiring ?? new NullMxAccessClient();
_historianDataSource = _config.Historian.Enabled
? new HistorianDataSource(_config.Historian)
? HistorianPluginLoader.TryLoad(_config.Historian)
: null;
IUserAuthenticationProvider? authProvider = null;
if (_hasAuthProviderOverride)

View File

@@ -43,39 +43,11 @@
</ItemGroup>
<ItemGroup>
<!-- MXAccess COM interop -->
<!-- MXAccess COM interop (unrelated to the historian SDK) -->
<Reference Include="ArchestrA.MxAccess">
<HintPath>..\..\lib\ArchestrA.MxAccess.dll</HintPath>
<EmbedInteropTypes>false</EmbedInteropTypes>
</Reference>
<!-- Wonderware Historian SDK -->
<Reference Include="aahClientManaged">
<HintPath>..\..\lib\aahClientManaged.dll</HintPath>
<EmbedInteropTypes>false</EmbedInteropTypes>
</Reference>
<Reference Include="aahClientCommon">
<HintPath>..\..\lib\aahClientCommon.dll</HintPath>
<EmbedInteropTypes>false</EmbedInteropTypes>
</Reference>
</ItemGroup>
<ItemGroup>
<!-- Historian SDK native dependencies -->
<None Include="..\..\lib\aahClient.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="..\..\lib\aahClientCommon.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="..\..\lib\Historian.CBE.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="..\..\lib\Historian.DPAPI.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="..\..\lib\ArchestrA.CloudHistorian.Contract.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>