docs(historian-gateway): document gateway backend, config keys, EnsureTags hook, known gates; retire Wonderware from docs
v2-ci / build (pull_request) Failing after 38s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (pull_request) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (pull_request) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (pull_request) Has been skipped
v2-ci / build (pull_request) Failing after 38s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (pull_request) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (pull_request) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (pull_request) Has been skipped
HistorianGateway is now the sole historian backend (read + alarm SendEvent + continuous WriteLiveValues). Document the final state and retire the Wonderware sidecar from the docs/config/labels: - CLAUDE.md: rewrite the Historian section — ServerHistorian / ContinuousHistorization / AlarmHistorian config keys, the IHistorianProvisioning EnsureTags hook, the GatewayAlarmHistorianWriter SendEvent path + ReadEvents dependency on gateway RuntimeDb:EventReadsEnabled=true, gateway-side prerequisites (RuntimeDb flags + historian:read/write/tags:write scopes), migration note, and two KNOWN-LIMITATION callouts (live-validation gate + empty historized-ref-set recorder follow-on). - appsettings.json: fix the stale ServerHistorian block (Host/Port/SharedSecret/ ServerCertThumbprint -> Endpoint/ApiKey/UseTls/AllowUntrustedServerCertificate/ CaCertificatePath/CallTimeout, keep MaxTieClusterOverfetch); add a disabled ContinuousHistorization block; prune the orphaned Wonderware keys from AlarmHistorian (keep the SQLite knobs). ApiKey env-supplied via ServerHistorian__ApiKey (commented; valid strict JSON via _comment keys). - README.md + docs (Historian.md, AlarmHistorian.md, Configuration.md, ServiceHosting.md, DriverLifecycle.md, drivers/README.md, Uns.md, VirtualTags.md, AlarmTracking.md, Client.UI.md, README.md, TestConnectProbes.md): retire the Wonderware historian backend from current-backend descriptions; fix the stale ServerHistorian/AlarmHistorian config tables (now gateway shape); convert drivers/Historian.Wonderware.md to a retired stub pointing at the gateway. - Source/UI labels (descriptive text only, no behavior change): OtOpcUaServerHostedService.cs, HistoryPaging.cs, OtOpcUaSdkServer.cs, HistorianAdapterActor.cs, VirtualTagModal.razor, ScriptedAlarmModal.razor, AlarmsHistorian.razor now name the HistorianGateway backend. Build clean (0 errors); AdminUI.Tests green (514 passed). Claude-Session: https://claude.ai/code/session_012SDSQ3AcaXqPcBtDESBRii
This commit is contained in:
+60
-50
@@ -3,9 +3,12 @@
|
||||
Phase C wires server-side OPC UA **HistoryRead** for authored equipment tags flagged
|
||||
historized. The feature is driver-agnostic: any equipment tag (Galaxy, Modbus, OpcUaClient,
|
||||
or any other driver) can be marked historized; the server dispatches all history reads to the
|
||||
registered `IHistorianDataSource` — today, the Wonderware sidecar client
|
||||
(`WonderwareHistorianClient`). No EF migration is required; the historian flag rides in the
|
||||
existing schemaless `TagConfig` JSON blob alongside the Phase B `alarm` object.
|
||||
registered `IHistorianDataSource` — the **HistorianGateway** read client
|
||||
(`GatewayHistorianDataSource`, talking gRPC to the external `ZB.MOM.WW.HistorianGateway` via the
|
||||
`ZB.MOM.WW.HistorianGateway.Client` package). No EF migration is required; the historian flag rides in
|
||||
the existing schemaless `TagConfig` JSON blob alongside the Phase B `alarm` object. (The bespoke
|
||||
Wonderware TCP sidecar backend this replaced was retired — see
|
||||
[drivers/Historian.Wonderware.md](drivers/Historian.Wonderware.md).)
|
||||
|
||||
Design reference: [docs/plans/2026-06-14-galaxy-phase-c-historian-design.md](plans/2026-06-14-galaxy-phase-c-historian-design.md).
|
||||
|
||||
@@ -60,11 +63,12 @@ and all HistoryRead calls on historized nodes return `GoodNoData` (empty, not an
|
||||
{
|
||||
"ServerHistorian": {
|
||||
"Enabled": false,
|
||||
"Host": "localhost",
|
||||
"Port": 32569,
|
||||
"UseTls": false,
|
||||
"ServerCertThumbprint": "",
|
||||
"SharedSecret": "",
|
||||
"Endpoint": "",
|
||||
"ApiKey": "",
|
||||
"UseTls": true,
|
||||
"AllowUntrustedServerCertificate": false,
|
||||
"CaCertificatePath": null,
|
||||
"CallTimeout": "00:00:30",
|
||||
"MaxTieClusterOverfetch": 65536
|
||||
}
|
||||
}
|
||||
@@ -72,20 +76,31 @@ and all HistoryRead calls on historized nodes return `GoodNoData` (empty, not an
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `Enabled` | bool | `false` | Enable the live `WonderwareHistorianClient`. `false` → `NullHistorianDataSource` (empty reads). |
|
||||
| `Host` | string | `localhost` | DNS name or IP of the machine running the historian sidecar. |
|
||||
| `Port` | int | `32569` | TCP port the sidecar listens on (`OTOPCUA_HISTORIAN_TCP_PORT`). |
|
||||
| `UseTls` | bool | `false` | Wrap the TCP connection in TLS. |
|
||||
| `ServerCertThumbprint` | string | — | Optional SHA-1 thumbprint to pin the sidecar's TLS certificate. Leave empty for CA-chain validation. |
|
||||
| `SharedSecret` | string | — | Shared secret token the sidecar expects on every connection. Required when `Enabled`. |
|
||||
| `Enabled` | bool | `false` | Enable the live `GatewayHistorianDataSource`. `false` → `NullHistorianDataSource` (empty reads). |
|
||||
| `Endpoint` | string | `""` | Absolute gateway URI, e.g. `https://host:5222`. Scheme selects transport (`https://` = TLS, `http://` = h2c plaintext). Required when `Enabled`. |
|
||||
| `ApiKey` | string | `""` | The gateway peppered-HMAC key (`histgw_<id>_<secret>`) sent as `Authorization: Bearer`. Required when `Enabled`. **Supply via env `ServerHistorian__ApiKey`.** |
|
||||
| `UseTls` | bool | `true` | Connect over TLS; must match the `Endpoint` scheme. |
|
||||
| `AllowUntrustedServerCertificate` | bool | `false` | Accept a self-signed / untrusted server certificate (dev / on-prem only). |
|
||||
| `CaCertificatePath` | string\|null | `null` | PEM CA file pinning the gateway's TLS chain. Null/empty uses the OS trust store. |
|
||||
| `CallTimeout` | TimeSpan | `00:00:30` | Per-call deadline applied to each unary gateway read. |
|
||||
| `MaxTieClusterOverfetch` | int | `65536` | Maximum samples the server will fetch in one shot to page through a tie cluster (multiple samples sharing one `SourceTimestamp`). A cluster larger than this ceiling fails `BadHistoryOperationUnsupported`. Raise to handle abnormally large tie clusters; the default covers all normal-data cases. |
|
||||
|
||||
> **Do not commit `SharedSecret` to `appsettings.json`.** Set it via an environment variable,
|
||||
> a secrets store, or a deployment-time overlay. The checked-in default is always empty.
|
||||
> **Do not commit `ApiKey` to `appsettings.json`.** Set it via the environment variable
|
||||
> `ServerHistorian__ApiKey`, a secrets store, or a deployment-time overlay. The checked-in default is
|
||||
> always empty.
|
||||
|
||||
> **Gateway-side prerequisites.** The target gateway must run `RuntimeDb:Enabled=true` (continuous
|
||||
> `WriteLiveValues`) + `RuntimeDb:EventReadsEnabled=true` (alarm-history `ReadEvents`), and the API key
|
||||
> must carry the scopes `historian:read`, `historian:write`, `historian:tags:write`.
|
||||
|
||||
> **Migration from the Wonderware backend.** Rename the old keys: `Host`/`Port` → `Endpoint`
|
||||
> (`https://host:5222`); `SharedSecret` → `ApiKey` (env `ServerHistorian__ApiKey`);
|
||||
> `ServerCertThumbprint` → `CaCertificatePath` (+ `UseTls` / `AllowUntrustedServerCertificate`).
|
||||
|
||||
The `ServerHistorian` section is independent of the `AlarmHistorian` section (the alarm
|
||||
write path). They share the same Wonderware sidecar process but hold separate client
|
||||
instances and separate `SharedSecret` values.
|
||||
write path) and the `ContinuousHistorization` section (driver-value capture). All three target the
|
||||
**same** gateway — but only `ServerHistorian` carries the connection (endpoint/key/TLS); the other two
|
||||
source it from there.
|
||||
|
||||
---
|
||||
|
||||
@@ -109,7 +124,8 @@ OPC UA client can discover historized capability from the node's attributes.
|
||||
**Equipment-folder event-notifier nodes** serve Event history. Every equipment folder that
|
||||
owns at least one alarm condition is already an event notifier; the server registers a
|
||||
`sourceName` (the equipment id) for each such folder and maps event history reads to the
|
||||
Wonderware historian using that source. Event-field projection supports the standard
|
||||
HistorianGateway using that source. (Alarm-history `ReadEvents` requires the gateway running
|
||||
`RuntimeDb:EventReadsEnabled=true`.) Event-field projection supports the standard
|
||||
`BaseEventType` select clauses — `EventId`, `SourceName`, `Time`, `ReceiveTime`, `Message`,
|
||||
and `Severity`; an unsupported select operand returns a null field (spec-conformant).
|
||||
|
||||
@@ -123,7 +139,7 @@ upstream `HistoryEvent` onto `HistoricalEvent` — the same six-field projection
|
||||
node-manager itself projects when serving event history. This is a **driver-level capability**:
|
||||
the OpcUaClient driver acts as a passthrough to whatever historian the upstream server exposes,
|
||||
and is independent of the single server-side `IHistorianDataSource` backend
|
||||
(`WonderwareHistorianClient` / `NullHistorianDataSource`) that the OtOpcUa node-manager
|
||||
(`GatewayHistorianDataSource` / `NullHistorianDataSource`) that the OtOpcUa node-manager
|
||||
dispatches HistoryRead to for tags on other drivers (Galaxy, Modbus, S7, etc.).
|
||||
|
||||
### Graceful degradation
|
||||
@@ -138,7 +154,7 @@ dispatches HistoryRead to for tags on other drivers (Galaxy, Modbus, S7, etc.).
|
||||
|
||||
A historized node with no historian configured never returns an error status — it returns
|
||||
empty. This means a deployment can author and publish historized tags before the historian
|
||||
sidecar is provisioned, without producing error spikes in connected clients.
|
||||
gateway is provisioned, without producing error spikes in connected clients.
|
||||
|
||||
### Continuation-point paging (Raw)
|
||||
|
||||
@@ -187,22 +203,14 @@ are disposed when the session closes). Resuming an unknown / evicted / released
|
||||
`BadContinuationPointInvalid`. `releaseContinuationPoints` drops the stored cursors without reading
|
||||
data.
|
||||
|
||||
### Total aggregate derivation
|
||||
### Total aggregate
|
||||
|
||||
The OPC UA `Total` aggregate is **supported** over the Wonderware backend. Because the
|
||||
Wonderware `AnalogSummary` query exposes no `Total` column, the value is derived client-side
|
||||
using the time-integral identity:
|
||||
|
||||
> **Total = time-weighted Average × interval-seconds**
|
||||
|
||||
The wire request is issued with the `Average` column; each returned bucket's value is
|
||||
multiplied by `interval.TotalSeconds` before the result is returned to the OPC UA client.
|
||||
Bucket status codes and timestamps are preserved unchanged. Null (unavailable) Average
|
||||
buckets produce a null Total (`BadNoData` downstream) — the scaling is not applied.
|
||||
|
||||
This derivation is exact for piecewise-constant (step) signals. For continuously varying
|
||||
signals it is an approximation identical to the one Wonderware would apply internally, so
|
||||
the result is consistent with what AVEVA Historian reports for the same window.
|
||||
The OPC UA `Total` aggregate is **supported** over the HistorianGateway backend. The gateway exposes a
|
||||
native **`Integral`** retrieval mode, so `Total` maps straight to it (`HistoryAggregateType.Total →
|
||||
RetrievalMode.Integral`) — no client-side scaling. (This replaces the retired Wonderware path, which had no
|
||||
`Total` column and derived it client-side as time-weighted `Average × interval-seconds`.) `Count` is
|
||||
likewise a native gateway mode. Bucket status codes and timestamps are preserved unchanged; empty / null
|
||||
buckets surface as `BadNoData`.
|
||||
|
||||
### Known limitations
|
||||
|
||||
@@ -213,12 +221,12 @@ the result is consistent with what AVEVA Historian reports for the same window.
|
||||
read and there is no "full page ⇒ maybe more" signal to page on. Returning the full result with
|
||||
no continuation point is spec-conformant.
|
||||
- **No modified-value history** (`HistoryReadModified`). Requests for modified values return
|
||||
`BadHistoryOperationUnsupported`. This is **infra-gated, not a server-code gap**: the AVEVA
|
||||
Wonderware historian backend (`IHistorianDataSource`, the TCP sidecar client) exposes only a
|
||||
current-value read path — there is no modified/edited-history surface to source the data from. The
|
||||
server-side override is in place (it cleanly rejects modified reads per node) and `IsReadModified`
|
||||
is honoured; serving real modified-value history is unblocked only once the historian client/sidecar
|
||||
grows a modified-read RPC. Until then, rejecting is the correct, spec-conformant behaviour.
|
||||
`BadHistoryOperationUnsupported`. This is **infra-gated, not a server-code gap**: the HistorianGateway
|
||||
backend (`GatewayHistorianDataSource`) exposes only a current-value read path — there is no
|
||||
modified/edited-history surface to source the data from. The server-side override is in place (it cleanly
|
||||
rejects modified reads per node) and `IsReadModified` is honoured; serving real modified-value history is
|
||||
unblocked only once the gateway grows a modified-read RPC. Until then, rejecting is the correct,
|
||||
spec-conformant behaviour.
|
||||
|
||||
### Redundancy and authorization
|
||||
|
||||
@@ -309,14 +317,16 @@ above), but is not exposed by this bundled CLI.
|
||||
|
||||
## Live /run gate
|
||||
|
||||
The live read gate requires the Wonderware historian sidecar running on the WW Historian VM
|
||||
(`10.100.0.48`) and AVEVA Historian healthy. Set `ServerHistorian:Enabled=true` with the
|
||||
correct `Host`, `Port`, and `SharedSecret` in `appsettings.json` (or via environment
|
||||
variables), then deploy and publish at least one historized Galaxy tag. The gate is
|
||||
operator-driven — it is not part of the local docker-dev rig.
|
||||
The live read gate requires a reachable `ZB.MOM.WW.HistorianGateway` (VPN to `wonder-sql-vd03`) with the
|
||||
AVEVA Historian behind it healthy. Set `ServerHistorian:Enabled=true` with the correct `Endpoint`
|
||||
(`https://host:5222`) and supply `ServerHistorian__ApiKey` via the environment, then deploy and publish at
|
||||
least one historized Galaxy tag. The gate is operator-driven — it is not part of the local docker-dev rig.
|
||||
The gateway-backed driver also ships an env-gated live suite (`Category=LiveIntegration`); see the
|
||||
`HISTGW_GATEWAY_ENDPOINT` / `HISTGW_GATEWAY_APIKEY` / `HISTGW_TEST_TAG` / `HISTGW_WRITE_SANDBOX_TAG` /
|
||||
`HISTGW_ALARM_SOURCE` env vars (it skips cleanly when they are absent).
|
||||
|
||||
See [AlarmHistorian.md](AlarmHistorian.md) for the historian sidecar setup and
|
||||
[ServiceHosting.md](ServiceHosting.md) for the sidecar service configuration.
|
||||
See [AlarmHistorian.md](AlarmHistorian.md) for the alarm write path and
|
||||
[ServiceHosting.md](ServiceHosting.md) for the (external) HistorianGateway deployment.
|
||||
|
||||
---
|
||||
|
||||
@@ -373,7 +383,7 @@ phases and are recorded here so future audits don't re-flag them.
|
||||
## See also
|
||||
|
||||
- [docs/plans/2026-06-14-galaxy-phase-c-historian-design.md](plans/2026-06-14-galaxy-phase-c-historian-design.md) — full design and implementation notes
|
||||
- [AlarmHistorian.md](AlarmHistorian.md) — alarm write path; shares the same Wonderware sidecar
|
||||
- [AlarmHistorian.md](AlarmHistorian.md) — alarm write path; drains to the same HistorianGateway (`SendEvent`)
|
||||
- [AlarmTracking.md](AlarmTracking.md) — OPC UA Part 9 alarm surface (event history source)
|
||||
- [Client.CLI.md](Client.CLI.md) — full `historyread` flag reference
|
||||
- [ScriptedAlarms.md](ScriptedAlarms.md) §"Native driver alarms" — the Phase B `alarm` object in `TagConfig` (parallel carrier)
|
||||
|
||||
Reference in New Issue
Block a user