Files
wwtools/histdb/06-alarms-events.md
T
Joseph Doherty 32f26272ae Initial commit: Wonderware / System Platform tools and reference
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>
2026-05-03 18:22:20 -04:00

8.4 KiB

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

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

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

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

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

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:

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)

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