docs(evidence): #801 real-AVEVA history gate — SDK 2.0.1 identical to 1.5.378.182
v2-ci / unit-tests (push) Waiting to run

Both builds served HistoryRead from the real AVEVA Historian via the DEV
HistorianGateway (vd03, ea9f21c): 225 cases / 3,855 rows / events, 0 differences.
Raw also matches direct SQL on Runtime.dbo.History (6/6) and histgw-cli; simple
aggregates match a recomputation from raw (336/336). Modified, HistoryUpdate,
seven refused aggregates and HistoryServerCapabilities answer as before. No write
reached the historian. Findings (pre-existing, not SDK): all HistoryServerCapabilities
booleans false; AVEVA attribute-alarm events unreachable per source.

Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
This commit is contained in:
Joseph Doherty
2026-11-12 10:02:21 -05:00
parent f607ee7502
commit 008f0b36b2
6 changed files with 116 additions and 0 deletions
@@ -0,0 +1,78 @@
# #801: real AVEVA history through HistorianGateway, SDK 2.0.1 vs 1.5.378.182 (2026-11-12)
**Scope.** This covers the "real AVEVA historian" part of [#801](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/801) only.
Consumer compatibility, pair/failover and #800 are separate gates and are not covered here.
**Result.** SDK 2.0.1 serves HistoryRead from the real AVEVA Historian with output identical to SDK 1.5.378.182. Both builds
ran the same queries over the same fixed past windows, through the same gateway. The comparison covered 225 cases,
3,855 rows and the event rows, and found 0 differences.
Raw rows also match two independent sources:
- direct SQL on `Runtime.dbo.History`, read-only (6/6 tag-windows);
- HistorianGateway's own CLI.
The simple aggregates also match a recomputation from the raw rows (336/336).
Unsupported capabilities answer the same way on both builds: modified history, HistoryUpdate, the seven refused
aggregates and the HistoryServerCapabilities flags.
## Setup
| | |
|---|---|
| Server under test | Local `d46044f5` = master `438484fd` + this harness (SDK **2.0.1**). It was pushed as `f607ee75` after a rebase onto `c266971d`, which adds only #789 docs and a ScriptRunner perf test, so no server or historian code changed. Baseline: `efb9197a`, master's first parent before the SDK 2 merge `b36e688f` (SDK **1.5.378.182**). Both use HistorianGateway Client/Contracts **1.0.0**. |
| How | `AvevaGatewayHistoryLiveGateTests` boots the OtOpcUa server in-process on the Mac. It wires the **production** Gateway backend (`HistorianBackendRegistry` → `GatewayHistorianDataSource`, default `ServerHistorianOptions`) and reads over a real SDK client session (`None`, anonymous, ACL off). The SDK 1 build runs the same file with [`sdk1-baseline-port.patch`](../../../../scripts/validation/aveva-history-801/sdk1-baseline-port.patch), which changes API spelling only. |
| Gateway | wonder-app-vd03 (DEV) `http://…:5221` (h2c), HistorianGateway **`ea9f21c`**. The SHA was read from the deployed `Server.dll` over SMB. The gateway talks gRPC to the AVEVA 2023 R2 historian on wonder-sql-vd03. The gateway config and service were not touched. |
| Credential | vd03 OtOpcUa's existing `ServerHistorian__ApiKey` (`OtOpcUa-wonder`, from the box's env backup, read over SMB). It was used only through env/a 0600 file and was never printed. A bogus key gets `Unauthenticated` (negative control). |
| Tags | `ZIPSEN.WTC1` (float, ~2 s), `ZIPSEN.OvenPressureTransducer` (float, sparse), `SysPerfCPUTotal` (5 s). The tag `Z28061` was not used. |
| Windows (UTC) | W1 `2026-11-10 11:30–11:41`: brackets the DevWinPlatform System.Stop at 11:32:35. It has a stored NULL/Bad row at 11:32:36, a gap, and a recovery row at 11:38:34 (QualityDetail 248). W2 `2026-11-11 14:00–14:10`: steady. Events: `2026-11-05 15:00` → `2026-11-12 00:00`. |
| Writes | **None reached the historian.** The Gateway provider has no updater (asserted). The only HistoryUpdate went to a node bound to the designated sandbox `HistGW.LiveTest.Sandbox` (`HISTORIAN_WRITE_SANDBOX_TAG`), and the server refused it. No provisioner, recorder or alarm writer was built. |
| Runs | SDK 2 run at 2026-11-12 15:00:06Z, SDK 1 run straight after. Both passed with every in-test assertion green. |
## Capability table
"Reference" means the same backend's direct `IHistorianDataSource` answer (asserted in-test, row by row: value, StatusCode,
SourceTimestamp), plus the SDK 1 run (diffed) and, for raw, SQL and histgw-cli.
| Capability | Query | SDK 2 result | Reference | Verdict |
|---|---|---|---|---|
| Raw | `ReadRawModifiedDetails`, `NumValuesPerNode=0`, `ReturnBounds=false`, 3 tags × 2 windows | 80 / 300 / 14 / 35 / 51 / 120 rows. W1's stored NULL is **null + `Bad` (0x80000000)**. QD 248 and QD 44 rows are Good. AVEVA's synthetic Q133 start row is dropped (#687). | Backend: identical. SDK 1: identical. SQL `History` (Full, UTC): 6/6 tag-windows match on value, ms timestamp and Good/Bad class. histgw-cli: 4/6 complete matches. On the 2 W1 windows of the ZIPSEN tags, histgw-cli stops at the NULL, but the prefix matches. | **PASS** |
| Raw paging | same, page 37 via continuation points | Concatenated pages (1–9 pages) equal the unpaged read. | identical on SDK 1 | **PASS** |
| Raw reverse | `StartTime > EndTime`, 25 | Newest first. The Part 11 window `(start, end]` includes a sample stored exactly at the end (SysPerfCPUTotal 11:41:00). | Backend `(start, end]`, reversed: identical. SDK 1: identical. | **PASS** |
| Raw bounds | `ReturnBounds=true` | Real prior and next samples are added at each end (e.g. 11:29:59.62 / 11:44:09.107). | SDK 1: identical | **PASS** |
| Modified | `IsReadModified=true` | `BadHistoryOperationUnsupported` | SDK 1: identical (Gateway has no modified history) | **PASS** (unsupported preserved) |
| Processed | 60 s interval, Average, Count, Minimum, Maximum, Range, Start, End, Delta, MinimumActualTime, MaximumActualTime | Values match. Statuses: Good+Calculated `0x00000401`. The outage interval is `UncertainDataSubNormal` `0x40A40401`. Empty intervals are `BadNoData` `0x809B0401`. Start/End carry the raw sample's own time and status (End of 11:32 = the NULL: null + Bad). | Backend: identical (asserted). SDK 1: identical (application aggregate semantics unchanged, `TreatUncertainAsBad=false`). Recomputed from raw: 336/336 non-empty all-Good intervals. | **PASS** |
| Processed, refused | TimeAverage, Total, TimeAverage2, Interpolative, PercentGood, DurationGood, StandardDeviationSample | `BadAggregateNotSupported` | SDK 1: identical. Native time-weighted is still off; no string backfill. | **PASS** (no new aggregates) |
| AtTime | window start, a stored time, a midpoint | Stored value exact (Good). Midpoints are linear + Interpolated `0x00000402`. | Backend: identical. SDK 1: identical. | **PASS** |
| Events | `ReadEventDetails` on folder `DevWinPlatform`: all, 1-event pages, reverse | 2 events, `System.Stop` 11:32:35.095Z / `System.Start` 11:43:46.563Z, with EventId, Time, ReceiveTime, SourceName, Message and Severity. Paging takes 2 pages. Reverse returns the newest first. | Backend: identical. SDK 1: identical. | **PASS** |
| Events, attribute alarm | same, folder `DevWinPlatform.CPULoadAvg` | `GoodNoData`. The gateway reports these 70 events with SourceName `DevWinPlatform.CPULoadAvg` in an unfiltered read, but a per-source read for that name returns none (Source_Object is `DevWinPlatform`). | SDK 1: identical | **PASS** (pre-existing gap, see Findings) |
| HistoryUpdate | Insert and DeleteRaw on the `HistGW.LiveTest.Sandbox`-bound node | `BadHistoryOperationUnsupported` on both | SDK 1: identical. No updater, no backend call. | **PASS** (unsupported preserved) |
| Node metadata | Historizing, AccessLevel, UserAccessLevel | `true`, `5` (CurrentRead + HistoryRead, **no HistoryWrite**), `5`, DataType `i=11` | SDK 1: identical | **PASS** |
| History capabilities | `HistoryServerCapabilities` children + AggregateFunctions | AggregateFunctions = the Gateway's ten. Every boolean capability flag is `false` (incl. AccessHistoryData/Events). `MaxReturnDataValues=10000`, `MaxReturnEventValues=0`. | SDK 1: identical | **PASS** (existing metadata preserved, see Findings) |
| Missing tag | raw on `OtOpcUa.Gate801.DoesNotExist` | `GoodNoData`, 0 rows (#653) | SDK 1: identical | **PASS** |
| SysTimeSec raw | 10 s window | Good, 10 rows, 69 ms | SDK 1: identical | **PASS**, informational. historiangw#29's timeout did **not** reproduce on `ea9f21c`. |
ServerTimestamp on history values is the read time (`now`) on both builds. Its presence is compared; its value is not.
## Findings (none caused by the SDK)
1. **The boolean flags under `HistoryServerCapabilities` are all `false`** on both SDKs, including AccessHistoryDataCapability and
AccessHistoryEventsCapability, even though the server serves data and event history. Clients that check these flags before
calling HistoryRead would skip it. This is existing behaviour and the upgrade preserves it. It is a candidate follow-up, not filed.
2. **AVEVA attribute-alarm events cannot be read per source.** An unfiltered read reports SourceName = `Source_ProcessVariable`
(e.g. `DevWinPlatform.CPULoadAvg`), but a per-source read with that name returns nothing. Reading by `Source_Object` returns
only the rows whose SourceName equals it: OtOpcUa's client-side filter drops the rest. Native AVEVA alarms are therefore
invisible through OtOpcUa's event history unless their object-level name matches. This is a gateway/consumer contract gap,
identical on SDK 1. It is a candidate follow-up, not filed.
3. **historiangw `histgw-cli read-raw` aborts on a stored NULL.** Its JSON writer cannot emit NaN. This is a CLI-only issue: OtOpcUa
maps the same row to null + Bad.
4. **historiangw#29 (SysTimeSec raw timeout) did not reproduce** against the `ea9f21c` gateway.
## Files
- [`aveva-history-801/aveva-gate-sdk2-d46044f5.json.gz`](aveva-history-801/aveva-gate-sdk2-d46044f5.json.gz), sha256 of the JSON `00b5ecb6…fc27`.
- [`aveva-history-801/aveva-gate-sdk1-efb9197a.json.gz`](aveva-history-801/aveva-gate-sdk1-efb9197a.json.gz), sha256 `21fe4985…751d`.
- [`aveva-history-801/compare.txt`](aveva-history-801/compare.txt): diff, SQL, recompute and histgw-cli outputs.
[`histgw-cli-read-raw.tar.gz`](aveva-history-801/histgw-cli-read-raw.tar.gz) holds the CLI rows.
[`test-runs.txt`](aveva-history-801/test-runs.txt) holds the two test-run summaries.
- Reproduction: [`scripts/validation/aveva-history-801/README.md`](../../../../scripts/validation/aveva-history-801/README.md).
@@ -0,0 +1,36 @@
$ diff_runs.py sdk2 sdk1
A=sdk2-d46044f5 sdk=2.0.1.0 cases=225 failures=0
B=sdk1-efb9197a sdk=1.5.378.0 cases=225 failures=0
compared 225 cases: identical=225 differing=0; rows compared=3855, events compared=8 (ServerTimestamp compared for presence only: it is the read time)
$ sql_raw_check.py sdk2-d46044f5
ZIPSEN.WTC1 2026-11-10T11:30:00 sql_stored=80 (boundary rows dropped=1) wire=80 quality(Q,QD,OPC)={('0', '192', '192'): 78, ('1', '24', '0'): 1, ('0', '248', '192'): 1} -> MATCH
ZIPSEN.WTC1 2026-11-11T14:00:00 sql_stored=300 (boundary rows dropped=1) wire=300 quality(Q,QD,OPC)={('0', '192', '192'): 300} -> MATCH
ZIPSEN.OvenPressureTransducer 2026-11-10T11:30:00 sql_stored=14 (boundary rows dropped=1) wire=14 quality(Q,QD,OPC)={('0', '192', '192'): 12, ('1', '24', '0'): 1, ('0', '248', '192'): 1} -> MATCH
ZIPSEN.OvenPressureTransducer 2026-11-11T14:00:00 sql_stored=35 (boundary rows dropped=1) wire=35 quality(Q,QD,OPC)={('0', '192', '192'): 35} -> MATCH
SysPerfCPUTotal 2026-11-10T11:30:00 sql_stored=51 (boundary rows dropped=0) wire=51 quality(Q,QD,OPC)={('0', '192', '192'): 50, ('0', '44', '192'): 1} -> MATCH
SysPerfCPUTotal 2026-11-11T14:00:00 sql_stored=120 (boundary rows dropped=0) wire=120 quality(Q,QD,OPC)={('0', '192', '192'): 120} -> MATCH
6 tag-windows compared, 0 mismatching
$ aggregate_recompute.py sdk2-d46044f5
336 non-empty interval values recomputed from raw, 0 mismatching
$ sql_raw_check.py sdk1-efb9197a
ZIPSEN.WTC1 2026-11-10T11:30:00 sql_stored=80 (boundary rows dropped=1) wire=80 quality(Q,QD,OPC)={('0', '192', '192'): 78, ('1', '24', '0'): 1, ('0', '248', '192'): 1} -> MATCH
ZIPSEN.WTC1 2026-11-11T14:00:00 sql_stored=300 (boundary rows dropped=1) wire=300 quality(Q,QD,OPC)={('0', '192', '192'): 300} -> MATCH
ZIPSEN.OvenPressureTransducer 2026-11-10T11:30:00 sql_stored=14 (boundary rows dropped=1) wire=14 quality(Q,QD,OPC)={('0', '192', '192'): 12, ('1', '24', '0'): 1, ('0', '248', '192'): 1} -> MATCH
ZIPSEN.OvenPressureTransducer 2026-11-11T14:00:00 sql_stored=35 (boundary rows dropped=1) wire=35 quality(Q,QD,OPC)={('0', '192', '192'): 35} -> MATCH
SysPerfCPUTotal 2026-11-10T11:30:00 sql_stored=51 (boundary rows dropped=0) wire=51 quality(Q,QD,OPC)={('0', '192', '192'): 50, ('0', '44', '192'): 1} -> MATCH
SysPerfCPUTotal 2026-11-11T14:00:00 sql_stored=120 (boundary rows dropped=0) wire=120 quality(Q,QD,OPC)={('0', '192', '192'): 120} -> MATCH
6 tag-windows compared, 0 mismatching
$ aggregate_recompute.py sdk1-efb9197a
336 non-empty interval values recomputed from raw, 0 mismatching
$ hgcli_raw_check.py sdk2
SysPerfCPUTotal 2026-11-10T11:30:00 histgw-cli=51 wire=51 compared=51 MATCH
SysPerfCPUTotal 2026-11-11T14:00:00 histgw-cli=120 wire=120 compared=120 MATCH
ZIPSEN.OvenPressureTransducer 2026-11-10T11:30:00 histgw-cli=11 wire=14 compared=11 MATCH (histgw-cli stopped at the stored NULL: its JSON writer cannot emit NaN)
ZIPSEN.OvenPressureTransducer 2026-11-11T14:00:00 histgw-cli=35 wire=35 compared=35 MATCH
ZIPSEN.WTC1 2026-11-10T11:30:00 histgw-cli=77 wire=80 compared=77 MATCH (histgw-cli stopped at the stored NULL: its JSON writer cannot emit NaN)
ZIPSEN.WTC1 2026-11-11T14:00:00 histgw-cli=300 wire=300 compared=300 MATCH
@@ -0,0 +1,2 @@
Passed! - Failed: 0, Passed: 1, Skipped: 0, Total: 1, Duration: 23 s - ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests.dll (net10.0)
Passed! - Failed: 0, Passed: 1, Skipped: 0, Total: 1, Duration: 17 s - ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests.dll (net10.0)