Wire Galaxy security_classification to OPC UA AccessLevel (ReadOnly for SecuredWrite/VerifiedWrite/ViewOnly). Use deployed package chain for attribute queries to exclude undeployed attributes. Group primitive attributes under their parent variable node (merged Variable+Object). Add is_historized and is_alarm detection via HistoryExtension/AlarmExtension primitives. Implement OPC UA HistoryRead backed by Wonderware Historian Runtime database. Implement AlarmConditionState nodes driven by InAlarm with condition refresh support. Add historyread and alarms CLI commands for testing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
315 lines
11 KiB
Markdown
315 lines
11 KiB
Markdown
# OPC UA Historical Data Access Plan
|
|
|
|
## Context
|
|
|
|
Galaxy attributes with `HistoryExtension` primitives are historized by the Wonderware Historian. The Historian exposes its data via SQL queries against the `Runtime` database. This plan documents how to implement OPC UA Historical Data Access (HDA) so OPC UA clients can read historical values through the server.
|
|
|
|
## 1. Wonderware Historian Data Source
|
|
|
|
### Connection
|
|
|
|
- **Database**: `Runtime` on `localhost` (Windows Auth)
|
|
- **Constraint**: History views require a `WHERE TagName='...'` clause — queries without a tag filter will fail.
|
|
|
|
### History View Schema (31 columns)
|
|
|
|
Key columns for OPC UA HDA:
|
|
|
|
| Column | Type | Description |
|
|
|---|---|---|
|
|
| `DateTime` | datetime2 | Timestamp of the value |
|
|
| `TagName` | nvarchar(256) | Galaxy tag reference (e.g., `TestMachine_001.TestHistoryValue`) |
|
|
| `Value` | float | Numeric value |
|
|
| `vValue` | nvarchar(4000) | String representation of value |
|
|
| `Quality` | tinyint | Quality code (0=Good, 1=Bad, 133=Uncertain) |
|
|
| `QualityDetail` | int | Detailed quality (192=Good) |
|
|
| `OPCQuality` | int | OPC-style quality code |
|
|
|
|
### Raw Data Query
|
|
|
|
```sql
|
|
SELECT DateTime, Value, vValue, Quality, QualityDetail
|
|
FROM Runtime.dbo.History
|
|
WHERE TagName = 'TestMachine_001.TestHistoryValue'
|
|
AND DateTime BETWEEN @StartTime AND @EndTime
|
|
ORDER BY DateTime
|
|
```
|
|
|
|
### Aggregate Data (AnalogSummaryHistory)
|
|
|
|
The Historian provides pre-calculated aggregates via the `AnalogSummaryHistory` view:
|
|
|
|
| Column | Description |
|
|
|---|---|
|
|
| `StartDateTime` | Start of aggregate interval |
|
|
| `EndDateTime` | End of aggregate interval |
|
|
| `First` | First value in interval |
|
|
| `Last` | Last value in interval |
|
|
| `Minimum` | Minimum value |
|
|
| `Maximum` | Maximum value |
|
|
| `Average` | Average value |
|
|
| `StdDev` | Standard deviation |
|
|
| `Integral` | Time-weighted integral |
|
|
| `ValueCount` | Number of values |
|
|
|
|
```sql
|
|
SELECT StartDateTime, EndDateTime, Average, Minimum, Maximum, ValueCount
|
|
FROM Runtime.dbo.AnalogSummaryHistory
|
|
WHERE TagName = 'TestMachine_001.TestHistoryValue'
|
|
AND StartDateTime BETWEEN @StartTime AND @EndTime
|
|
AND wwResolution = @IntervalMs
|
|
```
|
|
|
|
### Retrieval Modes
|
|
|
|
| Mode | Description |
|
|
|---|---|
|
|
| `DELTA` | Change-based retrieval (default) — returns values when they changed |
|
|
| `CYCLIC` | Periodic sampling — returns interpolated values at fixed intervals |
|
|
|
|
### Quality Mapping
|
|
|
|
| Historian Quality | OPC UA StatusCode |
|
|
|---|---|
|
|
| 0 (Good) | `Good` (0x00000000) |
|
|
| 1 (Bad) | `Bad` (0x80000000) |
|
|
| 133 (Uncertain) | `Uncertain` (0x40000000) |
|
|
|
|
### Test Data
|
|
|
|
Tag: `TestMachine_001.TestHistoryValue` (Analog, Integer)
|
|
- 4 records from 2026-03-26 00:44 to 01:09
|
|
- Values: 0, 3, 4, 7, 9
|
|
- InterpolationType: STAIRSTEP
|
|
|
|
## 2. OPC UA HDA Implementation
|
|
|
|
### Marking Variables as Historized
|
|
|
|
For attributes where `is_historized = 1` from the Galaxy query:
|
|
|
|
```csharp
|
|
variable.Historizing = true;
|
|
variable.AccessLevel |= AccessLevels.HistoryRead;
|
|
variable.UserAccessLevel |= AccessLevels.HistoryRead;
|
|
```
|
|
|
|
This tells OPC UA clients the variable supports `HistoryRead` requests.
|
|
|
|
### Server-Side Handler
|
|
|
|
Override `HistoryRead` on `LmxNodeManager` (inherits from `CustomNodeManager2`):
|
|
|
|
```csharp
|
|
public override void HistoryRead(
|
|
OperationContext context,
|
|
HistoryReadDetails details,
|
|
TimestampsToReturn timestampsToReturn,
|
|
bool releaseContinuationPoints,
|
|
IList<HistoryReadValueId> nodesToRead,
|
|
IList<HistoryReadResult> results,
|
|
IList<ServiceResult> errors)
|
|
```
|
|
|
|
Dispatch based on `details` type:
|
|
- `ReadRawModifiedDetails` → `HistoryReadRaw` → query `Runtime.dbo.History`
|
|
- `ReadProcessedDetails` → `HistoryReadProcessed` → query `Runtime.dbo.AnalogSummaryHistory`
|
|
- `ReadAtTimeDetails` → `HistoryReadAtTime` → query with `wwRetrievalMode = 'Cyclic'`
|
|
|
|
### ReadRaw Implementation
|
|
|
|
Map `HistoryReadRawModifiedDetails` to a Historian SQL query:
|
|
|
|
| OPC UA Parameter | SQL Mapping |
|
|
|---|---|
|
|
| `StartTime` | `DateTime >= @StartTime` |
|
|
| `EndTime` | `DateTime <= @EndTime` |
|
|
| `NumValuesPerNode` | `TOP @NumValues` |
|
|
| `ReturnBounds` | Include one value before StartTime and one after EndTime |
|
|
|
|
Result: populate `HistoryData` with `DataValue` list:
|
|
```csharp
|
|
new DataValue
|
|
{
|
|
Value = row.Value,
|
|
SourceTimestamp = row.DateTime,
|
|
StatusCode = MapQuality(row.Quality)
|
|
}
|
|
```
|
|
|
|
### ReadProcessed Implementation
|
|
|
|
Map `HistoryReadProcessedDetails` to `AnalogSummaryHistory`:
|
|
|
|
| OPC UA Aggregate | Historian Column |
|
|
|---|---|
|
|
| `Average` | `Average` |
|
|
| `Minimum` | `Minimum` |
|
|
| `Maximum` | `Maximum` |
|
|
| `Count` | `ValueCount` |
|
|
| `Start` | `First` |
|
|
| `End` | `Last` |
|
|
| `StandardDeviationPopulation` | `StdDev` |
|
|
|
|
`ProcessingInterval` maps to `wwResolution` (milliseconds).
|
|
|
|
### Continuation Points for Paging
|
|
|
|
When `NumValuesPerNode` limits the result:
|
|
|
|
1. Query `NumValuesPerNode + 1` rows
|
|
2. If more exist, save a continuation point (store last timestamp + query params)
|
|
3. Return `StatusCodes.GoodMoreData` with the continuation point
|
|
4. On next request, restore the continuation point and resume from last timestamp
|
|
|
|
Use `Session.SaveHistoryContinuationPoint()` / `RestoreHistoryContinuationPoint()` to manage state.
|
|
|
|
### Tag Name Resolution
|
|
|
|
The `FullTagReference` stored on each variable node (e.g., `TestMachine_001.TestHistoryValue`) is exactly the `TagName` used in the Historian query — no translation needed.
|
|
|
|
## 3. Galaxy Repository Detection
|
|
|
|
Already implemented: `is_historized` column in the attributes queries detects `HistoryExtension` primitives in the deployed package chain.
|
|
|
|
## 4. Implementation Steps
|
|
|
|
### Phase 1: Mark historized nodes
|
|
- Read `is_historized` from query results into `GalaxyAttributeInfo`
|
|
- In `LmxNodeManager.CreateAttributeVariable`, set `Historizing = true` and add `HistoryRead` to `AccessLevel`
|
|
|
|
### Phase 2: Historian data source
|
|
- New class: `HistorianDataSource` — executes SQL queries against `Runtime.dbo.History` and `AnalogSummaryHistory`
|
|
- Connection string configurable in `appsettings.json`
|
|
- Parameterized queries only (no dynamic SQL)
|
|
|
|
### Phase 3: HistoryRead handler
|
|
- Override `HistoryRead` on `LmxNodeManager`
|
|
- Implement `HistoryReadRaw` — query `History` view, map results to `HistoryData`
|
|
- Implement `HistoryReadProcessed` — query `AnalogSummaryHistory`, map aggregates
|
|
- Implement continuation points for large result sets
|
|
|
|
### Phase 4: Testing
|
|
- Unit tests for quality mapping, tag name resolution, SQL parameter building
|
|
- Integration test: create a historized variable, verify `Historizing = true` and `HistoryRead` access level
|
|
- Manual test: use OPC UA client to read historical data from deployed server
|
|
|
|
## 5. OPC UA CLI Tool — History Command
|
|
|
|
Add a `historyread` command to `tools/opcuacli-dotnet/` for manual testing of HDA.
|
|
|
|
### Usage
|
|
|
|
```bash
|
|
# Read raw history (last 24 hours)
|
|
dotnet run -- historyread -u opc.tcp://localhost:4840/LmxOpcUa -n "ns=1;s=TestMachine_001.TestHistoryValue"
|
|
|
|
# Read raw history with explicit time range
|
|
dotnet run -- historyread -u opc.tcp://localhost:4840/LmxOpcUa -n "ns=1;s=TestMachine_001.TestHistoryValue" --start "2026-03-25" --end "2026-03-30"
|
|
|
|
# Read with max values limit
|
|
dotnet run -- historyread -u opc.tcp://localhost:4840/LmxOpcUa -n "ns=1;s=TestMachine_001.TestHistoryValue" --start "2026-03-25" --end "2026-03-30" --max 100
|
|
|
|
# Read processed/aggregate history (1-hour intervals, Average)
|
|
dotnet run -- historyread -u opc.tcp://localhost:4840/LmxOpcUa -n "ns=1;s=TestMachine_001.TestHistoryValue" --start "2026-03-25" --end "2026-03-30" --aggregate Average --interval 3600000
|
|
```
|
|
|
|
### Command Options
|
|
|
|
| Flag | Description |
|
|
|------|-------------|
|
|
| `-u, --url` | OPC UA server endpoint URL (required) |
|
|
| `-n, --node` | Node ID to read history for (required) |
|
|
| `--start` | Start time, ISO 8601 or date string (default: 24 hours ago) |
|
|
| `--end` | End time, ISO 8601 or date string (default: now) |
|
|
| `--max` | Maximum number of values to return (default: 1000) |
|
|
| `--aggregate` | Aggregate function: Average, Minimum, Maximum, Count (default: none = raw) |
|
|
| `--interval` | Processing interval in milliseconds for aggregates (default: 3600000 = 1 hour) |
|
|
|
|
### Output Format
|
|
|
|
**Raw history:**
|
|
```
|
|
History for ns=1;s=TestMachine_001.TestHistoryValue (2026-03-25 → 2026-03-30)
|
|
|
|
Timestamp Value Status
|
|
2026-03-26T00:44:03.000Z 0 Good
|
|
2026-03-26T00:52:17.000Z 3 Good
|
|
2026-03-26T01:01:44.000Z 7 Good
|
|
2026-03-26T01:09:00.000Z 9 Good
|
|
|
|
4 values returned.
|
|
```
|
|
|
|
**Aggregate history:**
|
|
```
|
|
History for ns=1;s=TestMachine_001.TestHistoryValue (Average, interval=3600000ms)
|
|
|
|
Timestamp Value Status
|
|
2026-03-26T00:00:00.000Z 4.75 Good
|
|
|
|
1 values returned.
|
|
```
|
|
|
|
### Implementation
|
|
|
|
New file: `tools/opcuacli-dotnet/Commands/HistoryReadCommand.cs`
|
|
|
|
Uses the OPC UA client SDK's `Session.ReadRawHistory` and `Session.ReadProcessedHistory` methods (or `HistoryReadAsync` with appropriate `HistoryReadDetails`):
|
|
|
|
```csharp
|
|
// Raw read
|
|
var details = new ReadRawModifiedDetails
|
|
{
|
|
StartTime = startTime,
|
|
EndTime = endTime,
|
|
NumValuesPerNode = (uint)maxValues,
|
|
IsReadModified = false,
|
|
ReturnBounds = false
|
|
};
|
|
|
|
// Processed read
|
|
var details = new ReadProcessedDetails
|
|
{
|
|
StartTime = startTime,
|
|
EndTime = endTime,
|
|
ProcessingInterval = intervalMs,
|
|
AggregateType = new NodeIdCollection { aggregateNodeId }
|
|
};
|
|
```
|
|
|
|
Follow the same pattern as existing commands: use `OpcUaHelper.ConnectAsync()`, parse NodeId, call history read, print results.
|
|
|
|
### Continuation Point Handling
|
|
|
|
If the server returns `GoodMoreData` with a continuation point, automatically follow up with subsequent requests until all data is retrieved or `--max` is reached.
|
|
|
|
### README Update
|
|
|
|
Add `historyread` section to `tools/opcuacli-dotnet/README.md` documenting the new command.
|
|
|
|
## 6. Files to Modify/Create
|
|
|
|
| File | Change |
|
|
|---|---|
|
|
| `src/.../Domain/GalaxyAttributeInfo.cs` | Add `IsHistorized` property |
|
|
| `src/.../GalaxyRepository/GalaxyRepositoryService.cs` | Read `is_historized` column |
|
|
| `src/.../OpcUa/LmxNodeManager.cs` | Set `Historizing`/`AccessLevel` for historized nodes; override `HistoryRead` |
|
|
| `src/.../Configuration/HistorianConfiguration.cs` | NEW — connection string, query timeout |
|
|
| `src/.../Historian/HistorianDataSource.cs` | NEW — SQL queries against Runtime DB |
|
|
| `appsettings.json` | Add `Historian` section with connection string |
|
|
| `tools/opcuacli-dotnet/Commands/HistoryReadCommand.cs` | NEW — `historyread` CLI command |
|
|
| `tools/opcuacli-dotnet/README.md` | Add `historyread` command documentation |
|
|
|
|
## 6. Configuration
|
|
|
|
```json
|
|
{
|
|
"Historian": {
|
|
"ConnectionString": "Server=localhost;Database=Runtime;Integrated Security=true;",
|
|
"CommandTimeoutSeconds": 30,
|
|
"MaxValuesPerRead": 10000
|
|
}
|
|
}
|
|
```
|