Document client stack XML docs progress
This commit is contained in:
5126
lmxopcua-docs-fixed.md
Normal file
5126
lmxopcua-docs-fixed.md
Normal file
File diff suppressed because it is too large
Load Diff
8676
lmxopcua-docs-issues.md
Normal file
8676
lmxopcua-docs-issues.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -17,30 +17,56 @@ public abstract class CommandBase : ICommand
|
||||
|
||||
private readonly IOpcUaClientServiceFactory _factory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a CLI command with the shared OPC UA client factory used to create per-command sessions.
|
||||
/// </summary>
|
||||
/// <param name="factory">The factory that creates the shared client service for the command execution.</param>
|
||||
protected CommandBase(IOpcUaClientServiceFactory factory)
|
||||
{
|
||||
_factory = factory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the primary OPC UA endpoint URL the command should connect to.
|
||||
/// </summary>
|
||||
[CommandOption("url", 'u', Description = "OPC UA server endpoint URL", IsRequired = true)]
|
||||
public string Url { get; init; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the optional username used for authenticated OPC UA sessions.
|
||||
/// </summary>
|
||||
[CommandOption("username", 'U', Description = "Username for authentication")]
|
||||
public string? Username { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the optional password paired with <see cref="Username" /> for authenticated OPC UA sessions.
|
||||
/// </summary>
|
||||
[CommandOption("password", 'P', Description = "Password for authentication")]
|
||||
public string? Password { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the transport security mode requested by the operator for this CLI session.
|
||||
/// </summary>
|
||||
[CommandOption("security", 'S',
|
||||
Description = "Transport security: none, sign, encrypt, signandencrypt (default: none)")]
|
||||
public string Security { get; init; } = "none";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the optional comma-separated failover endpoints that should be tried if the primary endpoint is unavailable.
|
||||
/// </summary>
|
||||
[CommandOption("failover-urls", 'F', Description = "Comma-separated failover endpoint URLs for redundancy")]
|
||||
public string? FailoverUrls { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether verbose Serilog output should be enabled for troubleshooting.
|
||||
/// </summary>
|
||||
[CommandOption("verbose", Description = "Enable verbose/debug logging")]
|
||||
public bool Verbose { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Executes the command-specific workflow against the configured OPC UA endpoint.
|
||||
/// </summary>
|
||||
/// <param name="console">The CLI console used for output and cancellation handling.</param>
|
||||
public abstract ValueTask ExecuteAsync(IConsole console);
|
||||
|
||||
/// <summary>
|
||||
@@ -70,6 +96,7 @@ public abstract class CommandBase : ICommand
|
||||
/// Creates a new <see cref="IOpcUaClientService" />, connects it using the common options,
|
||||
/// and returns both the service and the connection info.
|
||||
/// </summary>
|
||||
/// <param name="ct">The cancellation token that aborts connection setup for the command.</param>
|
||||
protected async Task<(IOpcUaClientService Service, ConnectionInfo Info)> CreateServiceAndConnectAsync(
|
||||
CancellationToken ct)
|
||||
{
|
||||
@@ -94,4 +121,4 @@ public abstract class CommandBase : ICommand
|
||||
|
||||
Log.Logger = config.CreateLogger();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,19 +8,36 @@ namespace ZB.MOM.WW.LmxOpcUa.Client.CLI.Commands;
|
||||
[Command("alarms", Description = "Subscribe to alarm events")]
|
||||
public class AlarmsCommand : CommandBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates the alarm-monitoring command used to stream OPC UA condition events to the terminal.
|
||||
/// </summary>
|
||||
/// <param name="factory">The factory that creates the shared client service for the command run.</param>
|
||||
public AlarmsCommand(IOpcUaClientServiceFactory factory) : base(factory)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the optional source node whose alarm events should be monitored instead of the server root.
|
||||
/// </summary>
|
||||
[CommandOption("node", 'n', Description = "Node ID to monitor for events (default: Server node)")]
|
||||
public string? NodeId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the publishing interval, in milliseconds, for the alarm subscription.
|
||||
/// </summary>
|
||||
[CommandOption("interval", 'i', Description = "Publishing interval in milliseconds")]
|
||||
public int Interval { get; init; } = 1000;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether retained alarm conditions should be refreshed immediately after subscribing.
|
||||
/// </summary>
|
||||
[CommandOption("refresh", Description = "Request a ConditionRefresh after subscribing")]
|
||||
public bool Refresh { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Connects to the server, subscribes to alarm events, and streams operator-facing alarm state changes to the console.
|
||||
/// </summary>
|
||||
/// <param name="console">The CLI console used for output and cancellation handling.</param>
|
||||
public override async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
ConfigureLogging();
|
||||
@@ -83,4 +100,4 @@ public class AlarmsCommand : CommandBase
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,19 +9,36 @@ namespace ZB.MOM.WW.LmxOpcUa.Client.CLI.Commands;
|
||||
[Command("browse", Description = "Browse the OPC UA address space")]
|
||||
public class BrowseCommand : CommandBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates the browse command used to inspect the server address space from the terminal.
|
||||
/// </summary>
|
||||
/// <param name="factory">The factory that creates the shared client service for the command run.</param>
|
||||
public BrowseCommand(IOpcUaClientServiceFactory factory) : base(factory)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the optional starting node for the browse, defaulting to the Objects folder.
|
||||
/// </summary>
|
||||
[CommandOption("node", 'n', Description = "Node ID to browse (default: Objects folder)")]
|
||||
public string? NodeId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the maximum browse depth the command should traverse.
|
||||
/// </summary>
|
||||
[CommandOption("depth", 'd', Description = "Maximum browse depth")]
|
||||
public int Depth { get; init; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether child nodes should be traversed recursively.
|
||||
/// </summary>
|
||||
[CommandOption("recursive", 'r', Description = "Browse recursively (uses --depth as max depth)")]
|
||||
public bool Recursive { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Connects to the server and prints a tree view of the requested address-space branch.
|
||||
/// </summary>
|
||||
/// <param name="console">The CLI console used for output and cancellation handling.</param>
|
||||
public override async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
ConfigureLogging();
|
||||
@@ -77,4 +94,4 @@ public class BrowseCommand : CommandBase
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,18 @@ namespace ZB.MOM.WW.LmxOpcUa.Client.CLI.Commands;
|
||||
[Command("connect", Description = "Test connection to an OPC UA server")]
|
||||
public class ConnectCommand : CommandBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates the connectivity test command used to verify endpoint reachability and negotiated security settings.
|
||||
/// </summary>
|
||||
/// <param name="factory">The factory that creates the shared client service for the command run.</param>
|
||||
public ConnectCommand(IOpcUaClientServiceFactory factory) : base(factory)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connects to the server and prints the negotiated endpoint details for operator verification.
|
||||
/// </summary>
|
||||
/// <param name="console">The CLI console used for output and cancellation handling.</param>
|
||||
public override async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
ConfigureLogging();
|
||||
@@ -34,4 +42,4 @@ public class ConnectCommand : CommandBase
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,28 +10,54 @@ namespace ZB.MOM.WW.LmxOpcUa.Client.CLI.Commands;
|
||||
[Command("historyread", Description = "Read historical data from a node")]
|
||||
public class HistoryReadCommand : CommandBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates the historical-data command used to inspect raw or aggregate history for a node.
|
||||
/// </summary>
|
||||
/// <param name="factory">The factory that creates the shared client service for the command run.</param>
|
||||
public HistoryReadCommand(IOpcUaClientServiceFactory factory) : base(factory)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the historized node to query.
|
||||
/// </summary>
|
||||
[CommandOption("node", 'n', Description = "Node ID (e.g. ns=2;s=MyNode)", IsRequired = true)]
|
||||
public string NodeId { get; init; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the optional history start time string supplied by the operator.
|
||||
/// </summary>
|
||||
[CommandOption("start", Description = "Start time (ISO 8601 or date string, default: 24 hours ago)")]
|
||||
public string? StartTime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the optional history end time string supplied by the operator.
|
||||
/// </summary>
|
||||
[CommandOption("end", Description = "End time (ISO 8601 or date string, default: now)")]
|
||||
public string? EndTime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the maximum number of raw values the command should print.
|
||||
/// </summary>
|
||||
[CommandOption("max", Description = "Maximum number of values to return")]
|
||||
public int MaxValues { get; init; } = 1000;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the optional aggregate name used when the operator wants processed history instead of raw samples.
|
||||
/// </summary>
|
||||
[CommandOption("aggregate", Description = "Aggregate function: Average, Minimum, Maximum, Count, Start, End")]
|
||||
public string? Aggregate { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the aggregate bucket interval, in milliseconds, for processed history reads.
|
||||
/// </summary>
|
||||
[CommandOption("interval", Description = "Processing interval in milliseconds for aggregates")]
|
||||
public double IntervalMs { get; init; } = 3600000;
|
||||
|
||||
/// <summary>
|
||||
/// Connects to the server and prints raw or processed historical values for the requested node.
|
||||
/// </summary>
|
||||
/// <param name="console">The CLI console used for output and cancellation handling.</param>
|
||||
public override async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
ConfigureLogging();
|
||||
@@ -105,4 +131,4 @@ public class HistoryReadCommand : CommandBase
|
||||
$"Unknown aggregate: '{name}'. Supported: Average, Minimum, Maximum, Count, Start, End")
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,13 +8,24 @@ namespace ZB.MOM.WW.LmxOpcUa.Client.CLI.Commands;
|
||||
[Command("read", Description = "Read a value from a node")]
|
||||
public class ReadCommand : CommandBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates the point-read command used to inspect the current value of a node.
|
||||
/// </summary>
|
||||
/// <param name="factory">The factory that creates the shared client service for the command run.</param>
|
||||
public ReadCommand(IOpcUaClientServiceFactory factory) : base(factory)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the node whose current value should be read.
|
||||
/// </summary>
|
||||
[CommandOption("node", 'n', Description = "Node ID (e.g. ns=2;s=MyNode)", IsRequired = true)]
|
||||
public string NodeId { get; init; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Connects to the server and prints the current value, status, and timestamps for the requested node.
|
||||
/// </summary>
|
||||
/// <param name="console">The CLI console used for output and cancellation handling.</param>
|
||||
public override async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
ConfigureLogging();
|
||||
@@ -42,4 +53,4 @@ public class ReadCommand : CommandBase
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,18 @@ namespace ZB.MOM.WW.LmxOpcUa.Client.CLI.Commands;
|
||||
[Command("redundancy", Description = "Read redundancy state from an OPC UA server")]
|
||||
public class RedundancyCommand : CommandBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates the redundancy-inspection command used to display service-level and partner-server status.
|
||||
/// </summary>
|
||||
/// <param name="factory">The factory that creates the shared client service for the command run.</param>
|
||||
public RedundancyCommand(IOpcUaClientServiceFactory factory) : base(factory)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connects to the server and prints redundancy mode, service level, and partner-server identity data.
|
||||
/// </summary>
|
||||
/// <param name="console">The CLI console used for output and cancellation handling.</param>
|
||||
public override async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
ConfigureLogging();
|
||||
@@ -42,4 +50,4 @@ public class RedundancyCommand : CommandBase
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,16 +8,30 @@ namespace ZB.MOM.WW.LmxOpcUa.Client.CLI.Commands;
|
||||
[Command("subscribe", Description = "Monitor a node for value changes")]
|
||||
public class SubscribeCommand : CommandBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates the live-data subscription command used to watch runtime value changes from the terminal.
|
||||
/// </summary>
|
||||
/// <param name="factory">The factory that creates the shared client service for the command run.</param>
|
||||
public SubscribeCommand(IOpcUaClientServiceFactory factory) : base(factory)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the node whose live value changes should be monitored.
|
||||
/// </summary>
|
||||
[CommandOption("node", 'n', Description = "Node ID to monitor", IsRequired = true)]
|
||||
public string NodeId { get; init; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the sampling interval, in milliseconds, for the monitored item.
|
||||
/// </summary>
|
||||
[CommandOption("interval", 'i', Description = "Sampling interval in milliseconds")]
|
||||
public int Interval { get; init; } = 1000;
|
||||
|
||||
/// <summary>
|
||||
/// Connects to the server and streams live data-change notifications for the requested node.
|
||||
/// </summary>
|
||||
/// <param name="console">The CLI console used for output and cancellation handling.</param>
|
||||
public override async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
ConfigureLogging();
|
||||
@@ -61,4 +75,4 @@ public class SubscribeCommand : CommandBase
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,16 +10,30 @@ namespace ZB.MOM.WW.LmxOpcUa.Client.CLI.Commands;
|
||||
[Command("write", Description = "Write a value to a node")]
|
||||
public class WriteCommand : CommandBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates the write command used to update a node from the terminal.
|
||||
/// </summary>
|
||||
/// <param name="factory">The factory that creates the shared client service for the command run.</param>
|
||||
public WriteCommand(IOpcUaClientServiceFactory factory) : base(factory)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the node whose value should be updated.
|
||||
/// </summary>
|
||||
[CommandOption("node", 'n', Description = "Node ID (e.g. ns=2;s=MyNode)", IsRequired = true)]
|
||||
public string NodeId { get; init; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the raw operator-entered value that will be converted to the node's runtime type before writing.
|
||||
/// </summary>
|
||||
[CommandOption("value", 'v', Description = "Value to write", IsRequired = true)]
|
||||
public string Value { get; init; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Connects to the server, converts the supplied value to the node's current data type, and issues the write.
|
||||
/// </summary>
|
||||
/// <param name="console">The CLI console used for output and cancellation handling.</param>
|
||||
public override async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
ConfigureLogging();
|
||||
@@ -51,4 +65,4 @@ public class WriteCommand : CommandBase
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,20 +12,40 @@ internal sealed class DefaultSessionAdapter : ISessionAdapter
|
||||
private static readonly ILogger Logger = Log.ForContext<DefaultSessionAdapter>();
|
||||
private readonly Session _session;
|
||||
|
||||
/// <summary>
|
||||
/// Wraps a live OPC UA session so the shared client can issue runtime operations through a testable adapter surface.
|
||||
/// </summary>
|
||||
/// <param name="session">The connected OPC UA session used for browsing, reads, writes, history, and subscriptions.</param>
|
||||
public DefaultSessionAdapter(Session session)
|
||||
{
|
||||
_session = session;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Connected => _session.Connected;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string SessionId => _session.SessionId?.ToString() ?? string.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string SessionName => _session.SessionName ?? string.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string EndpointUrl => _session.Endpoint?.EndpointUrl ?? string.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string ServerName => _session.Endpoint?.Server?.ApplicationName?.Text ?? string.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string SecurityMode => _session.Endpoint?.SecurityMode.ToString() ?? string.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string SecurityPolicyUri => _session.Endpoint?.SecurityPolicyUri ?? string.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
public NamespaceTable NamespaceUris => _session.NamespaceUris;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RegisterKeepAliveHandler(Action<bool> callback)
|
||||
{
|
||||
_session.KeepAlive += (_, e) =>
|
||||
@@ -35,11 +55,13 @@ internal sealed class DefaultSessionAdapter : ISessionAdapter
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<DataValue> ReadValueAsync(NodeId nodeId, CancellationToken ct)
|
||||
{
|
||||
return await _session.ReadValueAsync(nodeId, ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<StatusCode> WriteValueAsync(NodeId nodeId, DataValue value, CancellationToken ct)
|
||||
{
|
||||
var writeValue = new WriteValue
|
||||
@@ -54,6 +76,7 @@ internal sealed class DefaultSessionAdapter : ISessionAdapter
|
||||
return response.Results[0];
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<(byte[]? ContinuationPoint, ReferenceDescriptionCollection References)> BrowseAsync(
|
||||
NodeId nodeId, uint nodeClassMask, CancellationToken ct)
|
||||
{
|
||||
@@ -70,6 +93,7 @@ internal sealed class DefaultSessionAdapter : ISessionAdapter
|
||||
return (continuationPoint, references ?? []);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<(byte[]? ContinuationPoint, ReferenceDescriptionCollection References)> BrowseNextAsync(
|
||||
byte[] continuationPoint, CancellationToken ct)
|
||||
{
|
||||
@@ -77,6 +101,7 @@ internal sealed class DefaultSessionAdapter : ISessionAdapter
|
||||
return (nextCp, nextRefs ?? []);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> HasChildrenAsync(NodeId nodeId, CancellationToken ct)
|
||||
{
|
||||
var (_, _, references) = await _session.BrowseAsync(
|
||||
@@ -92,6 +117,7 @@ internal sealed class DefaultSessionAdapter : ISessionAdapter
|
||||
return references != null && references.Count > 0;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<DataValue>> HistoryReadRawAsync(
|
||||
NodeId nodeId, DateTime startTime, DateTime endTime, int maxValues, CancellationToken ct)
|
||||
{
|
||||
@@ -142,6 +168,7 @@ internal sealed class DefaultSessionAdapter : ISessionAdapter
|
||||
return allValues;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<DataValue>> HistoryReadAggregateAsync(
|
||||
NodeId nodeId, DateTime startTime, DateTime endTime, NodeId aggregateId, double intervalMs,
|
||||
CancellationToken ct)
|
||||
@@ -182,6 +209,7 @@ internal sealed class DefaultSessionAdapter : ISessionAdapter
|
||||
return allValues;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ISubscriptionAdapter> CreateSubscriptionAsync(int publishingIntervalMs, CancellationToken ct)
|
||||
{
|
||||
var subscription = new Subscription(_session.DefaultSubscription)
|
||||
@@ -196,6 +224,7 @@ internal sealed class DefaultSessionAdapter : ISessionAdapter
|
||||
return new DefaultSubscriptionAdapter(subscription);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task CloseAsync(CancellationToken ct)
|
||||
{
|
||||
try
|
||||
@@ -208,6 +237,9 @@ internal sealed class DefaultSessionAdapter : ISessionAdapter
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases the wrapped OPC UA session when the shared client shuts down or swaps endpoints during failover.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
try
|
||||
@@ -221,6 +253,7 @@ internal sealed class DefaultSessionAdapter : ISessionAdapter
|
||||
_session.Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IList<object>?> CallMethodAsync(NodeId objectId, NodeId methodId, object[] inputArguments,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
@@ -243,4 +276,4 @@ internal sealed class DefaultSessionAdapter : ISessionAdapter
|
||||
|
||||
return callResult.OutputArguments?.Select(v => v.Value).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,13 +13,19 @@ internal sealed class DefaultSubscriptionAdapter : ISubscriptionAdapter
|
||||
private readonly Dictionary<uint, MonitoredItem> _monitoredItems = new();
|
||||
private readonly Subscription _subscription;
|
||||
|
||||
/// <summary>
|
||||
/// Wraps a live OPC UA subscription so client code can manage monitored items through a testable abstraction.
|
||||
/// </summary>
|
||||
/// <param name="subscription">The underlying OPC UA subscription that owns monitored items for this client workflow.</param>
|
||||
public DefaultSubscriptionAdapter(Subscription subscription)
|
||||
{
|
||||
_subscription = subscription;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint SubscriptionId => _subscription.Id;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<uint> AddDataChangeMonitoredItemAsync(
|
||||
NodeId nodeId, int samplingIntervalMs, Action<string, DataValue> onDataChange, CancellationToken ct)
|
||||
{
|
||||
@@ -46,6 +52,7 @@ internal sealed class DefaultSubscriptionAdapter : ISubscriptionAdapter
|
||||
return handle;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task RemoveMonitoredItemAsync(uint clientHandle, CancellationToken ct)
|
||||
{
|
||||
if (!_monitoredItems.TryGetValue(clientHandle, out var item))
|
||||
@@ -58,6 +65,7 @@ internal sealed class DefaultSubscriptionAdapter : ISubscriptionAdapter
|
||||
Logger.Debug("Removed monitored item handle={Handle}", clientHandle);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<uint> AddEventMonitoredItemAsync(
|
||||
NodeId nodeId, int samplingIntervalMs, EventFilter filter, Action<EventFieldList> onEvent, CancellationToken ct)
|
||||
{
|
||||
@@ -86,11 +94,13 @@ internal sealed class DefaultSubscriptionAdapter : ISubscriptionAdapter
|
||||
return handle;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ConditionRefreshAsync(CancellationToken ct)
|
||||
{
|
||||
await _subscription.ConditionRefreshAsync(ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task DeleteAsync(CancellationToken ct)
|
||||
{
|
||||
try
|
||||
@@ -105,6 +115,9 @@ internal sealed class DefaultSubscriptionAdapter : ISubscriptionAdapter
|
||||
_monitoredItems.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases the wrapped OPC UA subscription and clears tracked monitored items held by the adapter.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
try
|
||||
@@ -117,4 +130,4 @@ internal sealed class DefaultSubscriptionAdapter : ISubscriptionAdapter
|
||||
|
||||
_monitoredItems.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,59 +7,134 @@ namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Adapters;
|
||||
/// </summary>
|
||||
internal interface ISessionAdapter : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the underlying OPC UA session is currently usable for client operations.
|
||||
/// </summary>
|
||||
bool Connected { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the server-assigned session identifier for diagnostics and failover reporting.
|
||||
/// </summary>
|
||||
string SessionId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the friendly session name presented to the OPC UA server.
|
||||
/// </summary>
|
||||
string SessionName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the active endpoint URL that this adapter is connected to.
|
||||
/// </summary>
|
||||
string EndpointUrl { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the server name reported by the connected OPC UA endpoint.
|
||||
/// </summary>
|
||||
string ServerName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the negotiated OPC UA message security mode for the session.
|
||||
/// </summary>
|
||||
string SecurityMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the negotiated OPC UA security policy URI for the session.
|
||||
/// </summary>
|
||||
string SecurityPolicyUri { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the namespace table used to resolve expanded node identifiers returned by browse operations.
|
||||
/// </summary>
|
||||
NamespaceTable NamespaceUris { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Registers a keep-alive callback. The callback receives true when the session is healthy, false on failure.
|
||||
/// </summary>
|
||||
/// <param name="callback">The callback used by higher-level clients to trigger reconnect or failover behavior.</param>
|
||||
void RegisterKeepAliveHandler(Action<bool> callback);
|
||||
|
||||
/// <summary>
|
||||
/// Reads the current value for a node from the connected OPC UA server.
|
||||
/// </summary>
|
||||
/// <param name="nodeId">The node whose current runtime value should be read.</param>
|
||||
/// <param name="ct">The cancellation token that aborts the server read if the client cancels the request.</param>
|
||||
Task<DataValue> ReadValueAsync(NodeId nodeId, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Writes a typed value to a node on the connected OPC UA server.
|
||||
/// </summary>
|
||||
/// <param name="nodeId">The node whose value should be updated.</param>
|
||||
/// <param name="value">The typed OPC UA data value to write to the server.</param>
|
||||
/// <param name="ct">The cancellation token that aborts the write if the client cancels the request.</param>
|
||||
Task<StatusCode> WriteValueAsync(NodeId nodeId, DataValue value, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Browses forward hierarchical references from the given node.
|
||||
/// Returns (continuationPoint, references).
|
||||
/// </summary>
|
||||
/// <param name="nodeId">The starting node for the hierarchical browse.</param>
|
||||
/// <param name="nodeClassMask">The node classes that should be returned to the caller.</param>
|
||||
/// <param name="ct">The cancellation token that aborts the browse request.</param>
|
||||
Task<(byte[]? ContinuationPoint, ReferenceDescriptionCollection References)> BrowseAsync(
|
||||
NodeId nodeId, uint nodeClassMask = 0, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Continues a browse from a continuation point.
|
||||
/// </summary>
|
||||
/// <param name="continuationPoint">The continuation token returned by a prior browse result page.</param>
|
||||
/// <param name="ct">The cancellation token that aborts the browse-next request.</param>
|
||||
Task<(byte[]? ContinuationPoint, ReferenceDescriptionCollection References)> BrowseNextAsync(
|
||||
byte[] continuationPoint, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether a node has any forward hierarchical child references.
|
||||
/// </summary>
|
||||
/// <param name="nodeId">The node to inspect for child objects or variables.</param>
|
||||
/// <param name="ct">The cancellation token that aborts the child lookup.</param>
|
||||
Task<bool> HasChildrenAsync(NodeId nodeId, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Reads raw historical data.
|
||||
/// </summary>
|
||||
/// <param name="nodeId">The historized node whose raw samples should be retrieved.</param>
|
||||
/// <param name="startTime">The inclusive start of the requested history window.</param>
|
||||
/// <param name="endTime">The inclusive end of the requested history window.</param>
|
||||
/// <param name="maxValues">The maximum number of raw samples to return to the client.</param>
|
||||
/// <param name="ct">The cancellation token that aborts the history read.</param>
|
||||
Task<IReadOnlyList<DataValue>> HistoryReadRawAsync(NodeId nodeId, DateTime startTime, DateTime endTime,
|
||||
int maxValues, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Reads processed/aggregate historical data.
|
||||
/// </summary>
|
||||
/// <param name="nodeId">The historized node whose processed values should be retrieved.</param>
|
||||
/// <param name="startTime">The inclusive start of the requested processed-history window.</param>
|
||||
/// <param name="endTime">The inclusive end of the requested processed-history window.</param>
|
||||
/// <param name="aggregateId">The OPC UA aggregate function to evaluate over the history window.</param>
|
||||
/// <param name="intervalMs">The processing interval, in milliseconds, for each aggregate bucket.</param>
|
||||
/// <param name="ct">The cancellation token that aborts the aggregate history read.</param>
|
||||
Task<IReadOnlyList<DataValue>> HistoryReadAggregateAsync(NodeId nodeId, DateTime startTime, DateTime endTime,
|
||||
NodeId aggregateId, double intervalMs, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a subscription adapter for this session.
|
||||
/// </summary>
|
||||
/// <param name="publishingIntervalMs">The requested publishing interval for monitored items on the new subscription.</param>
|
||||
/// <param name="ct">The cancellation token that aborts subscription creation.</param>
|
||||
Task<ISubscriptionAdapter> CreateSubscriptionAsync(int publishingIntervalMs, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Calls an OPC UA method node with the provided input arguments.
|
||||
/// </summary>
|
||||
/// <param name="objectId">The object node that owns the target method.</param>
|
||||
/// <param name="methodId">The method node to invoke.</param>
|
||||
/// <param name="inputArguments">The ordered input arguments supplied to the server method call.</param>
|
||||
/// <param name="ct">The cancellation token that aborts the method invocation.</param>
|
||||
Task<IList<object>?> CallMethodAsync(NodeId objectId, NodeId methodId, object[] inputArguments, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Closes the underlying session gracefully before the adapter is disposed or replaced during failover.
|
||||
/// </summary>
|
||||
/// <param name="ct">The cancellation token that aborts the close request.</param>
|
||||
Task CloseAsync(CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Adapters;
|
||||
/// </summary>
|
||||
internal interface ISubscriptionAdapter : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the server-assigned subscription identifier for diagnostics and reconnect workflows.
|
||||
/// </summary>
|
||||
uint SubscriptionId { get; }
|
||||
|
||||
/// <summary>
|
||||
@@ -23,6 +26,8 @@ internal interface ISubscriptionAdapter : IDisposable
|
||||
/// <summary>
|
||||
/// Removes a previously added monitored item by its client handle.
|
||||
/// </summary>
|
||||
/// <param name="clientHandle">The client handle returned when the monitored item was created.</param>
|
||||
/// <param name="ct">The cancellation token that aborts the monitored-item removal.</param>
|
||||
Task RemoveMonitoredItemAsync(uint clientHandle, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
@@ -40,10 +45,12 @@ internal interface ISubscriptionAdapter : IDisposable
|
||||
/// <summary>
|
||||
/// Requests a condition refresh for this subscription.
|
||||
/// </summary>
|
||||
/// <param name="ct">The cancellation token that aborts the condition refresh request.</param>
|
||||
Task ConditionRefreshAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Removes all monitored items and deletes the subscription.
|
||||
/// </summary>
|
||||
/// <param name="ct">The cancellation token that aborts subscription deletion.</param>
|
||||
Task DeleteAsync(CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,33 +9,136 @@ namespace ZB.MOM.WW.LmxOpcUa.Client.Shared;
|
||||
/// </summary>
|
||||
public interface IOpcUaClientService : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the client is currently connected to an OPC UA endpoint.
|
||||
/// </summary>
|
||||
bool IsConnected { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current connection metadata shown to CLI and UI operators after a successful connect or failover.
|
||||
/// </summary>
|
||||
ConnectionInfo? CurrentConnectionInfo { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Connects the client to the configured OPC UA endpoint set, including failover-capable endpoints when provided.
|
||||
/// </summary>
|
||||
/// <param name="settings">The endpoint, security, and authentication settings used to establish the session.</param>
|
||||
/// <param name="ct">The cancellation token that aborts the connect workflow.</param>
|
||||
Task<ConnectionInfo> ConnectAsync(ConnectionSettings settings, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Disconnects from the active OPC UA endpoint and tears down subscriptions owned by the client.
|
||||
/// </summary>
|
||||
/// <param name="ct">The cancellation token that aborts disconnect cleanup.</param>
|
||||
Task DisconnectAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Reads the current value of an OPC UA node.
|
||||
/// </summary>
|
||||
/// <param name="nodeId">The node whose value should be retrieved.</param>
|
||||
/// <param name="ct">The cancellation token that aborts the read request.</param>
|
||||
Task<DataValue> ReadValueAsync(NodeId nodeId, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Writes an operator-supplied value to an OPC UA node after applying client-side type conversion when needed.
|
||||
/// </summary>
|
||||
/// <param name="nodeId">The node whose value should be updated.</param>
|
||||
/// <param name="value">The raw value supplied by the CLI or UI workflow.</param>
|
||||
/// <param name="ct">The cancellation token that aborts the write request.</param>
|
||||
Task<StatusCode> WriteValueAsync(NodeId nodeId, object value, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Browses the children of a node so the client can build an address-space tree for operators.
|
||||
/// </summary>
|
||||
/// <param name="parentNodeId">The node to browse, or <see cref="ObjectIds.ObjectsFolder"/> when omitted.</param>
|
||||
/// <param name="ct">The cancellation token that aborts the browse request.</param>
|
||||
Task<IReadOnlyList<BrowseResult>> BrowseAsync(NodeId? parentNodeId = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to live data changes for a node.
|
||||
/// </summary>
|
||||
/// <param name="nodeId">The node whose value changes should be monitored.</param>
|
||||
/// <param name="intervalMs">The monitored-item sampling and publishing interval in milliseconds.</param>
|
||||
/// <param name="ct">The cancellation token that aborts subscription creation.</param>
|
||||
Task SubscribeAsync(NodeId nodeId, int intervalMs = 1000, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Removes a previously created live-data subscription for a node.
|
||||
/// </summary>
|
||||
/// <param name="nodeId">The node whose live-data subscription should be removed.</param>
|
||||
/// <param name="ct">The cancellation token that aborts the unsubscribe request.</param>
|
||||
Task UnsubscribeAsync(NodeId nodeId, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to OPC UA alarm and condition events for a source node or the server root.
|
||||
/// </summary>
|
||||
/// <param name="sourceNodeId">The event source to monitor, or the server object when omitted.</param>
|
||||
/// <param name="intervalMs">The publishing interval in milliseconds for the alarm subscription.</param>
|
||||
/// <param name="ct">The cancellation token that aborts alarm subscription creation.</param>
|
||||
Task SubscribeAlarmsAsync(NodeId? sourceNodeId = null, int intervalMs = 1000, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Removes the active alarm subscription.
|
||||
/// </summary>
|
||||
/// <param name="ct">The cancellation token that aborts alarm subscription cleanup.</param>
|
||||
Task UnsubscribeAlarmsAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Requests retained alarm conditions again so a client can repopulate its alarm list after reconnecting.
|
||||
/// </summary>
|
||||
/// <param name="ct">The cancellation token that aborts the condition refresh request.</param>
|
||||
Task RequestConditionRefreshAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Acknowledges an active condition using the event identifier returned by an alarm notification.
|
||||
/// </summary>
|
||||
/// <param name="conditionNodeId">The condition node associated with the alarm event being acknowledged.</param>
|
||||
/// <param name="eventId">The event identifier returned by the OPC UA server for the alarm event.</param>
|
||||
/// <param name="comment">The operator acknowledgment comment to write with the method call.</param>
|
||||
/// <param name="ct">The cancellation token that aborts the acknowledgment request.</param>
|
||||
Task<StatusCode> AcknowledgeAlarmAsync(string conditionNodeId, byte[] eventId, string comment, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Reads raw historical samples for a historized node.
|
||||
/// </summary>
|
||||
/// <param name="nodeId">The historized node whose samples should be read.</param>
|
||||
/// <param name="startTime">The inclusive start of the requested history range.</param>
|
||||
/// <param name="endTime">The inclusive end of the requested history range.</param>
|
||||
/// <param name="maxValues">The maximum number of raw values to return.</param>
|
||||
/// <param name="ct">The cancellation token that aborts the history read.</param>
|
||||
Task<IReadOnlyList<DataValue>> HistoryReadRawAsync(NodeId nodeId, DateTime startTime, DateTime endTime,
|
||||
int maxValues = 1000, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Reads aggregate historical values for a historized node using an OPC UA aggregate function.
|
||||
/// </summary>
|
||||
/// <param name="nodeId">The historized node whose processed values should be read.</param>
|
||||
/// <param name="startTime">The inclusive start of the requested processed-history range.</param>
|
||||
/// <param name="endTime">The inclusive end of the requested processed-history range.</param>
|
||||
/// <param name="aggregate">The aggregate function the operator selected for processed history.</param>
|
||||
/// <param name="intervalMs">The processing interval, in milliseconds, for each aggregate bucket.</param>
|
||||
/// <param name="ct">The cancellation token that aborts the processed history request.</param>
|
||||
Task<IReadOnlyList<DataValue>> HistoryReadAggregateAsync(NodeId nodeId, DateTime startTime, DateTime endTime,
|
||||
AggregateType aggregate, double intervalMs = 3600000, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Reads redundancy status data such as redundancy mode, service level, and partner endpoint URIs.
|
||||
/// </summary>
|
||||
/// <param name="ct">The cancellation token that aborts redundancy inspection.</param>
|
||||
Task<RedundancyInfo> GetRedundancyInfoAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Raised when a subscribed node produces a new live data value.
|
||||
/// </summary>
|
||||
event EventHandler<DataChangedEventArgs>? DataChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when an alarm or condition event is received from the server.
|
||||
/// </summary>
|
||||
event EventHandler<AlarmEventArgs>? AlarmEvent;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the client changes connection state during connect, disconnect, or failover.
|
||||
/// </summary>
|
||||
event EventHandler<ConnectionStateChangedEventArgs>? ConnectionStateChanged;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,9 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
/// <summary>
|
||||
/// Creates a new OpcUaClientService with the specified adapter dependencies.
|
||||
/// </summary>
|
||||
/// <param name="configFactory">Builds the application configuration and certificate settings for the client session.</param>
|
||||
/// <param name="endpointDiscovery">Selects the best matching endpoint for the requested URL and security mode.</param>
|
||||
/// <param name="sessionFactory">Creates the underlying OPC UA session used for runtime operations.</param>
|
||||
internal OpcUaClientService(
|
||||
IApplicationConfigurationFactory configFactory,
|
||||
IEndpointDiscovery endpointDiscovery,
|
||||
@@ -59,13 +62,22 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<DataChangedEventArgs>? DataChanged;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<AlarmEventArgs>? AlarmEvent;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<ConnectionStateChangedEventArgs>? ConnectionStateChanged;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsConnected => _state == ConnectionState.Connected && _session?.Connected == true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ConnectionInfo? CurrentConnectionInfo { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ConnectionInfo> ConnectAsync(ConnectionSettings settings, CancellationToken ct = default)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
@@ -99,6 +111,7 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task DisconnectAsync(CancellationToken ct = default)
|
||||
{
|
||||
if (_state == ConnectionState.Disconnected)
|
||||
@@ -140,6 +153,7 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<DataValue> ReadValueAsync(NodeId nodeId, CancellationToken ct = default)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
@@ -147,6 +161,7 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
return await _session!.ReadValueAsync(nodeId, ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<StatusCode> WriteValueAsync(NodeId nodeId, object value, CancellationToken ct = default)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
@@ -164,6 +179,7 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
return await _session!.WriteValueAsync(nodeId, dataValue, ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<BrowseResult>> BrowseAsync(NodeId? parentNodeId = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
@@ -200,6 +216,7 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
return results;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task SubscribeAsync(NodeId nodeId, int intervalMs = 1000, CancellationToken ct = default)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
@@ -218,6 +235,7 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
Logger.Debug("Subscribed to data changes on {NodeId}", nodeId);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task UnsubscribeAsync(NodeId nodeId, CancellationToken ct = default)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
@@ -232,6 +250,7 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
Logger.Debug("Unsubscribed from data changes on {NodeId}", nodeId);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task SubscribeAlarmsAsync(NodeId? sourceNodeId = null, int intervalMs = 1000,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
@@ -252,6 +271,7 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
Logger.Debug("Subscribed to alarm events on {NodeId}", monitorNode);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task UnsubscribeAlarmsAsync(CancellationToken ct = default)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
@@ -265,6 +285,7 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
Logger.Debug("Unsubscribed from alarm events");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task RequestConditionRefreshAsync(CancellationToken ct = default)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
@@ -277,6 +298,7 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
Logger.Debug("Condition refresh requested");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<StatusCode> AcknowledgeAlarmAsync(string conditionNodeId, byte[] eventId, string comment,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
@@ -299,6 +321,7 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
return StatusCodes.Good;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<DataValue>> HistoryReadRawAsync(
|
||||
NodeId nodeId, DateTime startTime, DateTime endTime, int maxValues = 1000, CancellationToken ct = default)
|
||||
{
|
||||
@@ -307,6 +330,7 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
return await _session!.HistoryReadRawAsync(nodeId, startTime, endTime, maxValues, ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<DataValue>> HistoryReadAggregateAsync(
|
||||
NodeId nodeId, DateTime startTime, DateTime endTime, AggregateType aggregate,
|
||||
double intervalMs = 3600000, CancellationToken ct = default)
|
||||
@@ -317,6 +341,7 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
return await _session!.HistoryReadAggregateAsync(nodeId, startTime, endTime, aggregateNodeId, intervalMs, ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<RedundancyInfo> GetRedundancyInfoAsync(CancellationToken ct = default)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
@@ -357,6 +382,9 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
return new RedundancyInfo(redundancyMode, serviceLevel, serverUris, applicationUri);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases the current session and any active monitored-item subscriptions held by the client service.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
@@ -667,4 +695,4 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
if (_state != ConnectionState.Connected || _session == null)
|
||||
throw new InvalidOperationException("Not connected to an OPC UA server.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,14 +7,53 @@ namespace ZB.MOM.WW.LmxOpcUa.Client.UI.Services;
|
||||
/// </summary>
|
||||
public sealed class UserSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the last OPC UA endpoint URL entered by the user.
|
||||
/// </summary>
|
||||
public string EndpointUrl { get; set; } = "opc.tcp://localhost:4840";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the persisted username for authenticated sessions.
|
||||
/// </summary>
|
||||
public string? Username { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the persisted password for authenticated sessions.
|
||||
/// </summary>
|
||||
public string? Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the transport security mode selected by the user.
|
||||
/// </summary>
|
||||
public SecurityMode SecurityMode { get; set; } = SecurityMode.None;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the raw failover endpoint list entered in the UI.
|
||||
/// </summary>
|
||||
public string? FailoverUrls { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the persisted session timeout, in seconds, for new client connections.
|
||||
/// </summary>
|
||||
public int SessionTimeoutSeconds { get; set; } = 60;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether untrusted server certificates should be auto-accepted.
|
||||
/// </summary>
|
||||
public bool AutoAcceptCertificates { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the certificate store path used for client certificates and trust lists.
|
||||
/// </summary>
|
||||
public string? CertificateStorePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the node IDs that should be re-subscribed when the UI reconnects.
|
||||
/// </summary>
|
||||
public List<string> SubscribedNodes { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the alarm source node that should be restored when the UI reconnects.
|
||||
/// </summary>
|
||||
public string? AlarmSourceNodeId { get; set; }
|
||||
}
|
||||
|
||||
@@ -7,6 +7,19 @@ namespace ZB.MOM.WW.LmxOpcUa.Client.UI.ViewModels;
|
||||
/// </summary>
|
||||
public class AlarmEventViewModel : ObservableObject
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates an alarm row model from an OPC UA condition event so the alarms tab can display and acknowledge it.
|
||||
/// </summary>
|
||||
/// <param name="sourceName">The source object or variable name associated with the alarm.</param>
|
||||
/// <param name="conditionName">The OPC UA condition name reported by the server.</param>
|
||||
/// <param name="severity">The alarm severity value presented to the operator.</param>
|
||||
/// <param name="message">The alarm message text shown in the UI.</param>
|
||||
/// <param name="retain">Indicates whether the server is retaining the alarm condition.</param>
|
||||
/// <param name="activeState">Indicates whether the alarm is currently active.</param>
|
||||
/// <param name="ackedState">Indicates whether the alarm has already been acknowledged.</param>
|
||||
/// <param name="time">The event timestamp associated with the alarm state.</param>
|
||||
/// <param name="eventId">The OPC UA event identifier used for acknowledgment calls.</param>
|
||||
/// <param name="conditionNodeId">The condition node identifier used when acknowledging the alarm.</param>
|
||||
public AlarmEventViewModel(
|
||||
string sourceName,
|
||||
string conditionName,
|
||||
@@ -31,15 +44,54 @@ public class AlarmEventViewModel : ObservableObject
|
||||
ConditionNodeId = conditionNodeId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the source object or variable name associated with the alarm.
|
||||
/// </summary>
|
||||
public string SourceName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the OPC UA condition name reported for the alarm event.
|
||||
/// </summary>
|
||||
public string ConditionName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the severity value shown to the operator.
|
||||
/// </summary>
|
||||
public ushort Severity { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the alarm message text shown in the UI.
|
||||
/// </summary>
|
||||
public string Message { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the server is retaining the condition.
|
||||
/// </summary>
|
||||
public bool Retain { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the alarm is currently active.
|
||||
/// </summary>
|
||||
public bool ActiveState { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the alarm has already been acknowledged.
|
||||
/// </summary>
|
||||
public bool AckedState { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the event timestamp displayed for the alarm row.
|
||||
/// </summary>
|
||||
public DateTime Time { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the OPC UA event identifier needed to acknowledge the alarm.
|
||||
/// </summary>
|
||||
public byte[]? EventId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the condition node identifier used for acknowledgment method calls.
|
||||
/// </summary>
|
||||
public string? ConditionNodeId { get; }
|
||||
|
||||
/// <summary>Whether this alarm can be acknowledged (active, not yet acked, has EventId).</summary>
|
||||
|
||||
@@ -58,6 +58,12 @@ public partial class MainWindowViewModel : ObservableObject
|
||||
|
||||
[ObservableProperty] private string? _username;
|
||||
|
||||
/// <summary>
|
||||
/// Creates the main shell view model that coordinates connection state, browsing, subscriptions, alarms, history, and persisted settings.
|
||||
/// </summary>
|
||||
/// <param name="factory">Creates the shared OPC UA client service used by all panels.</param>
|
||||
/// <param name="dispatcher">Marshals service callbacks back onto the UI thread.</param>
|
||||
/// <param name="settingsService">Loads and saves persisted user connection settings.</param>
|
||||
public MainWindowViewModel(IOpcUaClientServiceFactory factory, IUiDispatcher dispatcher,
|
||||
ISettingsService? settingsService = null)
|
||||
{
|
||||
@@ -71,21 +77,49 @@ public partial class MainWindowViewModel : ObservableObject
|
||||
/// <summary>All available security modes.</summary>
|
||||
public IReadOnlyList<SecurityMode> SecurityModes { get; } = Enum.GetValues<SecurityMode>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the shell is currently connected to an OPC UA endpoint.
|
||||
/// </summary>
|
||||
public bool IsConnected => ConnectionState == ConnectionState.Connected;
|
||||
|
||||
/// <summary>The currently selected tree nodes (supports multi-select).</summary>
|
||||
public ObservableCollection<TreeNodeViewModel> SelectedTreeNodes { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the browse-tree panel view model for the address-space explorer.
|
||||
/// </summary>
|
||||
public BrowseTreeViewModel? BrowseTree { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the read/write panel view model for point operations against the selected node.
|
||||
/// </summary>
|
||||
public ReadWriteViewModel? ReadWrite { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the subscriptions panel view model for live data monitoring.
|
||||
/// </summary>
|
||||
public SubscriptionsViewModel? Subscriptions { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the alarms panel view model for active-condition monitoring and acknowledgment.
|
||||
/// </summary>
|
||||
public AlarmsViewModel? Alarms { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the history panel view model for raw and aggregate history queries.
|
||||
/// </summary>
|
||||
public HistoryViewModel? History { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the subscriptions tab header, including the current active subscription count when nonzero.
|
||||
/// </summary>
|
||||
public string SubscriptionsTabHeader => SubscriptionCount > 0
|
||||
? $"Subscriptions ({SubscriptionCount})"
|
||||
: "Subscriptions";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the alarms tab header, including the current active alarm count when nonzero.
|
||||
/// </summary>
|
||||
public string AlarmsTabHeader => ActiveAlarmCount > 0
|
||||
? $"Alarms ({ActiveAlarmCount})"
|
||||
: "Alarms";
|
||||
@@ -355,6 +389,9 @@ public partial class MainWindowViewModel : ObservableObject
|
||||
_savedAlarmSourceNodeId = s.AlarmSourceNodeId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Persists the current connection, subscription, and alarm-monitoring settings for the next UI session.
|
||||
/// </summary>
|
||||
public void SaveSettings()
|
||||
{
|
||||
_settingsService.Save(new UserSettings
|
||||
@@ -381,4 +418,4 @@ public partial class MainWindowViewModel : ObservableObject
|
||||
.Where(u => !string.IsNullOrEmpty(u))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,11 @@ public partial class SubscriptionsViewModel : ObservableObject
|
||||
|
||||
[ObservableProperty] private int _subscriptionCount;
|
||||
|
||||
/// <summary>
|
||||
/// Creates the subscriptions panel view model used to manage live data subscriptions and ad hoc writes from the UI.
|
||||
/// </summary>
|
||||
/// <param name="service">The shared client service that performs subscribe, unsubscribe, read, and write operations.</param>
|
||||
/// <param name="dispatcher">Marshals data-change callbacks back onto the UI thread.</param>
|
||||
public SubscriptionsViewModel(IOpcUaClientService service, IUiDispatcher dispatcher)
|
||||
{
|
||||
_service = service;
|
||||
@@ -123,6 +128,8 @@ public partial class SubscriptionsViewModel : ObservableObject
|
||||
/// <summary>
|
||||
/// Subscribes to a node by ID (used by context menu). Skips if already subscribed.
|
||||
/// </summary>
|
||||
/// <param name="nodeIdStr">The node ID to subscribe to from the browse tree or persisted settings.</param>
|
||||
/// <param name="intervalMs">The monitored-item interval, in milliseconds, for the subscription.</param>
|
||||
public async Task AddSubscriptionForNodeAsync(string nodeIdStr, int intervalMs = 1000)
|
||||
{
|
||||
if (!IsConnected || string.IsNullOrWhiteSpace(nodeIdStr)) return;
|
||||
@@ -151,6 +158,9 @@ public partial class SubscriptionsViewModel : ObservableObject
|
||||
/// Subscribes to a node and all its Variable descendants recursively.
|
||||
/// Object nodes are browsed for children; Variable nodes are subscribed directly.
|
||||
/// </summary>
|
||||
/// <param name="nodeIdStr">The root node whose variables should be subscribed recursively.</param>
|
||||
/// <param name="nodeClass">The node class of the starting node so variables can be subscribed immediately.</param>
|
||||
/// <param name="intervalMs">The monitored-item interval, in milliseconds, used for created subscriptions.</param>
|
||||
public Task AddSubscriptionRecursiveAsync(string nodeIdStr, string nodeClass, int intervalMs = 1000)
|
||||
{
|
||||
return AddSubscriptionRecursiveAsync(nodeIdStr, nodeClass, intervalMs, maxDepth: 10, currentDepth: 0);
|
||||
@@ -193,6 +203,7 @@ public partial class SubscriptionsViewModel : ObservableObject
|
||||
/// <summary>
|
||||
/// Restores subscriptions from a saved list of node IDs.
|
||||
/// </summary>
|
||||
/// <param name="nodeIds">The node IDs persisted from a prior UI session.</param>
|
||||
public async Task RestoreSubscriptionsAsync(IEnumerable<string> nodeIds)
|
||||
{
|
||||
foreach (var nodeId in nodeIds)
|
||||
@@ -203,6 +214,8 @@ public partial class SubscriptionsViewModel : ObservableObject
|
||||
/// Reads the current value of a node to determine its type, validates that the raw
|
||||
/// input can be parsed to that type, writes the value, and returns (success, message).
|
||||
/// </summary>
|
||||
/// <param name="nodeIdStr">The node ID the operator wants to write.</param>
|
||||
/// <param name="rawValue">The raw text value entered by the operator.</param>
|
||||
public async Task<(bool Success, string Message)> ValidateAndWriteAsync(string nodeIdStr, string rawValue)
|
||||
{
|
||||
try
|
||||
@@ -259,4 +272,4 @@ public partial class SubscriptionsViewModel : ObservableObject
|
||||
{
|
||||
_service.DataChanged -= OnDataChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,14 +69,22 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
|
||||
public Exception? WriteException { get; set; }
|
||||
public Exception? ConditionRefreshException { get; set; }
|
||||
|
||||
// IOpcUaClientService implementation
|
||||
/// <inheritdoc />
|
||||
public bool IsConnected => ConnectCalled && !DisconnectCalled;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ConnectionInfo? CurrentConnectionInfo => ConnectCalled ? ConnectionInfoResult : null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<DataChangedEventArgs>? DataChanged;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<AlarmEventArgs>? AlarmEvent;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<ConnectionStateChangedEventArgs>? ConnectionStateChanged;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<ConnectionInfo> ConnectAsync(ConnectionSettings settings, CancellationToken ct = default)
|
||||
{
|
||||
ConnectCalled = true;
|
||||
@@ -85,12 +93,14 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
|
||||
return Task.FromResult(ConnectionInfoResult);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task DisconnectAsync(CancellationToken ct = default)
|
||||
{
|
||||
DisconnectCalled = true;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<DataValue> ReadValueAsync(NodeId nodeId, CancellationToken ct = default)
|
||||
{
|
||||
ReadNodeIds.Add(nodeId);
|
||||
@@ -98,6 +108,7 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
|
||||
return Task.FromResult(ReadValueResult);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<StatusCode> WriteValueAsync(NodeId nodeId, object value, CancellationToken ct = default)
|
||||
{
|
||||
WriteValues.Add((nodeId, value));
|
||||
@@ -105,36 +116,42 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
|
||||
return Task.FromResult(WriteStatusCodeResult);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<BrowseResult>> BrowseAsync(NodeId? parentNodeId = null, CancellationToken ct = default)
|
||||
{
|
||||
BrowseNodeIds.Add(parentNodeId);
|
||||
return Task.FromResult(BrowseResults);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SubscribeAsync(NodeId nodeId, int intervalMs = 1000, CancellationToken ct = default)
|
||||
{
|
||||
SubscribeCalls.Add((nodeId, intervalMs));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UnsubscribeAsync(NodeId nodeId, CancellationToken ct = default)
|
||||
{
|
||||
UnsubscribeCalls.Add(nodeId);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SubscribeAlarmsAsync(NodeId? sourceNodeId = null, int intervalMs = 1000, CancellationToken ct = default)
|
||||
{
|
||||
SubscribeAlarmsCalls.Add((sourceNodeId, intervalMs));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UnsubscribeAlarmsAsync(CancellationToken ct = default)
|
||||
{
|
||||
UnsubscribeAlarmsCalled = true;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task RequestConditionRefreshAsync(CancellationToken ct = default)
|
||||
{
|
||||
RequestConditionRefreshCalled = true;
|
||||
@@ -142,12 +159,14 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<StatusCode> AcknowledgeAlarmAsync(string conditionNodeId, byte[] eventId, string comment,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return Task.FromResult(new StatusCode(StatusCodes.Good));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<DataValue>> HistoryReadRawAsync(
|
||||
NodeId nodeId, DateTime startTime, DateTime endTime, int maxValues = 1000, CancellationToken ct = default)
|
||||
{
|
||||
@@ -155,6 +174,7 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
|
||||
return Task.FromResult(HistoryReadResult);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<DataValue>> HistoryReadAggregateAsync(
|
||||
NodeId nodeId, DateTime startTime, DateTime endTime, AggregateType aggregate,
|
||||
double intervalMs = 3600000, CancellationToken ct = default)
|
||||
@@ -163,12 +183,16 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
|
||||
return Task.FromResult(HistoryReadResult);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<RedundancyInfo> GetRedundancyInfoAsync(CancellationToken ct = default)
|
||||
{
|
||||
GetRedundancyInfoCalled = true;
|
||||
return Task.FromResult(RedundancyInfoResult);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks the fake client as disposed so CLI command tests can assert cleanup behavior.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
DisposeCalled = true;
|
||||
@@ -191,4 +215,4 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
|
||||
{
|
||||
ConnectionStateChanged?.Invoke(this, new ConnectionStateChangedEventArgs(oldState, newState, endpointUrl));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,22 @@ using ZB.MOM.WW.LmxOpcUa.Client.Shared.Adapters;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Tests.Fakes;
|
||||
|
||||
/// <summary>
|
||||
/// Test double for <see cref="ISessionAdapter" /> used to simulate reads, writes, browsing, history, and failover callbacks.
|
||||
/// </summary>
|
||||
internal sealed class FakeSessionAdapter : ISessionAdapter
|
||||
{
|
||||
private readonly List<FakeSubscriptionAdapter> _createdSubscriptions = [];
|
||||
private Action<bool>? _keepAliveCallback;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the fake session has been closed through the client disconnect path.
|
||||
/// </summary>
|
||||
public bool Closed { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the fake session has been disposed.
|
||||
/// </summary>
|
||||
public bool Disposed { get; private set; }
|
||||
public int ReadCount { get; private set; }
|
||||
public int WriteCount { get; private set; }
|
||||
@@ -38,27 +48,47 @@ internal sealed class FakeSessionAdapter : ISessionAdapter
|
||||
public bool ThrowOnHistoryReadAggregate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The next FakeSubscriptionAdapter to return from CreateSubscriptionAsync.
|
||||
/// If null, a new one is created automatically.
|
||||
/// Gets or sets the next fake subscription returned when the client creates a monitored-item subscription.
|
||||
/// If unset, the fake builds a new subscription automatically.
|
||||
/// </summary>
|
||||
public FakeSubscriptionAdapter? NextSubscription { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the fake subscriptions created by this session so tests can inspect replay and cleanup behavior.
|
||||
/// </summary>
|
||||
public IReadOnlyList<FakeSubscriptionAdapter> CreatedSubscriptions => _createdSubscriptions;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Connected { get; set; } = true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string SessionId { get; set; } = "ns=0;i=12345";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string SessionName { get; set; } = "FakeSession";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string EndpointUrl { get; set; } = "opc.tcp://localhost:4840";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string ServerName { get; set; } = "FakeServer";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string SecurityMode { get; set; } = "None";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string SecurityPolicyUri { get; set; } = "http://opcfoundation.org/UA/SecurityPolicy#None";
|
||||
|
||||
/// <inheritdoc />
|
||||
public NamespaceTable NamespaceUris { get; set; } = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RegisterKeepAliveHandler(Action<bool> callback)
|
||||
{
|
||||
_keepAliveCallback = callback;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<DataValue> ReadValueAsync(NodeId nodeId, CancellationToken ct)
|
||||
{
|
||||
ReadCount++;
|
||||
@@ -71,6 +101,7 @@ internal sealed class FakeSessionAdapter : ISessionAdapter
|
||||
return Task.FromResult(ReadResponse ?? new DataValue(new Variant(0), StatusCodes.Good));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<StatusCode> WriteValueAsync(NodeId nodeId, DataValue value, CancellationToken ct)
|
||||
{
|
||||
WriteCount++;
|
||||
@@ -79,6 +110,7 @@ internal sealed class FakeSessionAdapter : ISessionAdapter
|
||||
return Task.FromResult(WriteResponse);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<(byte[]? ContinuationPoint, ReferenceDescriptionCollection References)> BrowseAsync(
|
||||
NodeId nodeId, uint nodeClassMask, CancellationToken ct)
|
||||
{
|
||||
@@ -88,6 +120,7 @@ internal sealed class FakeSessionAdapter : ISessionAdapter
|
||||
return Task.FromResult((BrowseContinuationPoint, BrowseResponse));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<(byte[]? ContinuationPoint, ReferenceDescriptionCollection References)> BrowseNextAsync(
|
||||
byte[] continuationPoint, CancellationToken ct)
|
||||
{
|
||||
@@ -95,12 +128,14 @@ internal sealed class FakeSessionAdapter : ISessionAdapter
|
||||
return Task.FromResult((BrowseNextContinuationPoint, BrowseNextResponse));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<bool> HasChildrenAsync(NodeId nodeId, CancellationToken ct)
|
||||
{
|
||||
HasChildrenCount++;
|
||||
return Task.FromResult(HasChildrenResponse);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<DataValue>> HistoryReadRawAsync(
|
||||
NodeId nodeId, DateTime startTime, DateTime endTime, int maxValues, CancellationToken ct)
|
||||
{
|
||||
@@ -110,6 +145,7 @@ internal sealed class FakeSessionAdapter : ISessionAdapter
|
||||
return Task.FromResult<IReadOnlyList<DataValue>>(HistoryReadRawResponse);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<DataValue>> HistoryReadAggregateAsync(
|
||||
NodeId nodeId, DateTime startTime, DateTime endTime, NodeId aggregateId, double intervalMs,
|
||||
CancellationToken ct)
|
||||
@@ -120,6 +156,7 @@ internal sealed class FakeSessionAdapter : ISessionAdapter
|
||||
return Task.FromResult<IReadOnlyList<DataValue>>(HistoryReadAggregateResponse);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<ISubscriptionAdapter> CreateSubscriptionAsync(int publishingIntervalMs, CancellationToken ct)
|
||||
{
|
||||
var sub = NextSubscription ?? new FakeSubscriptionAdapter();
|
||||
@@ -128,12 +165,14 @@ internal sealed class FakeSessionAdapter : ISessionAdapter
|
||||
return Task.FromResult<ISubscriptionAdapter>(sub);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IList<object>?> CallMethodAsync(NodeId objectId, NodeId methodId, object[] inputArguments,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return Task.FromResult<IList<object>?>(null);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task CloseAsync(CancellationToken ct)
|
||||
{
|
||||
Closed = true;
|
||||
@@ -141,6 +180,9 @@ internal sealed class FakeSessionAdapter : ISessionAdapter
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks the fake session as disposed so tests can verify cleanup after disconnect or failover.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Disposed = true;
|
||||
@@ -154,4 +196,4 @@ internal sealed class FakeSessionAdapter : ISessionAdapter
|
||||
{
|
||||
_keepAliveCallback?.Invoke(isGood);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ using ZB.MOM.WW.LmxOpcUa.Client.Shared.Adapters;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Tests.Fakes;
|
||||
|
||||
/// <summary>
|
||||
/// Test double for <see cref="ISubscriptionAdapter" /> used to drive monitored-item behavior in shared-client tests.
|
||||
/// </summary>
|
||||
internal sealed class FakeSubscriptionAdapter : ISubscriptionAdapter
|
||||
{
|
||||
private readonly
|
||||
@@ -10,8 +13,19 @@ internal sealed class FakeSubscriptionAdapter : ISubscriptionAdapter
|
||||
)> _items = new();
|
||||
|
||||
private uint _nextHandle = 100;
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the fake subscription has been deleted.
|
||||
/// </summary>
|
||||
public bool Deleted { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether a condition refresh was requested by the client under test.
|
||||
/// </summary>
|
||||
public bool ConditionRefreshCalled { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether condition refresh should throw to simulate unsupported servers.
|
||||
/// </summary>
|
||||
public bool ThrowOnConditionRefresh { get; set; }
|
||||
public int AddDataChangeCount { get; private set; }
|
||||
public int AddEventCount { get; private set; }
|
||||
@@ -22,8 +36,10 @@ internal sealed class FakeSubscriptionAdapter : ISubscriptionAdapter
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<uint> ActiveHandles => _items.Keys.ToList();
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint SubscriptionId { get; set; } = 42;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<uint> AddDataChangeMonitoredItemAsync(NodeId nodeId, int samplingIntervalMs,
|
||||
Action<string, DataValue> onDataChange, CancellationToken ct)
|
||||
{
|
||||
@@ -33,6 +49,7 @@ internal sealed class FakeSubscriptionAdapter : ISubscriptionAdapter
|
||||
return Task.FromResult(handle);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task RemoveMonitoredItemAsync(uint clientHandle, CancellationToken ct)
|
||||
{
|
||||
RemoveCount++;
|
||||
@@ -40,6 +57,7 @@ internal sealed class FakeSubscriptionAdapter : ISubscriptionAdapter
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<uint> AddEventMonitoredItemAsync(NodeId nodeId, int samplingIntervalMs, EventFilter filter,
|
||||
Action<EventFieldList> onEvent, CancellationToken ct)
|
||||
{
|
||||
@@ -49,6 +67,7 @@ internal sealed class FakeSubscriptionAdapter : ISubscriptionAdapter
|
||||
return Task.FromResult(handle);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task ConditionRefreshAsync(CancellationToken ct)
|
||||
{
|
||||
ConditionRefreshCalled = true;
|
||||
@@ -57,6 +76,7 @@ internal sealed class FakeSubscriptionAdapter : ISubscriptionAdapter
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task DeleteAsync(CancellationToken ct)
|
||||
{
|
||||
Deleted = true;
|
||||
@@ -64,6 +84,9 @@ internal sealed class FakeSubscriptionAdapter : ISubscriptionAdapter
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears tracked monitored items when the fake subscription is disposed by the client under test.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_items.Clear();
|
||||
@@ -85,4 +108,4 @@ internal sealed class FakeSubscriptionAdapter : ISubscriptionAdapter
|
||||
{
|
||||
if (_items.TryGetValue(handle, out var item) && item.EventCallback != null) item.EventCallback(eventFields);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@ using ZB.MOM.WW.LmxOpcUa.Client.Shared.Tests.Fakes;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the shared OPC UA client service behaviors for connection management, browsing, subscriptions, history, alarms, and redundancy.
|
||||
/// </summary>
|
||||
public class OpcUaClientServiceTests : IDisposable
|
||||
{
|
||||
private readonly FakeApplicationConfigurationFactory _configFactory = new();
|
||||
@@ -18,6 +21,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
_service = new OpcUaClientService(_configFactory, _endpointDiscovery, _sessionFactory);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases the shared client service after each test so session and subscription state do not leak between scenarios.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_service.Dispose();
|
||||
@@ -34,6 +40,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
|
||||
// --- Connection tests ---
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that a valid connection request returns populated connection metadata and marks the client as connected.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ConnectAsync_ValidSettings_ReturnsConnectionInfo()
|
||||
{
|
||||
@@ -45,6 +54,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
_service.CurrentConnectionInfo.ShouldBe(info);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that invalid connection settings fail validation before any OPC UA session is created.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ConnectAsync_InvalidSettings_ThrowsBeforeCreatingSession()
|
||||
{
|
||||
@@ -55,6 +67,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
_service.IsConnected.ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that server and security details from the session are copied into the exposed connection info.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ConnectAsync_PopulatesConnectionInfo()
|
||||
{
|
||||
@@ -77,6 +92,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
info.SessionName.ShouldBe("TestSession");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that connection-state transitions are raised for the connecting and connected phases.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ConnectAsync_RaisesConnectionStateChangedEvents()
|
||||
{
|
||||
@@ -92,6 +110,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
events[1].NewState.ShouldBe(ConnectionState.Connected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that a failed session creation leaves the client in the disconnected state.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ConnectAsync_SessionFactoryFails_TransitionsToDisconnected()
|
||||
{
|
||||
@@ -105,6 +126,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
events.Last().NewState.ShouldBe(ConnectionState.Disconnected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that username and password settings are passed through to the session-creation pipeline.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ConnectAsync_WithUsername_PassesThroughToFactory()
|
||||
{
|
||||
@@ -120,6 +144,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
|
||||
// --- Disconnect tests ---
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that disconnect closes the active session and clears exposed connection state.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task DisconnectAsync_WhenConnected_ClosesSession()
|
||||
{
|
||||
@@ -133,6 +160,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
_service.CurrentConnectionInfo.ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that disconnect is safe to call when no server session is active.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task DisconnectAsync_WhenNotConnected_IsIdempotent()
|
||||
{
|
||||
@@ -140,6 +170,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
_service.IsConnected.ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that repeated disconnect calls do not throw after cleanup has already run.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task DisconnectAsync_CalledTwice_IsIdempotent()
|
||||
{
|
||||
@@ -150,6 +183,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
|
||||
// --- Read tests ---
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that a connected client can read the current value of a node through the session adapter.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ReadValueAsync_WhenConnected_ReturnsValue()
|
||||
{
|
||||
@@ -166,6 +202,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
session.ReadCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that reads are rejected when the client is not connected to a server.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ReadValueAsync_WhenDisconnected_Throws()
|
||||
{
|
||||
@@ -173,6 +212,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
_service.ReadValueAsync(new NodeId("ns=2;s=MyNode")));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that session-level read failures are surfaced to callers instead of being swallowed.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ReadValueAsync_SessionThrows_PropagatesException()
|
||||
{
|
||||
@@ -186,6 +228,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
|
||||
// --- Write tests ---
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that writes succeed through the session adapter when the client is connected.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task WriteValueAsync_WhenConnected_WritesValue()
|
||||
{
|
||||
@@ -203,6 +248,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
session.WriteCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that string inputs are coerced to the node's current data type before writing.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task WriteValueAsync_StringValue_CoercesToTargetType()
|
||||
{
|
||||
@@ -219,6 +267,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
session.ReadCount.ShouldBe(1); // Read for type inference
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that non-string values are written directly without an extra type-inference read.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task WriteValueAsync_NonStringValue_WritesDirectly()
|
||||
{
|
||||
@@ -232,6 +283,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
session.ReadCount.ShouldBe(0); // No read for non-string values
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that writes are rejected when the client is disconnected.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task WriteValueAsync_WhenDisconnected_Throws()
|
||||
{
|
||||
@@ -241,6 +295,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
|
||||
// --- Browse tests ---
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that browse results are mapped into the client browse model used by CLI and UI consumers.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task BrowseAsync_WhenConnected_ReturnsMappedResults()
|
||||
{
|
||||
@@ -267,6 +324,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
results[0].HasChildren.ShouldBeFalse(); // Variable nodes don't check HasChildren
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that a null browse root defaults to the OPC UA Objects folder.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task BrowseAsync_NullParent_UsesObjectsFolder()
|
||||
{
|
||||
@@ -282,6 +342,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
session.BrowseCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that object nodes trigger child-detection checks so the client can mark expandable branches.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task BrowseAsync_ObjectNode_ChecksHasChildren()
|
||||
{
|
||||
@@ -307,6 +370,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
session.HasChildrenCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that browse continuation points are followed so multi-page address-space branches are fully returned.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task BrowseAsync_WithContinuationPoint_FollowsIt()
|
||||
{
|
||||
@@ -342,6 +408,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
session.BrowseNextCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that browse requests are rejected when the client is disconnected.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task BrowseAsync_WhenDisconnected_Throws()
|
||||
{
|
||||
@@ -350,6 +419,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
|
||||
// --- Subscribe tests ---
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that subscribing to a node creates a monitored item on a data-change subscription.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task SubscribeAsync_CreatesSubscription()
|
||||
{
|
||||
@@ -363,6 +435,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
session.CreatedSubscriptions[0].AddDataChangeCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that duplicate subscribe requests for the same node do not create duplicate monitored items.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task SubscribeAsync_DuplicateNode_IsIdempotent()
|
||||
{
|
||||
@@ -376,6 +451,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
session.CreatedSubscriptions[0].AddDataChangeCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that data-change notifications from the subscription are raised through the shared client event.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task SubscribeAsync_RaisesDataChangedEvent()
|
||||
{
|
||||
@@ -398,6 +476,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
received.Value.Value.ShouldBe(99);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that unsubscribing removes the corresponding monitored item from the active subscription.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task UnsubscribeAsync_RemovesMonitoredItem()
|
||||
{
|
||||
@@ -412,6 +493,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
fakeSub.RemoveCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that unsubscribing an unknown node is treated as a safe no-op.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task UnsubscribeAsync_WhenNotSubscribed_DoesNotThrow()
|
||||
{
|
||||
@@ -423,6 +507,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
// Should not throw
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that data subscriptions cannot be created while the client is disconnected.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task SubscribeAsync_WhenDisconnected_Throws()
|
||||
{
|
||||
@@ -432,6 +519,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
|
||||
// --- Alarm subscription tests ---
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that alarm subscription requests create an event monitored item on the session.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task SubscribeAlarmsAsync_CreatesEventSubscription()
|
||||
{
|
||||
@@ -445,6 +535,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
session.CreatedSubscriptions[0].AddEventCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that duplicate alarm-subscription requests do not create duplicate event subscriptions.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task SubscribeAlarmsAsync_Duplicate_IsIdempotent()
|
||||
{
|
||||
@@ -458,6 +551,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
session.CreatedSubscriptions.Count.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that OPC UA event notifications are mapped into the shared client alarm event model.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task SubscribeAlarmsAsync_RaisesAlarmEvent()
|
||||
{
|
||||
@@ -503,6 +599,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
received.AckedState.ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that removing alarm monitoring deletes the underlying event subscription.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task UnsubscribeAlarmsAsync_DeletesSubscription()
|
||||
{
|
||||
@@ -517,6 +616,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
fakeSub.Deleted.ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that removing alarms is safe even when no alarm subscription exists.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task UnsubscribeAlarmsAsync_WhenNoSubscription_DoesNotThrow()
|
||||
{
|
||||
@@ -527,6 +629,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
await _service.UnsubscribeAlarmsAsync(); // Should not throw
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that condition refresh requests are forwarded to the active alarm subscription.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task RequestConditionRefreshAsync_CallsAdapter()
|
||||
{
|
||||
@@ -541,6 +646,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
fakeSub.ConditionRefreshCalled.ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that condition refresh fails fast when no alarm subscription is active.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task RequestConditionRefreshAsync_NoAlarmSubscription_Throws()
|
||||
{
|
||||
@@ -552,6 +660,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
_service.RequestConditionRefreshAsync());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that alarm subscriptions cannot be created while disconnected.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task SubscribeAlarmsAsync_WhenDisconnected_Throws()
|
||||
{
|
||||
@@ -561,6 +672,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
|
||||
// --- History read tests ---
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that raw history reads return the session-provided values.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task HistoryReadRawAsync_ReturnsValues()
|
||||
{
|
||||
@@ -580,6 +694,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
session.HistoryReadRawCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that raw history reads are rejected while disconnected.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task HistoryReadRawAsync_WhenDisconnected_Throws()
|
||||
{
|
||||
@@ -587,6 +704,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
_service.HistoryReadRawAsync(new NodeId("ns=2;s=Temp"), DateTime.UtcNow.AddHours(-1), DateTime.UtcNow));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that raw-history failures from the session are propagated to callers.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task HistoryReadRawAsync_SessionThrows_PropagatesException()
|
||||
{
|
||||
@@ -598,6 +718,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
_service.HistoryReadRawAsync(new NodeId("ns=2;s=Temp"), DateTime.UtcNow.AddHours(-1), DateTime.UtcNow));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that aggregate history reads return the processed values from the session adapter.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task HistoryReadAggregateAsync_ReturnsValues()
|
||||
{
|
||||
@@ -617,6 +740,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
session.HistoryReadAggregateCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that aggregate history reads are rejected while disconnected.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task HistoryReadAggregateAsync_WhenDisconnected_Throws()
|
||||
{
|
||||
@@ -626,6 +752,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
AggregateType.Average));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that aggregate-history failures from the session are propagated to callers.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task HistoryReadAggregateAsync_SessionThrows_PropagatesException()
|
||||
{
|
||||
@@ -641,6 +770,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
|
||||
// --- Redundancy tests ---
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that redundancy mode, service level, and server URIs are read from the standard OPC UA redundancy nodes.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task GetRedundancyInfoAsync_ReturnsInfo()
|
||||
{
|
||||
@@ -670,6 +802,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
info.ApplicationUri.ShouldBe("urn:server1");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that missing optional redundancy arrays do not prevent a redundancy snapshot from being returned.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task GetRedundancyInfoAsync_MissingOptionalArrays_ReturnsGracefully()
|
||||
{
|
||||
@@ -697,6 +832,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
info.ApplicationUri.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that redundancy inspection is rejected while disconnected.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task GetRedundancyInfoAsync_WhenDisconnected_Throws()
|
||||
{
|
||||
@@ -706,6 +844,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
|
||||
// --- Failover tests ---
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that a keep-alive failure moves the client to a configured failover endpoint.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task KeepAliveFailure_TriggersFailover()
|
||||
{
|
||||
@@ -734,6 +875,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
e.EndpointUrl == "opc.tcp://backup:4840");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that connection metadata is refreshed to reflect the newly active failover endpoint.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task KeepAliveFailure_UpdatesConnectionInfo()
|
||||
{
|
||||
@@ -757,6 +901,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
_service.CurrentConnectionInfo.ServerName.ShouldBe("BackupServer");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the client falls back to disconnected when every failover endpoint is unreachable.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task KeepAliveFailure_AllEndpointsFail_TransitionsToDisconnected()
|
||||
{
|
||||
@@ -775,6 +922,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
|
||||
// --- Dispose tests ---
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that dispose releases the underlying session and clears exposed connection state.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Dispose_CleansUpResources()
|
||||
{
|
||||
@@ -788,12 +938,18 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
_service.CurrentConnectionInfo.ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that dispose is safe to call even when no connection was established.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Dispose_WhenNotConnected_DoesNotThrow()
|
||||
{
|
||||
_service.Dispose(); // Should not throw
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that public operations reject use after the shared client has been disposed.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task OperationsAfterDispose_Throw()
|
||||
{
|
||||
@@ -807,6 +963,9 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
|
||||
// --- Factory tests ---
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the factory creates a usable shared OPC UA client service instance.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void OpcUaClientServiceFactory_CreatesService()
|
||||
{
|
||||
@@ -816,4 +975,4 @@ public class OpcUaClientServiceTests : IDisposable
|
||||
service.ShouldBeAssignableTo<IOpcUaClientService>();
|
||||
service.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,27 +6,81 @@ using BrowseResult = ZB.MOM.WW.LmxOpcUa.Client.Shared.Models.BrowseResult;
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.UI.Tests.Fakes;
|
||||
|
||||
/// <summary>
|
||||
/// Fake IOpcUaClientService for unit testing.
|
||||
/// Test double for the shared OPC UA client service used by UI view-model tests.
|
||||
/// It lets tests script connection, browse, history, redundancy, and alarm behavior without a live server.
|
||||
/// </summary>
|
||||
public sealed class FakeOpcUaClientService : IOpcUaClientService
|
||||
{
|
||||
// Configurable responses
|
||||
/// <summary>
|
||||
/// Gets or sets the connection metadata returned when a UI test performs a successful connect.
|
||||
/// </summary>
|
||||
public ConnectionInfo? ConnectResult { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the exception thrown to simulate a failed connect workflow in the UI.
|
||||
/// </summary>
|
||||
public Exception? ConnectException { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default browse results returned when no parent-specific branch is configured.
|
||||
/// </summary>
|
||||
public IReadOnlyList<BrowseResult> BrowseResults { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets browse results keyed by parent node so tree-navigation tests can model multiple address-space branches.
|
||||
/// </summary>
|
||||
public Dictionary<string, IReadOnlyList<BrowseResult>> BrowseResultsByParent { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the exception thrown to simulate browse failures in the UI tree.
|
||||
/// </summary>
|
||||
public Exception? BrowseException { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the current value returned by point-read tests.
|
||||
/// </summary>
|
||||
public DataValue ReadResult { get; set; } =
|
||||
new(new Variant(42), StatusCodes.Good, DateTime.UtcNow, DateTime.UtcNow);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the exception thrown to simulate point-read failures in the UI.
|
||||
/// </summary>
|
||||
public Exception? ReadException { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the status code returned by write operations in UI tests.
|
||||
/// </summary>
|
||||
public StatusCode WriteResult { get; set; } = StatusCodes.Good;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the exception thrown to simulate failed write workflows in the UI.
|
||||
/// </summary>
|
||||
public Exception? WriteException { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the redundancy snapshot returned when the UI inspects server redundancy state.
|
||||
/// </summary>
|
||||
public RedundancyInfo? RedundancyResult { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the exception thrown to simulate redundancy lookup failures.
|
||||
/// </summary>
|
||||
public Exception? RedundancyException { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the raw historical values returned to history-view tests.
|
||||
/// </summary>
|
||||
public IReadOnlyList<DataValue> HistoryRawResult { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the aggregate historical values returned to history-view tests.
|
||||
/// </summary>
|
||||
public IReadOnlyList<DataValue> HistoryAggregateResult { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the exception thrown to simulate historical-data failures in the UI.
|
||||
/// </summary>
|
||||
public Exception? HistoryException { get; set; }
|
||||
|
||||
// Call tracking
|
||||
@@ -54,13 +108,22 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
|
||||
public NodeId? LastUnsubscribeNodeId { get; private set; }
|
||||
public AggregateType? LastAggregateType { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsConnected { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public ConnectionInfo? CurrentConnectionInfo { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<DataChangedEventArgs>? DataChanged;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<AlarmEventArgs>? AlarmEvent;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<ConnectionStateChangedEventArgs>? ConnectionStateChanged;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<ConnectionInfo> ConnectAsync(ConnectionSettings settings, CancellationToken ct = default)
|
||||
{
|
||||
ConnectCallCount++;
|
||||
@@ -71,6 +134,7 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
|
||||
return Task.FromResult(ConnectResult!);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task DisconnectAsync(CancellationToken ct = default)
|
||||
{
|
||||
DisconnectCallCount++;
|
||||
@@ -79,6 +143,7 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<DataValue> ReadValueAsync(NodeId nodeId, CancellationToken ct = default)
|
||||
{
|
||||
ReadCallCount++;
|
||||
@@ -87,6 +152,7 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
|
||||
return Task.FromResult(ReadResult);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<StatusCode> WriteValueAsync(NodeId nodeId, object value, CancellationToken ct = default)
|
||||
{
|
||||
WriteCallCount++;
|
||||
@@ -96,6 +162,7 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
|
||||
return Task.FromResult(WriteResult);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<BrowseResult>> BrowseAsync(NodeId? parentNodeId = null, CancellationToken ct = default)
|
||||
{
|
||||
BrowseCallCount++;
|
||||
@@ -108,6 +175,7 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
|
||||
return Task.FromResult(BrowseResults);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SubscribeAsync(NodeId nodeId, int intervalMs = 1000, CancellationToken ct = default)
|
||||
{
|
||||
SubscribeCallCount++;
|
||||
@@ -116,6 +184,7 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UnsubscribeAsync(NodeId nodeId, CancellationToken ct = default)
|
||||
{
|
||||
UnsubscribeCallCount++;
|
||||
@@ -123,18 +192,21 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SubscribeAlarmsAsync(NodeId? sourceNodeId = null, int intervalMs = 1000, CancellationToken ct = default)
|
||||
{
|
||||
SubscribeAlarmsCallCount++;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UnsubscribeAlarmsAsync(CancellationToken ct = default)
|
||||
{
|
||||
UnsubscribeAlarmsCallCount++;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task RequestConditionRefreshAsync(CancellationToken ct = default)
|
||||
{
|
||||
RequestConditionRefreshCallCount++;
|
||||
@@ -145,6 +217,7 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
|
||||
public Exception? AcknowledgeException { get; set; }
|
||||
public int AcknowledgeCallCount { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<StatusCode> AcknowledgeAlarmAsync(string conditionNodeId, byte[] eventId, string comment,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
@@ -153,6 +226,7 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
|
||||
return Task.FromResult(AcknowledgeResult);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<DataValue>> HistoryReadRawAsync(NodeId nodeId, DateTime startTime, DateTime endTime,
|
||||
int maxValues = 1000, CancellationToken ct = default)
|
||||
{
|
||||
@@ -161,6 +235,7 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
|
||||
return Task.FromResult(HistoryRawResult);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<DataValue>> HistoryReadAggregateAsync(NodeId nodeId, DateTime startTime, DateTime endTime,
|
||||
AggregateType aggregate, double intervalMs = 3600000, CancellationToken ct = default)
|
||||
{
|
||||
@@ -170,6 +245,7 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
|
||||
return Task.FromResult(HistoryAggregateResult);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<RedundancyInfo> GetRedundancyInfoAsync(CancellationToken ct = default)
|
||||
{
|
||||
GetRedundancyInfoCallCount++;
|
||||
@@ -177,24 +253,35 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
|
||||
return Task.FromResult(RedundancyResult!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases fake service resources at the end of a UI test run.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
// No-op for testing
|
||||
}
|
||||
|
||||
// Methods to raise events from tests
|
||||
/// <summary>
|
||||
/// Raises a simulated data-change notification so UI tests can validate live update handling.
|
||||
/// </summary>
|
||||
public void RaiseDataChanged(DataChangedEventArgs args)
|
||||
{
|
||||
DataChanged?.Invoke(this, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises a simulated alarm event so UI tests can validate alarm-list behavior.
|
||||
/// </summary>
|
||||
public void RaiseAlarmEvent(AlarmEventArgs args)
|
||||
{
|
||||
AlarmEvent?.Invoke(this, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises a simulated connection-state transition so UI tests can validate status presentation and failover behavior.
|
||||
/// </summary>
|
||||
public void RaiseConnectionStateChanged(ConnectionStateChangedEventArgs args)
|
||||
{
|
||||
ConnectionStateChanged?.Invoke(this, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,9 @@ using ConnectionState = ZB.MOM.WW.LmxOpcUa.Client.Shared.Models.ConnectionState;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Client.UI.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the main UI shell behavior for connection state, settings persistence, browsing, subscriptions, and history navigation.
|
||||
/// </summary>
|
||||
public class MainWindowViewModelTests
|
||||
{
|
||||
private readonly FakeOpcUaClientService _service;
|
||||
@@ -39,6 +42,9 @@ public class MainWindowViewModelTests
|
||||
_vm = new MainWindowViewModel(factory, dispatcher, _settingsService);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the shell starts disconnected with the default endpoint and status text.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DefaultState_IsDisconnected()
|
||||
{
|
||||
@@ -48,18 +54,27 @@ public class MainWindowViewModelTests
|
||||
_vm.StatusMessage.ShouldBe("Disconnected");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the connect command is available before a session is established.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ConnectCommand_CanExecute_WhenDisconnected()
|
||||
{
|
||||
_vm.ConnectCommand.CanExecute(null).ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that disconnect is disabled until a server session is active.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DisconnectCommand_CannotExecute_WhenDisconnected()
|
||||
{
|
||||
_vm.DisconnectCommand.CanExecute(null).ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that a successful connect command updates the shell into the connected state.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ConnectCommand_TransitionsToConnected()
|
||||
{
|
||||
@@ -70,6 +85,9 @@ public class MainWindowViewModelTests
|
||||
_service.ConnectCallCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the initial browse tree is loaded after a successful connect.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ConnectCommand_LoadsRootNodes()
|
||||
{
|
||||
@@ -79,6 +97,9 @@ public class MainWindowViewModelTests
|
||||
_vm.BrowseTree.RootNodes[0].DisplayName.ShouldBe("Root");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that redundancy details are fetched and exposed after connecting.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ConnectCommand_FetchesRedundancyInfo()
|
||||
{
|
||||
@@ -89,6 +110,9 @@ public class MainWindowViewModelTests
|
||||
_vm.RedundancyInfo.ServiceLevel.ShouldBe((byte)200);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the session label shows the connected server and session identity.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ConnectCommand_SetsSessionLabel()
|
||||
{
|
||||
@@ -98,6 +122,9 @@ public class MainWindowViewModelTests
|
||||
_vm.SessionLabel.ShouldContain("TestSession");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that disconnect returns the shell to the disconnected state.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task DisconnectCommand_TransitionsToDisconnected()
|
||||
{
|
||||
@@ -109,6 +136,9 @@ public class MainWindowViewModelTests
|
||||
_service.DisconnectCallCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that disconnect clears session-specific UI state such as browse data and redundancy details.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Disconnect_ClearsStateAndChildren()
|
||||
{
|
||||
@@ -121,6 +151,9 @@ public class MainWindowViewModelTests
|
||||
_vm.SubscriptionCount.ShouldBe(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that connection-state events from the client update the shell status text and state.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ConnectionStateChangedEvent_UpdatesState()
|
||||
{
|
||||
@@ -134,6 +167,9 @@ public class MainWindowViewModelTests
|
||||
_vm.StatusMessage.ShouldBe("Reconnecting...");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that selecting a tree node updates the dependent read/write and history panels.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task SelectedTreeNode_PropagatesToChildViewModels()
|
||||
{
|
||||
@@ -146,6 +182,9 @@ public class MainWindowViewModelTests
|
||||
_vm.History.SelectedNodeId.ShouldBe(node.NodeId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that a successful connect propagates connected state into the child tabs.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ConnectCommand_PropagatesIsConnectedToChildViewModels()
|
||||
{
|
||||
@@ -157,6 +196,9 @@ public class MainWindowViewModelTests
|
||||
_vm.History.IsConnected.ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that disconnect propagates disconnected state into the child tabs.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task DisconnectCommand_PropagatesIsConnectedFalseToChildViewModels()
|
||||
{
|
||||
@@ -169,6 +211,9 @@ public class MainWindowViewModelTests
|
||||
_vm.History.IsConnected.ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that failed connection attempts restore the disconnected shell state and surface the error text.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ConnectFailure_RevertsToDisconnected()
|
||||
{
|
||||
@@ -180,6 +225,9 @@ public class MainWindowViewModelTests
|
||||
_vm.StatusMessage.ShouldContain("Connection refused");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that connection-state transitions raise property-changed notifications for UI binding updates.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task PropertyChanged_FiredForConnectionState()
|
||||
{
|
||||
@@ -195,6 +243,9 @@ public class MainWindowViewModelTests
|
||||
changed.ShouldContain(nameof(MainWindowViewModel.ConnectionState));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the shell initializes advanced connection settings with the expected defaults.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DefaultState_HasCorrectAdvancedSettings()
|
||||
{
|
||||
@@ -205,6 +256,9 @@ public class MainWindowViewModelTests
|
||||
_vm.CertificateStorePath.ShouldContain("pki");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that failover endpoint text is parsed into connection settings on connect.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ConnectCommand_MapsFailoverUrlsToSettings()
|
||||
{
|
||||
@@ -218,6 +272,9 @@ public class MainWindowViewModelTests
|
||||
_service.LastConnectionSettings.FailoverUrls[1].ShouldBe("opc.tcp://backup2:4840");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that empty failover text is normalized to no configured failover endpoints.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ConnectCommand_MapsEmptyFailoverUrlsToNull()
|
||||
{
|
||||
@@ -228,6 +285,9 @@ public class MainWindowViewModelTests
|
||||
_service.LastConnectionSettings!.FailoverUrls.ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the configured session timeout is passed into the connection settings.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ConnectCommand_MapsSessionTimeoutToSettings()
|
||||
{
|
||||
@@ -238,6 +298,9 @@ public class MainWindowViewModelTests
|
||||
_service.LastConnectionSettings!.SessionTimeoutSeconds.ShouldBe(120);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the auto-accept certificate toggle is passed into the connection settings.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ConnectCommand_MapsAutoAcceptCertificatesToSettings()
|
||||
{
|
||||
@@ -248,6 +311,9 @@ public class MainWindowViewModelTests
|
||||
_service.LastConnectionSettings!.AutoAcceptCertificates.ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that a custom certificate store path is passed into the connection settings.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ConnectCommand_MapsCertificateStorePathToSettings()
|
||||
{
|
||||
@@ -258,6 +324,9 @@ public class MainWindowViewModelTests
|
||||
_service.LastConnectionSettings!.CertificateStorePath.ShouldBe("/custom/pki/path");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that subscribing selected nodes adds subscriptions and switches the shell to the subscriptions tab.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task SubscribeSelectedNodesCommand_SubscribesAndSwitchesToTab()
|
||||
{
|
||||
@@ -275,6 +344,9 @@ public class MainWindowViewModelTests
|
||||
_vm.SelectedTabIndex.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that subscribing selected nodes is a no-op when nothing is selected.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task SubscribeSelectedNodesCommand_DoesNothing_WhenNoSelection()
|
||||
{
|
||||
@@ -285,6 +357,9 @@ public class MainWindowViewModelTests
|
||||
_vm.Subscriptions.ActiveSubscriptions.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the history command targets the selected node and switches the shell to the history tab.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ViewHistoryForSelectedNodeCommand_SetsNodeAndSwitchesToTab()
|
||||
{
|
||||
@@ -299,6 +374,9 @@ public class MainWindowViewModelTests
|
||||
_vm.SelectedTabIndex.ShouldBe(3);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that history actions are enabled when a variable node is selected.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task UpdateHistoryEnabledForSelection_TrueForVariableNode()
|
||||
{
|
||||
@@ -313,6 +391,9 @@ public class MainWindowViewModelTests
|
||||
_vm.IsHistoryEnabledForSelection.ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that history actions stay disabled when an object node rather than a variable is selected.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task UpdateHistoryEnabledForSelection_FalseForObjectNode()
|
||||
{
|
||||
@@ -327,6 +408,9 @@ public class MainWindowViewModelTests
|
||||
_vm.IsHistoryEnabledForSelection.ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that history actions stay disabled when no server connection is active.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void UpdateHistoryEnabledForSelection_FalseWhenDisconnected()
|
||||
{
|
||||
@@ -335,12 +419,18 @@ public class MainWindowViewModelTests
|
||||
_vm.IsHistoryEnabledForSelection.ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that saved user settings are loaded during shell construction.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Constructor_LoadsSettingsFromService()
|
||||
{
|
||||
_settingsService.LoadCallCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that persisted connection and security settings are applied to the shell on startup.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Constructor_AppliesSavedSettings()
|
||||
{
|
||||
@@ -376,6 +466,9 @@ public class MainWindowViewModelTests
|
||||
vm.CertificateStorePath.ShouldBe("/custom/path");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that successful connections persist the current connection settings.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ConnectCommand_SavesSettingsOnSuccess()
|
||||
{
|
||||
@@ -390,6 +483,9 @@ public class MainWindowViewModelTests
|
||||
_settingsService.LastSaved.Username.ShouldBe("admin");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that failed connection attempts do not overwrite saved settings.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ConnectCommand_DoesNotSaveOnFailure()
|
||||
{
|
||||
@@ -400,6 +496,9 @@ public class MainWindowViewModelTests
|
||||
_settingsService.SaveCallCount.ShouldBe(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that active subscriptions are persisted when the shell disconnects.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ConnectCommand_SavesSubscribedNodes()
|
||||
{
|
||||
@@ -416,6 +515,9 @@ public class MainWindowViewModelTests
|
||||
_settingsService.LastSaved!.SubscribedNodes.ShouldContain("ns=2;s=TestSub");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that saved subscriptions are restored after reconnecting the shell.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ConnectCommand_RestoresSavedSubscriptions()
|
||||
{
|
||||
@@ -437,4 +539,4 @@ public class MainWindowViewModelTests
|
||||
vm.Subscriptions.ActiveSubscriptions[1].NodeId.ShouldBe("ns=2;s=Restored2");
|
||||
vm.SubscriptionCount.ShouldBe(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user