Files
ScadaBridge/docs/former-api-specs/mes/Alarm-API.md
T
Joseph Doherty fad833e8d1 docs(former-api-specs): legacy MES alarm API — code-verified behaviour notes
Checked against the legacy mesapi source (MesNotifier/MesServices/Tag) plus ServiceStack/OrmLite
6.0.2 in a scratch harness:
- unhandled exceptions are an HTTP 500 ServiceStack ErrorResponse, not WasSuccessful=false
- no matching alarms = success with an empty list; failures don't short-circuit
- AckDT is null only for a null MXAccess value, not 'when unacked'
- tag type-conversion failures are silent (InAlarm false -> alarm drops out)
- simplealarmstatus SAPID edge cases: null never matches, "" matches, duplicates -> arbitrary TOP 1
- 'test' API keys hit the production BatchDB (TestDb routing never reaches MesNotifier)
- latent SetResult-after-timeout race in the MXAccess data-change callback
2026-09-28 09:25:10 -04:00

14 KiB
Raw Blame History

WWSupport MES API — Alarm Status API

Reference for the alarm-status endpoints exposed by the WWSupport / APIServer ServiceStack service. The API reads live machine-alarm state out of AVEVA Wonderware (System Platform / Galaxy) via MXAccess and returns it to MES callers.

  • Service host: AppHost (AppSelfHostBase, name "APIServer") — APIServer/APIServer/AppHost.cs
  • Service implementation: MesServices — APIServer/APIServer.ServiceInterface/MesServices.cs
  • Business logic: MesNotifier — APIServer/APIServer.ServiceInterface/MesNotifier.cs
  • Framework: ServiceStack 6.0.2, self-hosted; SQL Server (OrmLite, SqlServer2016Dialect)

Serialization note: JsConfig.IncludeNullValues = true, so null fields are emitted in JSON responses (e.g. "AckDT": null).


Endpoints

Verb Route Request DTO Response DTO
POST /mes/alarmstatus AlarmStatusRequest AlarmStatusResponse
POST /mes/simplealarmstatus SimpleAlarmStatusRequest AlarmStatusResponse

Both endpoints return the same AlarmStatusResponse shape. Both are dispatched through ServiceStack Any(...) handlers in MesServices, which resolve the singleton MesNotifier and call AlarmStatus(...) / SimpleAlarmStatus(...).

The service also enables PostmanFeature and OpenApiFeature (Swagger), so a running instance exposes a browsable contract and a Postman collection.


Authentication & authorization

All MES services are decorated with:

[Authenticate]
[RequiredRole("MESAPI")]
public class MesServices : Service { ... }

The caller must be authenticated and hold the MESAPI role. Auth is configured in AppHost.Configure:

  • API key — ApiKeyAuthProvider
    • SessionCacheDuration = 30 minutes
    • RequireSecureConnection = false (HTTP is accepted; TLS not enforced)
    • AppHost.GetDbConnection routes an API key whose Environment == "test" to a TestDb connection — but this does NOT apply to these endpoints (see Behavior notes: test keys query the production BatchDB).
  • LDAP — LdapAuthProvider (directory credentials).
  • AllowGetAuthenticateRequests = true.

User/role data is persisted with OrmLiteAuthRepository (UseDistinctRoleTables = true) in the same SQL Server database.


POST /mes/simplealarmstatus

The convenience endpoint: identify a machine by SAPID, get back its MES-relevant alarms.

Request — SimpleAlarmStatusRequest

Field Type Notes
SAPID string SAP identifier of the machine. Required.
{ "SAPID": "100012345" }

Behavior

  1. Look up the Machine by SAPID. If not found → WasSuccessful = false, ErrorText = "Failed to find machine with SAPID '<id>'".
  2. Select that machine's alarms filtered to FlaggedForMES = true only.
  3. Read live tag state and return every alarm that is currently triggered (InAlarm == true).

Notes specific to this endpoint:

  • Always flagged-only (cannot return non-MES alarms).
  • Does not honor an "include acked" toggle — acked alarms are always included (with StatusCode = "Triggered.Acked").

POST /mes/alarmstatus

The full endpoint: pick the machine by any one of several keys, and filter the alarm set.

Request — AlarmStatusRequest

Field Type Notes
MachineFilter MachineFilter Selects the machine (see below). Defaults to empty.
AlarmFilter AlarmFilter Filters the alarms (see below). Defaults to "all flagged + unflagged, triggered + acked".

MachineFilter

Field Type Notes
MachineID int? DB primary key.
SAPID string SAP identifier.
ZTag string Z-tag identifier.
Code string Machine code (also the MXAccess tag prefix).

