diff --git a/docs/plans/evidence/2026-11-10-opcua2/aveva-history-801.md b/docs/plans/evidence/2026-11-10-opcua2/aveva-history-801.md new file mode 100644 index 000000000..b11e99e63 --- /dev/null +++ b/docs/plans/evidence/2026-11-10-opcua2/aveva-history-801.md @@ -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). diff --git a/docs/plans/evidence/2026-11-10-opcua2/aveva-history-801/aveva-gate-sdk1-efb9197a.json.gz b/docs/plans/evidence/2026-11-10-opcua2/aveva-history-801/aveva-gate-sdk1-efb9197a.json.gz new file mode 100644 index 000000000..0820fcab4 Binary files /dev/null and b/docs/plans/evidence/2026-11-10-opcua2/aveva-history-801/aveva-gate-sdk1-efb9197a.json.gz differ diff --git a/docs/plans/evidence/2026-11-10-opcua2/aveva-history-801/aveva-gate-sdk2-d46044f5.json.gz b/docs/plans/evidence/2026-11-10-opcua2/aveva-history-801/aveva-gate-sdk2-d46044f5.json.gz new file mode 100644 index 000000000..df8e370c6 Binary files /dev/null and b/docs/plans/evidence/2026-11-10-opcua2/aveva-history-801/aveva-gate-sdk2-d46044f5.json.gz differ diff --git a/docs/plans/evidence/2026-11-10-opcua2/aveva-history-801/compare.txt b/docs/plans/evidence/2026-11-10-opcua2/aveva-history-801/compare.txt new file mode 100644 index 000000000..80eecd7ab --- /dev/null +++ b/docs/plans/evidence/2026-11-10-opcua2/aveva-history-801/compare.txt @@ -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 diff --git a/docs/plans/evidence/2026-11-10-opcua2/aveva-history-801/histgw-cli-read-raw.tar.gz b/docs/plans/evidence/2026-11-10-opcua2/aveva-history-801/histgw-cli-read-raw.tar.gz new file mode 100644 index 000000000..79dc8831e Binary files /dev/null and b/docs/plans/evidence/2026-11-10-opcua2/aveva-history-801/histgw-cli-read-raw.tar.gz differ diff --git a/docs/plans/evidence/2026-11-10-opcua2/aveva-history-801/test-runs.txt b/docs/plans/evidence/2026-11-10-opcua2/aveva-history-801/test-runs.txt new file mode 100644 index 000000000..9e29c3fad --- /dev/null +++ b/docs/plans/evidence/2026-11-10-opcua2/aveva-history-801/test-runs.txt @@ -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)