# 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: ```csharp [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) - An API key whose `Environment == "test"` is routed to the `TestDb` connection instead of the production DB (`AppHost.GetDbConnection`). - **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. | ```json { "SAPID": "100012345" } ``` ### Behavior 1. Look up the `Machine` by `SAPID`. If not found → `WasSuccessful = false`, `ErrorText = "Failed to find machine with SAPID ''"`. 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 ''"`. A supplied-but-unmatched selector fails with a selector-specific message, e.g. `"Failed to find machine with 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. | ```json { "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 (null if unacked). | | `AckComment` | string | Live `…​.AckMsg` tag value. | ### Example response ```json { "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}.`, where `` 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`. - The route attributes carry placeholder Swagger text (`Summary = "POST Summary"`, `Notes = "Notes"`); these are cosmetic. --- ## Quick reference (curl) ```bash # Simple — flagged alarms for one machine by SAPID curl -X POST http://:/mes/simplealarmstatus \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{"SAPID":"100012345"}' # Full — filtered alarms, machine chosen by Code curl -X POST http://:/mes/alarmstatus \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -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.