Files
lmxopcua/docs/Client.CLI.md
T
Joseph Doherty 78f604e0cd feat(historian): Gateway serves the per-sample aggregates from raw reads with the shared Sql.Common evaluator (#681)
Average, Count, Minimum, Maximum, Range, Start, End, Delta and Minimum/MaximumActualTime are evaluated
on the Gateway backend from forward ReadRaw reads with BucketPlan + AggregateEvaluator, so Gateway
matches SqlServer/QuestDB exactly (values, status bits, interval-start stamps, #677 reverse).

- Window read [start - 1 ms, end] so AVEVA's initial-value row never lands in a bucket; a pre-window
  row proves earlier data only with a non-Bad OPC quality (histsdk puts a value on every NULL row).
- Earlier data otherwise from a 2-row [1970, start) probe; later data from a 2-row [end, now+1d]
  look-ahead; both ignore rows stamped at their query start/end.
- No gateway stream is ever abandoned (a client-side cancel makes the gateway evict its shared
  historian session); past ServerHistorian:MaxTimeWeightedRawRows the bounded stream is drained, then
  BadTooManyOperations. The gateway's max_values-ceiling InvalidArgument maps to the same limit; a
  caller cancellation is not a backend fault.
- TimeAverage/Total stay AVEVA-native (no prior-sample read for exact Part 13 bounds), now stamped with
  the interval start (PENDING the live leg: histsdk's tail+43 start_time field is unverified) and
  carrying the Calculated bits.
- Served set: GatewayHistorianDataSource.ServedAggregates (12). Live AVEVA leg open (VPN).
2026-10-12 09:56:54 -04:00

22 KiB

Client CLI

Overview

ZB.MOM.WW.OtOpcUa.Client.CLI is a cross-platform command-line client for the OtOpcUa OPC UA server. It targets .NET 10 and uses the shared IOpcUaClientService from Client.Shared for all OPC UA operations. Commands are routed and parsed by CliFx.

The CLI is the primary tool for operators and developers to test and interact with the server from a terminal. It supports all core operations: connectivity testing, browsing, reading, writing, subscriptions, alarm monitoring, alarm operator actions (acknowledge / confirm / comment / shelve / enable / disable), history reads, and redundancy queries. Any driver surface exposed by the server (Galaxy, Modbus, S7, AB CIP, AB Legacy, TwinCAT, FOCAS, OPC UA Client) is reachable through these commands — the CLI is driver-agnostic because everything below the OPC UA endpoint is.

Build and Run

cd src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI
dotnet build
dotnet run -- <command> [options]

The executable name is otopcua-cli. Dev boxes carrying a pre-task-#208 install may still have the legacy {LocalAppData}/LmxOpcUaClient/ folder on disk; on first launch of any post-#208 CLI or UI build, ClientStoragePaths (src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/ClientStoragePaths.cs) migrates it to {LocalAppData}/OtOpcUaClient/ automatically so trusted certificates + saved settings survive the rename.

Architecture

All commands inherit from CommandBase, which provides common connection options and helper methods. Every command follows the same lifecycle:

  1. Build ConnectionSettings from common options
  2. Create an IOpcUaClientService through the factory
  3. Call ConnectAsync to establish a session
  4. Perform the command-specific operation
  5. Call DisconnectAsync in a finally block

No command accesses the OPC UA Session directly; all operations go through the shared service abstraction. This ensures consistent behavior with the desktop UI client.

Common Options

All commands accept these options:

Flag Description
-u / --url OPC UA server endpoint URL (required)
-U / --username Username for UserName token authentication
-P / --password Password for UserName token authentication
-S / --security Transport security mode: none, sign, encrypt, signandencrypt (default: none)
-F / --failover-urls Comma-separated failover endpoint URLs for redundancy
--verbose Enable debug-level Serilog console logging (default: warning)

--security none does not fall back. Endpoint selection picks the server's None-policy endpoint and fails with an InvalidOperationException naming the endpoints the server does offer when it has none. The older stack helper silently fell back to the first endpoint (often a secured one), so a command that defaulted to none against a secured-only server used to connect and now errors. Pass -S sign, -S encrypt or -S signandencrypt to talk to such a server.

Authentication

When -U and -P are provided, the shared service passes a UserIdentity(username, password) to the OPC UA session. Without credentials, anonymous identity is used.

otopcua-cli write -u opc.tcp://localhost:4840 -n "ns=2;s=MyNode" -v 42 -U operator -P op123

Failover

Mapped ports and internal hostnames. Endpoint discovery rewrites the host and port of the endpoint the server advertises to the URL you passed (when the scheme matches), so connecting through a port mapping works even though the server advertises its internal address (for example the docker-dev rig: -u opc.tcp://localhost:4841 reaches the node mapped to 4841, although every container advertises :4840).

When -F is provided, the shared service tries the primary URL first, then each failover URL in order. For long-running commands (subscribe, alarms), the service monitors the session via keep-alive and automatically reconnects to the next available server on failure.

otopcua-cli connect -u opc.tcp://localhost:4840/OtOpcUa -F opc.tcp://localhost:4841/OtOpcUa

Transport Security

When sign or encrypt is specified, the shared service:

  1. Ensures a client application certificate exists under {LocalAppData}/OtOpcUaClient/pki/ (auto-created if missing; pre-rename LmxOpcUaClient/ is migrated in place on first launch)
  2. Discovers server endpoints and selects one matching the requested security mode
  3. Prefers Basic256Sha256 when multiple matching endpoints exist
  4. Fails with a clear error if no matching endpoint is found
otopcua-cli browse -u opc.tcp://localhost:4840/OtOpcUa -S encrypt -U admin -P secret -r -d 2

Verbose Logging

The --verbose flag switches Serilog output from Warning to Debug level, showing internal connection lifecycle, endpoint discovery, and OPC UA SDK diagnostics on the console.

Commands

connect

Tests connectivity to an OPC UA server. Creates a session, prints connection metadata, and disconnects.

otopcua-cli connect -u opc.tcp://localhost:4840/OtOpcUa -U admin -P admin123

Output:

Connected to: opc.tcp://localhost:4840/OtOpcUa
Server: OtOpcUa Server
Security Mode: None
Security Policy: http://opcfoundation.org/UA/SecurityPolicy#None
Connection successful.

read

Reads the current value of a single node and prints the value, status code, and timestamps.

otopcua-cli read -u opc.tcp://localhost:4840/OtOpcUa -n "ns=3;s=DEV.ScanState" -U admin -P admin123
Flag Description
-n / --node Node ID to read (required)

Output:

Node:        ns=3;s=DEV.ScanState
Value:       True
Status:      0x00000000
Source Time: 2026-03-30T19:58:38.0961252Z
Server Time: 2026-03-30T19:58:38.0971257Z

A Bad node status (unknown node, access denied, ...) is reported, not thrown: the command prints Node:, an empty Value: and Status: 0x80340000 (BadNodeIdUnknown). Session-level failures (closed connection / channel) still fail the command.

write

Writes a value to a node. The shared service reads the current value first to determine the target data type, then converts the supplied string value using ValueConverter.ConvertValue().

otopcua-cli write -u opc.tcp://localhost:4840 -n "ns=2;s=MyNode" -v 42
Flag Description
-n / --node Node ID to write to (required)
-v / --value Value to write (required)

browse

Browses the OPC UA address space starting from the Objects folder or a specified node. Supports recursive traversal with a configurable depth limit. Output uses tree-style indentation with [Object], [Variable], and [Method] markers.

# Browse top-level Objects folder
otopcua-cli browse -u opc.tcp://localhost:4840/OtOpcUa -U admin -P admin123

# Browse a specific node recursively to depth 3
otopcua-cli browse -u opc.tcp://localhost:4840/OtOpcUa -U admin -P admin123 -r -d 3 -n "ns=3;s=ZB"
Flag Description
-n / --node Node ID to browse (default: Objects folder)
-d / --depth Maximum browse depth (default: 1)
-r / --recursive Browse recursively using -d as max depth

subscribe

Monitors a node (or every Variable in its subtree) for value changes using OPC UA subscriptions. Prints each data-change notification with timestamp, value, and status code, then prints a summary on exit. Exits on Ctrl+C, or automatically after --duration seconds.

# Subscribe to a single node
otopcua-cli subscribe -u opc.tcp://localhost:4840 -n "ns=2;s=MyNode" -i 500

# Browse a subtree and subscribe to every Variable, run for 60 seconds, write the summary to disk
otopcua-cli subscribe -u opc.tcp://localhost:4840 -n "ns=3;s=ZB" -r --max-depth 4 \
  --duration 60 --quiet --summary-file C:\Temp\subscribe-summary.txt
Flag Description
-n / --node Node ID to monitor (required). When --recursive is set, this is the browse root.
-i / --interval Sampling interval in milliseconds (default: 1000)
-r / --recursive Browse recursively from --node and subscribe to every Variable found
--max-depth Maximum recursion depth when --recursive is set (default: 10)
-q / --quiet Suppress per-update output; only print the final summary
--duration Auto-exit after N seconds and print the summary (0 = run until Ctrl+C, default: 0)
--summary-file Also write the summary to this file path on exit

Summary buckets

The summary prints per-node counts across these buckets:

  • Ever went BAD during window — node received at least one notification whose status was not Good.
  • NEVER went bad (suspect) — node received at least one notification and every one was Good.
  • Last status GOOD / NOT-GOOD — final observed status across nodes that received any update.
  • No update received at all — node was subscribed but no notification arrived during the window.

historyread

Reads historical data from a node. Supports raw history reads and aggregate (processed) history reads. --start and --end are parsed with CultureInfo.InvariantCulture and treated as UTC; supply them in ISO 8601 UTC form (YYYY-MM-DDTHH:MM:SSZ) for unambiguous behaviour across hosts.

A Bad per-node status from the server (for example BadHistoryOperationUnsupported for a node that is not historized, or BadUserAccessDenied) fails the command with that status named. It is never printed as 0 values returned; only BadNoData and Good/GoodNoData read as an empty result.

# Raw history
otopcua-cli historyread -u opc.tcp://localhost:4840/OtOpcUa \
  -n "ns=1;s=TestMachine_001.TestHistoryValue" \
  --start "2026-03-25T00:00:00Z" --end "2026-03-30T00:00:00Z"

# Aggregate: 1-hour average (every backend; on Gateway it is computed from raw samples since #681 —
# capped by ServerHistorian:MaxTimeWeightedRawRows, so read long dense windows in slices)
otopcua-cli historyread -u opc.tcp://localhost:4840/OtOpcUa \
  -n "ns=1;s=TestMachine_001.TestHistoryValue" \
  --start "2026-03-25T00:00:00Z" --end "2026-03-30T00:00:00Z" \
  --aggregate Average --interval 3600000

# Modified-value history (what HistoryUpdate changed): prior value, update type, user, modification time
otopcua-cli historyread -u opc.tcp://localhost:4840/OtOpcUa -U apikey -P otua_<keyId>_<secret> \
  -n "ns=2;s=Plant/Modbus/dev1/Speed" \
  --start "2026-01-15T00:00:00Z" --end "2026-01-16T00:00:00Z" --modified

--modified sends ReadRawModifiedDetails with IsReadModified = true (OPC UA Part 11, Gitea #668) and prints one line per recorded modification: the source timestamp, the prior value ((none) for an insert, which had none), its status, the update type (Insert / Replace / Update / Delete), the user (the API key's principal, or anonymous) and the modification time. It follows continuation points up to --max. A server on the Gateway backend (no modification trail) answers BadHistoryOperationUnsupported. --modified cannot be combined with --aggregate. See Historian.md §Modified-value history.

Flag Description
-n / --node Node ID to read history for (required)
--start Start time in ISO 8601 UTC format, e.g. 2026-01-15T08:00:00Z (default: 24 hours ago)
--end End time in ISO 8601 UTC format, e.g. 2026-01-15T09:00:00Z (default: now)
--max Maximum number of values (default: 1000)
--aggregate Aggregate function: Average, Minimum, Maximum, Count, Start, End, StandardDeviation, TimeAverage, TimeAverage2, Total, Total2, Interpolative
--interval Processing interval in milliseconds for aggregates (default: 3600000)
--modified Read modified-value history (Part 11 IsReadModified) instead of raw samples

historyupdate

Inserts, replaces, updates or deletes stored history on a historized node via the OPC UA HistoryUpdate service (requires a server whose historian backend supports updates and a session holding the history-write permission). Prints one line per item (index time StatusCodeName; a code the SDK has no name for prints as 0xXXXXXXXX) followed by the overall service result. It exits non-zero unless every result is Good: a Bad or Uncertain item, a non-Good node or service result, or a Good service result that is missing per-item results (fewer than the items sent, printed as (no result returned)) all fail the command.

# Insert two values
otopcua-cli historyupdate -u opc.tcp://localhost:4840/OtOpcUa -n "ns=2;s=Plant/Modbus/dev1/Speed" \
  --mode insert --value 1.5 --time 2026-01-15T08:00:00Z --value 2.5 --time 2026-01-15T09:00:00Z

# Replace an int64 value
otopcua-cli historyupdate -u ... -n "..." --mode replace --type int64 --value 42 --time 2026-01-15T08:00:00Z

# Delete a raw range / specific timestamps
otopcua-cli historyupdate -u ... -n "..." --mode delete-raw --start 2026-01-15T08:00:00Z --end 2026-01-15T10:00:00Z
otopcua-cli historyupdate -u ... -n "..." --mode delete-at-time --time 2026-01-15T08:00:00Z
Flag Description
-n / --node Node ID to modify (required)
--mode insert, replace, update, delete-raw or delete-at-time (required)
--value Value to write; repeatable, paired positionally with --time (insert/replace/update)
--time ISO 8601 UTC timestamp; repeatable (insert/replace/update/delete-at-time)
--type double (default), int64, bool, string, datetime - how --value is parsed
--start / --end Range for delete-raw (both required; --end later than --start)

Expected per-item codes on the SQL Server historian backend: insert ⇒ GoodEntryInserted (repeat ⇒ BadEntryExists), replace ⇒ GoodEntryReplaced (BadNoEntryExists if nothing is stored at that time), update ⇒ either Good code, delete-at-time ⇒ Good / BadNoEntryExists; delete-raw reports one node-level code (Good, or BadNoData if the range held nothing). A node that is not historized, or a server on the Gateway historian backend (no updater, so no HistoryWrite access bit), answers BadHistoryOperationUnsupported; a NaN/Infinity double is BadInvalidArgument. The session's API key (-U apikey -P otua_<keyId>_<secret>) needs the HistoryUpdate permission — via a Cluster-scoped grant when Security:Acl:Enabled is off, or a scoped grant on the node when it is on; a key with only WriteOperate gets BadUserAccessDenied. See Historian.md §HistoryUpdate and security.md. historyread --aggregate only knows the twelve names in the table below; the SQL backend's other aggregates (Range, Delta, PercentGood, …) need a different client. A refused history read (for example BadHistoryOperationUnsupported) prints one error line and exits non-zero; it does not dump a stack trace.

delete-raw is sent with IsDeleteModified=false. Invalid or inconsistent options (unknown mode, unpaired value/time, unparsable value, options that do not apply to the mode) fail before any connection is made.

Aggregate mapping

Name Aliases OPC UA Node ID
Average avg AggregateFunction_Average
Minimum min AggregateFunction_Minimum
Maximum max AggregateFunction_Maximum
Count AggregateFunction_Count
Start first AggregateFunction_Start
End last AggregateFunction_End
StandardDeviation stddev, stdev AggregateFunction_StandardDeviationPopulation
TimeAverage timeavg AggregateFunction_TimeAverage
TimeAverage2 timeavg2 AggregateFunction_TimeAverage2
Total AggregateFunction_Total
Total2 AggregateFunction_Total2
Interpolative interp AggregateFunction_Interpolative

alarms

Subscribes to alarm events on a node. Prints structured alarm output including source, condition, severity, active/acknowledged state, and message. Runs until Ctrl+C, then unsubscribes and disconnects cleanly.

# Subscribe to alarm events on the Server node
otopcua-cli alarms -u opc.tcp://localhost:4840/OtOpcUa

# Subscribe to a specific source node with condition refresh
otopcua-cli alarms -u opc.tcp://localhost:4840/OtOpcUa \
  -n "ns=1;s=TestMachine_001" --refresh
Flag Description
-n / --node Node ID to monitor for events (default: Server node)
-i / --interval Publishing interval in milliseconds (default: 1000)
--refresh Request a ConditionRefresh after subscribing to get current retained alarm states

The active/acknowledged columns come from the standard ActiveState/Id, AckedState/Id and EnabledState/Id event fields. Before the 2026-10 fix the client's filter selected them with a single QualifiedName named ActiveState/Id (a /-joined string passed to EventFilter.AddSelectClause is not split into a browse path), which matched nothing — so every event printed inactive and unacknowledged whatever the condition's real state. A direct Read of the condition was always correct.

ack

Acknowledges an active alarm condition via the OPC UA Part 9 Acknowledge method. The server gates this on the AlarmAck role (fail-closed — a session whose API-key principal holds no AlarmAck-conferring NodeAcl grant gets BadUserAccessDenied; the data plane does not use LDAP; see ScriptedAlarms.md §"AlarmAck gate"). The --event-id is the hex-encoded EventId taken from a prior alarms subscription notification for the same condition.

otopcua-cli ack -u opc.tcp://localhost:4840/OtOpcUa \
  -n "ns=1;s=TestMachine_001" -e 0A1B2C -c "Investigating"
Flag Description
-n / --node Condition node ID of the alarm to acknowledge (required)
-e / --event-id EventId from the alarm notification, hex-encoded (e.g. 0A1B2C) (required)
-c / --comment Operator comment for the acknowledgment (optional)

Example output:

Acknowledge successful: ns=1;s=TestMachine_001

confirm

Confirms an already-acknowledged alarm condition via the OPC UA Part 9 Confirm method — the second stage of two-stage acknowledgment. Same AlarmAck role gate and same --event-id source (a prior alarms notification) as ack.

otopcua-cli confirm -u opc.tcp://localhost:4840/OtOpcUa \
  -n "ns=1;s=TestMachine_001" -e 0A1B2C -c "Root cause cleared"
Flag Description
-n / --node Condition node ID of the alarm to confirm (required)
-e / --event-id EventId from the alarm notification, hex-encoded (required)
-c / --comment Operator comment for the confirmation (optional)

Example output:

Confirm successful: ns=1;s=TestMachine_001

comment

Attaches an operator comment to an alarm condition (OPC UA Part 9 AddComment) without changing its acknowledge/confirm state. Works on scripted and native conditions alike. The --node and --event-id are the same values ack/confirm take.

otopcua-cli comment -u opc.tcp://localhost:4840/OtOpcUa \
  -n "ns=2;s=Plant/Line1/Pump1/HighTemp" -e 0A1B2C -c "Checked on site"
Flag Description
-n / --node Condition node ID of the alarm to comment on (required)
-e / --event-id Hex-encoded EventId from the alarm notification (required)
-c / --comment Operator comment to attach (required)

shelve

Shelves or unshelves an active alarm condition via the OPC UA Part 9 ShelvedStateMachine (OneShotShelve / TimedShelve / Unshelve). Gated on the AlarmAck role. --duration is given in seconds (converted to milliseconds for the OPC UA call) and is required only for --kind Timed. The --node is the condition node itself (the SourceNode/condition id an alarms notification reports): ack, confirm, shelve, enable and disable call the Part 9 method ids directly on that node — no .Condition / .ShelvingState suffix is appended (those child nodes do not exist on the OtOpcUa server).

# One-shot shelve until the next clear
otopcua-cli shelve -u opc.tcp://localhost:4840/OtOpcUa \
  -n "ns=1;s=TestMachine_001" -k OneShot

# Timed shelve for 300 seconds
otopcua-cli shelve -u opc.tcp://localhost:4840/OtOpcUa \
  -n "ns=1;s=TestMachine_001" -k Timed -d 300

# Unshelve
otopcua-cli shelve -u opc.tcp://localhost:4840/OtOpcUa \
  -n "ns=1;s=TestMachine_001" -k Unshelve
Flag Description
-n / --node Condition node ID of the alarm to shelve/unshelve (required)
-k / --kind Shelve operation: OneShot | Timed | Unshelve (required)
-d / --duration Shelving duration in seconds (must be > 0; required for --kind Timed)

Example output:

Timed successful: ns=1;s=TestMachine_001

enable

Enables an alarm condition via the OPC UA Part 9 ConditionType.Enable method (resumes evaluation/reporting for a previously disabled condition). Gated on the AlarmAck role.

otopcua-cli enable -u opc.tcp://localhost:4840/OtOpcUa -n "ns=1;s=TestMachine_001"
Flag Description
-n / --node Condition node ID of the alarm to enable (required)

Example output:

Enable successful: ns=1;s=TestMachine_001

disable

Disables an alarm condition via the OPC UA Part 9 ConditionType.Disable method (stops evaluation/reporting). Gated on the AlarmAck role.

otopcua-cli disable -u opc.tcp://localhost:4840/OtOpcUa -n "ns=1;s=TestMachine_001"
Flag Description
-n / --node Condition node ID of the alarm to disable (required)

Example output:

Disable successful: ns=1;s=TestMachine_001

redundancy

Reads the OPC UA redundancy state from a server: redundancy mode, service level, server URIs, and application URI.

otopcua-cli redundancy -u opc.tcp://localhost:4840/OtOpcUa -U admin -P admin123

Example output:

Redundancy Mode:  Warm
Service Level:    200
Server URIs:
  - urn:localhost:OtOpcUa:instance1
  - urn:localhost:OtOpcUa:instance2
Application URI:  urn:localhost:OtOpcUa:instance1

Testing

The Client CLI has 104 unit tests covering option parsing, service invocation, output formatting, and cleanup behavior:

dotnet test tests/Client/ZB.MOM.WW.OtOpcUa.Client.CLI.Tests