Files
lmxopcua/historian_plan.md
Joseph Doherty 415e62c585 Add security classification, alarm detection, historical data access, and primitive grouping
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>
2026-03-26 11:32:33 -04:00

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
}
}
```