Resolution precedence (first non-empty wins): SAPID → Code → ZTag → MachineID. At least one selector must be supplied; if all are empty/unmatched the call fails with "Failed to find machine with given machine filter '<dump>'". A supplied-but-unmatched selector fails with a selector-specific message, e.g. "Failed to find machine with Code '<code>'".

AlarmFilter

Field Type Default Effect
NameFilter string null Case-insensitive substring match on alarm Name.
MinSeverity int? null Keep alarms with Severity >= MinSeverity.
MaxSeverity int? null Keep alarms with Severity <= MaxSeverity.
IncludeTriggered bool true Currently unused — see Behavior notes.
IncludeAcked bool true When false, acked alarms are excluded from the result.
FlaggedOnly bool false When true, restrict to FlaggedForMES = true alarms.
{
  "MachineFilter": { "SAPID": "100012345" },
  "AlarmFilter": {
    "MinSeverity": 500,
    "FlaggedOnly": true,
    "IncludeAcked": false
  }
}

Behavior

  1. Resolve the machine via MachineFilter precedence (above).
  2. Load all MachineAlarm rows for that machine, then apply the in-process AlarmFilter in this order: FlaggedOnly → MinSeverity → MaxSeverity → NameFilter.
  3. Read live tag state; return every remaining alarm that is triggered (InAlarm == true), skipping acked alarms when IncludeAcked == false.

Response — AlarmStatusResponse

Field Type Notes
WasSuccessful bool false on any lookup or tag-read failure.
ErrorText string Populated when WasSuccessful == false.
Alarms AlarmInfo[] Triggered alarms matching the request. Cleared if WasSuccessful == false.

AlarmInfo

Field Type Source
Name string MachineAlarm.Name.
HierarchicalName string "{Machine.Code}.{Name}".
Description string Live …​.DescAttrName tag value.
IsFlaggedForMES bool MachineAlarm.FlaggedForMES.
Severity int MachineAlarm.Severity (0–999).
StatusCode string "Triggered", or "Triggered.Acked" when acked.
TriggeredDT DateTime Live …​.TimeAlarmOn tag value.
AckDT DateTime? Live …​.TimeAlarmAcked tag value — read for every triggered alarm, acked or not; null only if MXAccess delivers a null value (see Behavior notes).
AckComment string Live …​.AckMsg tag value.

Example response

{
  "WasSuccessful": true,
  "ErrorText": null,
  "Alarms": [
    {
      "Name": "HighVacuumFault",
      "HierarchicalName": "Z28061.HighVacuumFault",
      "Description": "High vacuum sensor out of range",
      "IsFlaggedForMES": true,
      "Severity": 800,
      "StatusCode": "Triggered.Acked",
      "TriggeredDT": "2026-06-25T01:14:22",
      "AckDT": "2026-06-25T01:16:05",
      "AckComment": "Investigating - day shift"
    }
  ]
}

How alarm state is read (MXAccess)

Alarm configuration (name, severity, MES flag) lives in SQL; live state is read from Wonderware at request time:

  • For each MachineAlarm, an AlarmTagset (AlarmTagset.cs) builds MXAccess tag paths of the form {Machine.Code}.{Alarm.Name}.<suffix>, where <suffix> is one of: Quality, InAlarm, TimeAlarmOn, DescAttrName, Acked, TimeAlarmAcked, AckMsg.
  • MesNotifier subscribes via LMXProxyServerClass.AdviseSupervisory (handle registered as "MesNotifier"), waits for the first data change per tag, then unsubscribes.
  • Quality gate: every alarm's Quality must be OPC-Good (192); otherwise the whole request fails with ErrorText = "Failed to read machine alarm status" and Alarms is cleared.
  • Detail tags (TimeAlarmOn, DescAttrName, Acked, TimeAlarmAcked, AckMsg) are only read for alarms whose InAlarm is true.
  • Per-request timeout: 30 seconds (CancellationTokenSource(30000)); on timeout the pending reads resolve as failed.

Underlying data model — MachineAlarm

SQL table backing alarm configuration (APIServer.ServiceModel/DTO/MachineAlarm.cs):

Column Type Constraints
MachineAlarmID int PK, auto-increment
MachineID int FK → Machine
Machine Machine OrmLite [Reference]
Name string Required, ≤ 256
Severity int 0–999
FlaggedForMES bool Marks an alarm as MES-relevant
LastUpdate DateTime
LastUpdateBy string Required, ≤ 128
OtherData string Max-length text

