# OPC UA Client driver

Tier-A in-process driver that opens a `Session` against a remote OPC UA server
and re-exposes its address space through the local OtOpcUa server. The
"gateway / aggregation" direction — opposite to the usual "server exposes PLC
data" flow.

For the test fixture (opc-plc) see [`OpcUaClient-Test-Fixture.md`](OpcUaClient-Test-Fixture.md).
For the configuration surface see `OpcUaClientDriverOptions` in
[`src/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriverOptions.cs`](../../src/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriverOptions.cs).

## Auto re-import on `ModelChangeEvent`

The driver subscribes to `BaseModelChangeEventType` (and its subtype
`GeneralModelChangeEventType`) on the upstream `Server` node (`i=2253`) at
the end of `InitializeAsync`. When the upstream server advertises a
topology change, the driver coalesces events over a debounce window and
runs a single re-import (equivalent to calling `ReinitializeAsync` —
internally `ShutdownAsync` + `InitializeAsync`).

### Configuration

| Option | Default | Notes |
| --- | --- | --- |
| `WatchModelChanges` | `true` | Disable to skip the watch entirely (no extra subscription, no re-import on topology change). |
| `ModelChangeDebounce` | `5s` | Coalescing window. The first event starts the timer; further events extend it; when it elapses with no new events, the driver fires one re-import. |

### Behaviour

- One model-change subscription per driver instance, separate from the
  data + alarm subscriptions. Created best-effort: a server that doesn't
  advertise the event types or rejects the `EventFilter` falls through to
  no-watch — `InitializeAsync` still succeeds.
- The `EventFilter` selects only the `EventType` field (a `WhereClause`
  constrains by `OfType BaseModelChangeEventType`). Payload fields like
  `Changes[]` are intentionally ignored: the driver always re-imports the
  full upstream root, so per-event delta tracking would just add wire
  overhead.
- Debounce is implemented via a single-shot `Timer`; every event calls
  `Timer.Change(window, Infinite)` so a burst of N events triggers exactly
  one re-import after the window elapses with no further events.
- The re-import path acquires the same `_gate` semaphore that `ReadAsync`
  / `WriteAsync` / `BrowseAsync` / `SubscribeAsync` use. Downstream callers
  see a brief browse-gap (≈ the upstream `DiscoverAsync` duration) while
  the gate is held — but no torn reads or split-batch writes.
- Failure during the re-import is best-effort: the next `ModelChangeEvent`
  triggers another attempt, and the keep-alive watchdog covers permanent
  upstream loss. Operators see failures through `DriverHealth.LastError`
  + the diagnostics counters.

### When to disable

Flip `WatchModelChanges` to `false` when:

- The upstream topology is known-static (e.g. firmware-pinned PLC) and
  the driver should never run a re-import unprompted.
- The brief browse-gap during re-import is unacceptable and a manual
  `ReinitializeAsync` call from the operator is preferred.
- The upstream server fires spurious `ModelChangeEvent`s that don't
  reflect real topology changes, causing wasted re-imports. Tighten or
  disable rather than chasing the noise downstream.
