# OPC UA Client Requirements

> **Revision** — Refreshed 2026-04-19 for the OtOpcUa v2 multi-driver platform (task #205). The Client surface (shared library + CLI + UI) shipped for v2 is preserved; this refresh restructures the document into numbered, directly-verifiable requirements (CLI-* and UI-* prefixes) layered on top of the existing detailed design content. Requirement coverage added for the `redundancy` command, alarm subscribe/ack round-trip, history-read, and UI tree-browser drag-to-subscribe behaviors. Original design-spec material for `ConnectionSettings`, `IOpcUaClientService`, models, and view-models is retained as reference-level details below the numbered requirements.

Parent: [HLR-001](HighLevelReqs.md#hlr-001-opc-ua-server), [HLR-009](HighLevelReqs.md#hlr-009-transport-security-and-authentication), [HLR-013](HighLevelReqs.md#hlr-013-cluster-redundancy)

See also: `docs/Client.CLI.md`, `docs/Client.UI.md`.

## Projects

| Project | Type | Purpose |
|---------|------|---------|
| `ZB.MOM.WW.OtOpcUa.Client.Shared` | Class library | Core OPC UA client, models, interfaces |
| `ZB.MOM.WW.OtOpcUa.Client.CLI` | Console app | Command-line interface using CliFx |
| `ZB.MOM.WW.OtOpcUa.Client.UI` | Avalonia app | Desktop UI |
| `ZB.MOM.WW.OtOpcUa.Client.Shared.Tests` | Test project | Shared-library unit tests |
| `ZB.MOM.WW.OtOpcUa.Client.CLI.Tests` | Test project | CLI command tests |
| `ZB.MOM.WW.OtOpcUa.Client.UI.Tests` | Test project | ViewModel unit tests |

## Shared Requirements (Client.Shared)

### SHR-001: Single Service Interface

The Client.Shared library shall expose a single service interface `IOpcUaClientService` covering connect, disconnect, read, write, browse, subscribe, alarm-subscribe, alarm-ack, history-read-raw, history-read-aggregate, and get-redundancy-info operations.

### SHR-002: ConnectionSettings Model

The library shall expose a `ConnectionSettings` record with the fields: `EndpointUrl` (required), `FailoverUrls[]`, `Username`, `Password`, `SecurityMode` (None/Sign/SignAndEncrypt; default None), `SessionTimeoutSeconds` (default 60), `AutoAcceptCertificates` (default true), `CertificateStorePath`.

### SHR-003: Automatic Failover

The library shall monitor session keep-alive and automatically fail over across `FailoverUrls` when the primary endpoint is unreachable, emitting a `ConnectionStateChanged` event on each transition (Disconnected / Connecting / Connected / Reconnecting).

### SHR-004: Cross-Platform Certificate Store

The library shall auto-generate a client certificate on first use and store it in a cross-platform path (default `{AppData}/OtOpcUaClient/pki/`). Server certificates are auto-accepted when `AutoAcceptCertificates = true`.

### SHR-005: Type-Coercing Write

The library's `WriteValueAsync(NodeId, object)` shall read the node's current value to determine target type and coerce the input value before sending.

### SHR-006: UI-Thread Dispatch Neutrality

The library shall not assume any specific synchronization context. Events (`DataChanged`, `AlarmEvent`, `ConnectionStateChanged`) are raised on the OPC UA stack thread; the consuming CLI / UI is responsible for dispatching to its UI thread.

---

## CLI Requirements (Client.CLI)

### CLI-001: Command Surface

The CLI shall expose the following commands: `connect`, `read`, `write`, `browse`, `subscribe`, `historyread`, `alarms`, `redundancy`.

### CLI-002: Common Options

All CLI commands shall accept the options `-u, --url` (required), `-U, --username`, `-P, --password`, `-S, --security none|sign|encrypt`, `-F, --failover-urls` (comma-separated), `--verbose`.

### CLI-003: Connect Command

The `connect` command shall attempt to establish a session using the supplied options and print `Connected` plus the resolved endpoint's `ServerUriArray` and `ApplicationUri` on success, or a diagnostic error message on failure.

### CLI-004: Read Command

The `read -n <NodeId>` command shall print `NodeId`, `Value`, `StatusCode`, `SourceTimestamp`, `ServerTimestamp` one per line.

### CLI-005: Write Command

The `write -n <NodeId> -v <value>` command shall coerce the value to the node's current type (per SHR-005) and print the resulting `StatusCode`. A `Bad_UserAccessDenied` result is printed verbatim so operators see the authorization outcome.

### CLI-006: Browse Command

The `browse [-n <parent>] [-r] [-d <depth>]` command shall list child nodes under `parent` (or the `Objects` folder if omitted). `-r` enables recursion up to `-d` depth (default 1).

### CLI-007: Subscribe Command

The `subscribe -n <NodeId> -i <intervalMs>` command shall create a monitored item at `intervalMs` publishing interval, print each `DataChanged` event as `<timestamp> <nodeId> <value> <status>` until Ctrl-C, then cleanly unsubscribe.

### CLI-008: Historyread Command

The `historyread -n <NodeId> --start <utc> --end <utc> [--max <n>] [--aggregate <type> --interval <ms>]` command shall print raw values or aggregate buckets. Supported aggregate types: Average, Minimum, Maximum, Count, Start, End.

### CLI-009: Alarms Command

The `alarms [-n <source>] [-i <intervalMs>]` command shall subscribe to alarm events, print each event as `<time> <source> <condition> <severity> <state> <acked> <message>`, accept `ack <conditionId>` commands interactively, and support `refresh` to trigger `RequestConditionRefreshAsync`.

### CLI-010: Redundancy Command

The `redundancy` command shall call `GetRedundancyInfoAsync` and print `Mode`, `ServiceLevel`, `ApplicationUri`, and `ServerUris` (one per line). Suitable for redundancy-failover smoke tests.

### CLI-011: Logging

The CLI shall use Serilog console sink at `Warning` minimum by default; `--verbose` raises to `Debug`.

---

## UI Requirements (Client.UI)

### UI-001: Connection Panel

The UI shall present a top-bar connection panel with fields for Endpoint URL, Username, Password, Security mode, and a Connect / Disconnect button. The resolved `RedundancyInfo` is displayed next to the bar on successful connect.

### UI-002: Tree Browser

The UI shall present a left-pane tree browser backed by `IOpcUaClientService.BrowseAsync`, lazy-loading children on node expansion (one level per `BrowseAsync` call).

### UI-003: Read/Write Tab

The UI shall provide a Read/Write tab that auto-reads the selected tree node's current value, displays `Value` + `StatusCode` + `SourceTimestamp`, and accepts a write value with a Send button.

### UI-004: Subscriptions Tab

The UI shall provide a Subscriptions tab that lists active monitored items (columns: NodeId, Value, Status, Timestamp), supports Add and Remove, and dispatches `DataChanged` events to the Avalonia UI thread via `Dispatcher.UIThread.Post`.

### UI-005: Alarms Tab

The UI shall provide an Alarms tab that supports SubscribeAlarms / UnsubscribeAlarms / RefreshConditions commands, displays live alarm events, and supports `Acknowledge` on selected events. Acknowledgment failure (including `Bad_UserAccessDenied`) is surfaced to the user.

### UI-006: History Tab

The UI shall provide a History tab with inputs for StartTime, EndTime, MaxValues, AggregateType, Interval, a Read command, and a results table with columns (Timestamp, Value, Status).

### UI-007: Connection State Reflects in UI

All tabs shall reflect the connection state — when disconnected, all action commands are disabled; the status bar shows `Disconnected` / `Connecting` / `Connected` / `Reconnecting` tied to the `ConnectionStateChanged` event.

### UI-008: Cross-Platform

The UI shall build and run on Windows (win-x64) and macOS (osx-arm64 / osx-x64). No platform-specific OPC UA stack APIs are used.

---

## Technology Stack

- .NET 10, C#
- OPC UA: `OPCFoundation.NetStandard.Opc.Ua.Client`
- Logging: Serilog
- CLI: CliFx
- UI: Avalonia 11.x with CommunityToolkit.Mvvm
- Tests: xUnit 3, Shouldly, Microsoft.Testing.Platform runner

## Client.Shared — Design Detail

### IOpcUaClientService Interface (reference)

**Lifecycle:** `ConnectAsync(ConnectionSettings)`, `DisconnectAsync()`, `IsConnected`.

**Read/Write:** `ReadValueAsync(NodeId)`, `WriteValueAsync(NodeId, object)`.

**Browse:** `BrowseAsync(NodeId? parent)` → `BrowseResult[]` (NodeId, DisplayName, NodeClass, HasChildren); lazy-load compatible.

**Subscribe:** `SubscribeAsync(NodeId, int intervalMs)`, `UnsubscribeAsync(NodeId)`, `event DataChanged(NodeId, DataValue)`.

**Alarms:** `SubscribeAlarmsAsync(NodeId? source, int intervalMs)`, `UnsubscribeAlarmsAsync()`, `AcknowledgeAsync(conditionId, comment)`, `RequestConditionRefreshAsync()`, `event AlarmEvent(AlarmEventArgs)`.

**History:** `HistoryReadRawAsync(NodeId, start, end, maxValues)`, `HistoryReadAggregateAsync(NodeId, start, end, AggregateType, intervalMs)`.

**Redundancy:** `GetRedundancyInfoAsync()` → `RedundancyInfo` (Mode, ServiceLevel, ServerUris, ApplicationUri).

### Models

- `BrowseResult` — NodeId, DisplayName, NodeClass, HasChildren
- `AlarmEventArgs` — SourceName, ConditionName, Severity, Message, Retain, ActiveState, AckedState, Time
- `RedundancyInfo` — Mode, ServiceLevel, ServerUris, ApplicationUri
- `ConnectionState` — enum (Disconnected, Connecting, Connected, Reconnecting)
- `AggregateType` — enum (Average, Minimum, Maximum, Count, Start, End)

---

## Client.UI — View Layout (reference)

Single-window Avalonia application:

```
┌─────────────────────────────────────────────────────────┐
│ [Endpoint URL] [User] [Pass] [Security▼] [Connect]     │
│ Redundancy: Mode=Warm  ServiceLevel=200  AppUri=...     │
├──────────────┬──────────────────────────────────────────┤
│              │ ┌Read/Write┬Subscriptions┬Alarms┬History┐│
│  Address     │ │ Node: ns=3;s=Tag.Attr               ││
│  Space       │ │ Value: 42.5 Status: Good            ││
│  Tree        │ │ [Write: ____] [Send]                ││
│  Browser     │ └───────────────────────────────────────┘│
├──────────────┴──────────────────────────────────────────┤
│ Status: Connected | Session: abc123 | 3 subscriptions   │
└─────────────────────────────────────────────────────────┘
```

### ViewModels (CommunityToolkit.Mvvm)

- `MainWindowViewModel` — connection fields, connect/disconnect commands, `ConnectionState`, `RedundancyInfo`, `SelectedTreeNode`, `StatusMessage`.
- `BrowseTreeViewModel` — root collection (`ObservableCollection<TreeNodeViewModel>`), lazy-load on expand.
- `ReadWriteViewModel` — auto-read on selection, `WriteValue` + `WriteCommand`.
- `SubscriptionsViewModel` — `ActiveSubscriptions`, `AddSubscription`, `RemoveSubscription`, live `DataChanged` dispatch to UI thread.
- `AlarmsViewModel` — `AlarmEvents`, Subscribe / Unsubscribe / Refresh / Acknowledge commands.
- `HistoryViewModel` — `StartTime`, `EndTime`, `MaxValues`, `AggregateType`, `Interval`, `ReadCommand`, `Results`.

## Test Projects

### Client.Shared.Tests
- `ConnectionSettings` validation
- Type conversion
- `BrowseResult` / `AlarmEventArgs` / `RedundancyInfo` model construction
- FailoverUrl parsing

### Client.CLI.Tests
- Command option parsing (via CliFx test infrastructure)
- Output formatting for each command

### Client.UI.Tests
- ViewModel property-change notifications
- Command `CanExecute` logic
- Tree lazy-load behavior (with mocked `IOpcUaClientService`)

### Test Framework
- xUnit 3 with Microsoft.Testing.Platform runner
- Shouldly
- No live OPC UA server required — mock `IOpcUaClientService` for unit tests