Behavior notes & gotchas

  • AlarmFilter.IncludeTriggered is declared but never used. Both endpoints only ever return alarms whose InAlarm tag is true; setting IncludeTriggered = false has no effect.
  • Only triggered alarms are returned. There is no "all configured alarms" mode — an alarm not currently in alarm state never appears, regardless of filters.
  • /simplealarmstatus is flagged-only and ignores ack filtering; use /mes/alarmstatus with FlaggedOnly / IncludeAcked for control over those.
  • MachineFilter precedence matters — supplying both SAPID and Code uses SAPID; the others are ignored.
  • All-or-nothing on read errors. A single bad-quality tag fails the whole response (success flag flips, alarm list is emptied) rather than returning a partial set.
  • Severity bounds are inclusive on both MinSeverity and MaxSeverity.
  • Unhandled exceptions are an HTTP 500, not WasSuccessful = false. Neither MesServices handler catches anything (they block on .Result) and AppHost registers no exception handlers. A SQL error, an exception from the MXAccess COM calls (AddItem / AdviseSupervisory) or cancellation while opening the DB connection returns ServiceStack's error body (response DTO has no ResponseStatus, so the generic ErrorResponse is used; ErrorCode = the inner exception type name, Message = its raw text, StackTrace null outside Debug builds):
    HTTP 500
    {"ResponseStatus":{"ErrorCode":"SqlException","Message":"<raw exception text>","StackTrace":null,"Errors":[]}}
    
    A caller without Accept: application/json gets ServiceStack's HTML error page instead. The error is also written to requestlogs/{year}-{month}/{…}-errors.csv. (Behaviour reproduced on ServiceStack 6.0.2 with the same handler shape, 2026-09-28.) The 30-second read timeout does not throw — pending reads resolve as failed and yield the normal WasSuccessful = false body.
  • No matching alarms = success with an empty list. A machine with no MachineAlarm rows (or, on /simplealarmstatus, no FlaggedForMES rows) returns WasSuccessful = true, Alarms: [] — indistinguishable from "configured alarms, none in alarm".
  • Failures don't short-circuit. After a quality/read failure the handler still reads the detail tags and builds the list, then clears it before returning — cost only (time + MXAccess load).
  • AckDT is not guaranteed null for an unacked alarm. Tag<T>.ParseUpdate yields null only for a null MXAccess value; otherwise it is whatever TimeAlarmAcked holds. A Galaxy time attribute normally holds a date, so expect a stale/default timestamp rather than null (unverified against a live Galaxy). Use StatusCode to decide acked vs. unacked.
  • Tag type-conversion failures are silent. If a value fails Convert.ChangeType, ParseUpdate sets the tag to default(T) and IsGood = false, but the read still reports success (it keys on MXAccess quality only) and only the Quality tag's IsGood is checked. So a bad InAlarm reads as false and the alarm silently drops out; a bad TimeAlarmOn becomes 0001-01-01T00:00:00; a bad DescAttrName/AckMsg becomes null.
  • /simplealarmstatus machine lookup edge cases (SQL is SELECT TOP 1 … WHERE SAPID = @0, confirmed on OrmLite 6.0.2; the column is nullable and not unique):
    • missing / null SAPID → parameter is NULL, = NULL never matches, so machines with a NULL SAPID are not returned → "Failed to find machine with SAPID ''";
    • "SAPID": "" does match a machine whose SAPID is the empty string (the full endpoint guards with IsNullOrEmpty and never issues this query);
    • duplicate SAPIDs → TOP 1 with no ORDER BY, so SQL Server picks an arbitrary machine. The same applies to SAPID/Code/ZTag lookups on /mes/alarmstatus.
  • "test" API keys hit the production database. AppHost.GetDbConnection's TestDb routing only affects a service's Db property; MesNotifier opens its own connection from the registered OrmLiteConnectionFactory (MesNotifier.cs lines 71/168/347/525), which is always BatchDB. No TestDb named connection is registered either. Applies to MoveIn/MoveOut too.
  • Late-arriving value after timeout (latent). LmxProxyOnOnDataChange completes a pending read with SetResult, not TrySetResult; a value that lands after the 30 s timeout already resolved the read (before its ContinueWith removes it) would throw inside the MXAccess COM callback. Not reproduced.
  • The route attributes carry placeholder Swagger text (Summary = "POST Summary", Notes = "Notes"); these are cosmetic.

Quick reference (curl)

# Simple — flagged alarms for one machine by SAPID
curl -X POST http://<host>:<port>/mes/simplealarmstatus \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <api-key>" \
  -d '{"SAPID":"100012345"}'

# Full — filtered alarms, machine chosen by Code
curl -X POST http://<host>:<port>/mes/alarmstatus \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <api-key>" \
  -d '{
        "MachineFilter": { "Code": "Z28061" },
        "AlarmFilter": { "MinSeverity": 500, "IncludeAcked": false }
      }'

Exact auth header format depends on how ApiKeyAuthProvider is configured for the deployment (bearer token vs. HTTP Basic with the key as username). Confirm against the live Swagger/Postman metadata for the target server.