32f26272ae
Five tools under one repo, all docs organized per DOCS-GUIDE.md: - aalogcli: .NET 4.8 / x86 CliFx CLI for reading System Platform binary logs (*.aaLGX) for LLM debugging, built on aaOpenSource/aaLog. Commands: last, tail, range, unread, fields. Stable JSON envelope under --llm-json. Build template under lib/build/ for rebuilding aaLogReader.dll. - aot: ArchestrA Object Toolkit 2014 v4.0 reference material. Dev guide (Markdown converted from CHM), API reference for the ArchestrA.Toolkit namespace, and the Monitor / Watchdog VS sample solutions. - graccesscli: .NET 4.8 / x86 CliFx CLI that automates Galaxy configuration via the ArchestrA GRAccess COM interop. Includes session daemon, IPC protocol, and llm-json envelope contract. - grdb: SQL/DDL exploration of the Galaxy Repository database. DDL captures, reusable queries, hierarchy / contained-name <-> tag-name translation notes. - histdb: LLM-oriented reference for AVEVA Historian retrieval. INSQL linked-server, extension tables, every wwXxx time-domain extension, every retrieval mode, alarm/event SQL recipes, REST API. Distilled from the 243-page Historian Retrieval Guide. Root contains: - CLAUDE.md: thin index pointing into each tool's README. - DOCS-GUIDE.md: doctrine for organizing docs for LLM consumption. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
215 lines
8.4 KiB
Markdown
215 lines
8.4 KiB
Markdown
# Alarms and events SQL
|
|
|
|
The Historian's `Events` table holds records emitted by Application Server's alarm/event subsystem — set, acknowledged, cleared, plus user actions (writes, secured/verified writes). This is **not** the legacy "Classic Event subsystem" — that's a separate data path documented elsewhere in the source PDF.
|
|
|
|
## Time columns and `wwTimeZone`
|
|
|
|
Two ways `Events` expresses time:
|
|
|
|
- **`EventTimeUtc`** — UTC. Not affected by `wwTimeZone`.
|
|
- **`EventTime`** (and similar) — local time on the Historian server, projected through `wwTimeZone` if it's set.
|
|
|
|
Pick one and stick with it within a query, or you'll cross zones unexpectedly when DST flips.
|
|
|
|
## Schema essentials
|
|
|
|
The columns referenced in the recipes below — there are more, see PDF p. 175+:
|
|
|
|
| Column | Meaning |
|
|
| --- | --- |
|
|
| `EventTime`, `EventTimeUtc` | When the event occurred (local / UTC). |
|
|
| `ReceivedTime` | When the Historian received it. |
|
|
| `Type` | `Alarm.Set`, `Alarm.Acknowledge`, `Alarm.Clear`, `Alarm.Write`, `User.Write`, `User.Write.Secured`, `User.Write.Verified`, `Application.Write`, … |
|
|
| `Alarm_ID` | GUID identifying the alarm instance — same across `Set` / `Ack` / `Clear` rows. |
|
|
| `Alarm_State` | `UNACK_ALM`, `ACK_ALM`, `UNACK_RTN`, `ACK_RTN`. |
|
|
| `Alarm_DurationMs` | Total alarm life. |
|
|
| `Alarm_UnAckDurationMs` | Time spent unacknowledged. |
|
|
| `Severity` | 1 = Critical, 2 = Major, 3 = Minor, 4 = Informational. |
|
|
| `Priority` | 1-999, lower = higher priority. |
|
|
| `Source_Area`, `Source_Object`, `Source_ConditionVariable` | Origin of the alarm. |
|
|
| `User_Name` | Operator who performed an action (ack, write). |
|
|
| `IsAlarm` | `true` for alarm-related events, `false` for plain events. |
|
|
| `Namespace` | The event tag's namespace. |
|
|
|
|
Property values are **case-sensitive** — `"TRUE"`, `"True"`, and `"true"` are distinct strings as stored.
|
|
|
|
## Recipe 1 — list every event in a window
|
|
|
|
```sql
|
|
SELECT *
|
|
FROM Events
|
|
WHERE EventTime BETWEEN '2015-10-25 00:00' AND '2015-10-26 00:00';
|
|
```
|
|
|
|
Use this as a sanity probe — if it's empty, your time-zone or window assumptions are off.
|
|
|
|
## Recipe 2 — average alarm rate per hour
|
|
|
|
```sql
|
|
DECLARE @StartTime varchar(60) = '2015-10-25 12:00:00';
|
|
DECLARE @EndTime varchar(60) = '2015-10-26 12:00:00';
|
|
|
|
DECLARE @AlarmRaise TABLE (
|
|
EventTime nvarchar(60),
|
|
ID nvarchar(50),
|
|
AlarmState nvarchar(20),
|
|
SourceArea nvarchar(20),
|
|
SourceObject nvarchar(20)
|
|
);
|
|
INSERT @AlarmRaise
|
|
SELECT EventTime, Alarm_ID, Alarm_State, Source_Area, Source_Object
|
|
FROM Events
|
|
WHERE EventTime > @StartTime AND EventTime < @EndTime
|
|
AND Alarm_State = 'UNACK_ALM';
|
|
|
|
DECLARE @AlarmCounts TABLE (ForDate date, OnHour int, CountPerHour int);
|
|
INSERT @AlarmCounts
|
|
SELECT CAST(EventTime AS date), DATEPART(hour, EventTime), COUNT(*)
|
|
FROM @AlarmRaise
|
|
GROUP BY CAST(EventTime AS date), DATEPART(hour, EventTime);
|
|
|
|
SELECT AVG(CAST(CountPerHour AS INT)) AS [Average Alarm Rate on Hourly Basis]
|
|
FROM @AlarmCounts;
|
|
```
|
|
|
|
Pattern: stage matching alarm rows into a table variable, group, average. Filter on `Alarm_State = 'UNACK_ALM'` to count only fresh alarms (not re-emissions).
|
|
|
|
## Recipe 3 — most frequent alarms per hour
|
|
|
|
```sql
|
|
DECLARE @StartTime varchar(60) = '2017-11-10 12:00:00';
|
|
DECLARE @EndTime varchar(60) = '2017-11-10 12:10:00';
|
|
|
|
DECLARE @AlarmRaise TABLE (
|
|
EventTime nvarchar(60),
|
|
ID nvarchar(50),
|
|
AlarmState nvarchar(20),
|
|
SourceArea nvarchar(20),
|
|
SourceObject nvarchar(20),
|
|
SourceConditionVariable nvarchar(40)
|
|
);
|
|
INSERT @AlarmRaise
|
|
SELECT EventTime, Alarm_ID, Alarm_State,
|
|
Source_Area, Source_Object, Source_ConditionVariable
|
|
FROM Events
|
|
WHERE EventTime > @StartTime AND EventTime < @EndTime
|
|
AND Alarm_State = 'UNACK_ALM';
|
|
|
|
SELECT CAST(EventTime AS date) AS ForDate,
|
|
DATEPART(hour, EventTime) AS OnHour,
|
|
COUNT(*) AS [Count per Hour],
|
|
SourceObject + SourceConditionVariable AS [Alarm Attribute]
|
|
FROM @AlarmRaise
|
|
GROUP BY CAST(EventTime AS date),
|
|
DATEPART(hour, EventTime),
|
|
SourceObject, SourceConditionVariable
|
|
ORDER BY ForDate ASC, OnHour, [Alarm Attribute];
|
|
```
|
|
|
|
## Recipe 4 — alarms by area / object
|
|
|
|
```sql
|
|
SELECT Source_Area AS [Source Area/Object], COUNT(*) AS [Total]
|
|
FROM Events
|
|
WHERE EventTime > '2015-10-25 12:00:00'
|
|
AND EventTime < '2015-10-26 12:00:00'
|
|
AND Alarm_State = 'UNACK_ALM'
|
|
GROUP BY Source_Area
|
|
|
|
UNION ALL
|
|
|
|
SELECT Source_Object, COUNT(*)
|
|
FROM Events
|
|
WHERE EventTime > '2015-10-25 12:00:00'
|
|
AND EventTime < '2015-10-26 12:00:00'
|
|
AND Alarm_State = 'UNACK_ALM'
|
|
GROUP BY Source_Object;
|
|
```
|
|
|
|
Useful for "where in the plant are alarms coming from."
|
|
|
|
## Recipe 5 — average time-to-acknowledge per hour and severity
|
|
|
|
```sql
|
|
DECLARE @start datetime2 = '2017-12-11';
|
|
DECLARE @end datetime2 = '2017-12-12';
|
|
|
|
SELECT DATEADD(hour, DATEDIFF(hour, 0, e.EventTime), 0) AS hour,
|
|
e.Severity,
|
|
AVG(Alarm_UnAckDurationMs) AS avg_unack,
|
|
COUNT(*) AS count
|
|
FROM Events e
|
|
WHERE e.EventTime < @end
|
|
AND e.EventTime >= @start
|
|
AND e.Severity <= 3 -- 1=Critical, 2=High, 3=Medium, (4=Low)
|
|
AND e.Type = 'Alarm.Acknowledged'
|
|
GROUP BY DATEADD(hour, DATEDIFF(hour, 0, e.EventTime), 0), Severity
|
|
ORDER BY hour, Severity;
|
|
```
|
|
|
|
`Alarm_UnAckDurationMs` is set on the `Alarm.Acknowledged` row; that's why you filter on `Type = 'Alarm.Acknowledged'`.
|
|
|
|
To break it down by user instead:
|
|
|
|
```sql
|
|
SELECT DATEADD(hour, DATEDIFF(hour, 0, e.EventTime), 0) AS hour,
|
|
e.User_Name,
|
|
AVG(Alarm_UnAckDurationMs) AS avg_unack,
|
|
COUNT(*) AS count
|
|
FROM Events e
|
|
WHERE e.EventTime BETWEEN @start AND @end
|
|
AND e.Type = 'Alarm.Acknowledged'
|
|
GROUP BY DATEADD(hour, DATEDIFF(hour, 0, e.EventTime), 0), e.User_Name;
|
|
```
|
|
|
|
## Recipe 6 — alarm life cycle (raise → ack → clear)
|
|
|
|
```sql
|
|
DECLARE @StartTime varchar(60) = '2017-12-12 12:00:00';
|
|
DECLARE @EndTime varchar(60) = '2017-12-12 12:02:00';
|
|
|
|
DECLARE @AlarmRaise TABLE (EventTime nvarchar(60), ID nvarchar(50), AlarmState nvarchar(20));
|
|
INSERT @AlarmRaise
|
|
SELECT EventTime, Alarm_ID, Alarm_State FROM Events
|
|
WHERE EventTime > @StartTime AND EventTime < @EndTime
|
|
AND Alarm_State = 'UNACK_ALM';
|
|
|
|
DECLARE @AlarmAck TABLE (EventTime nvarchar(60), ID nvarchar(50), UnAckDuration nvarchar(20));
|
|
INSERT @AlarmAck
|
|
SELECT EventTime, Alarm_ID, Alarm_UnAckDurationMs FROM Events
|
|
WHERE EventTime > @StartTime AND EventTime < @EndTime
|
|
AND Alarm_Acknowledged = 1;
|
|
|
|
DECLARE @AlarmClear TABLE (EventTime nvarchar(60), ID nvarchar(50), AlarmDuration nvarchar(20));
|
|
INSERT @AlarmClear
|
|
SELECT EventTime, Alarm_ID, Alarm_DurationMs FROM Events
|
|
WHERE EventTime > @StartTime AND EventTime < @EndTime
|
|
AND Type = 'Alarm.Clear';
|
|
|
|
SELECT 'Alarm Life - ' + s.ID AS Tag,
|
|
CASE WHEN a.EventTime > c.EventTime THEN 'Cleared Before Ack'
|
|
WHEN a.EventTime < c.EventTime THEN 'Acked Before Clear'
|
|
ELSE '-' END AS Comment,
|
|
s.EventTime AS AlarmRaised,
|
|
a.EventTime AS AlarmAcked,
|
|
c.EventTime AS AlarmClear,
|
|
a.UnAckDuration AS UnAckDuration,
|
|
c.AlarmDuration AS AlarmDuration
|
|
FROM (@AlarmRaise s INNER JOIN @AlarmClear c ON c.ID = s.ID)
|
|
LEFT JOIN @AlarmAck a ON a.ID = c.ID AND a.EventTime <> c.EventTime
|
|
ORDER BY AlarmRaised ASC;
|
|
```
|
|
|
|
The `LEFT JOIN` keeps unacknowledged-but-cleared alarms in the report. The `Cleared Before Ack` / `Acked Before Clear` heuristic is useful for diagnosing alarm-flood vs. proper-handling patterns.
|
|
|
|
## Notes
|
|
|
|
- **Alarm IDs are GUIDs and persist across the lifecycle.** Joining `Set` / `Ack` / `Clear` rows by `Alarm_ID` is the only safe way to reconstruct an alarm timeline.
|
|
- **Severity values are inverted relative to priority** — 1 is highest severity, 4 is lowest. Don't confuse with `Priority` (1 = highest priority, 999 = lowest).
|
|
- **`Alarm_State`** uses two-character suffixes: `_ALM` for active, `_RTN` for return-to-normal. Combine with `UNACK_` / `ACK_` for the four states.
|
|
- The PDF chapter dedicates pp. 175-181 to these patterns. For full property catalog see PDF pp. 202-207.
|
|
|
|
## Next
|
|
|
|
- [`07-rest-api.md`](07-rest-api.md) — same data over HTTP.
|