# Alarm & History Detection Plan ## Context Galaxy attributes can be alarms (with `AlarmExtension` primitives) or historized (with `HistoryExtension` primitives). This plan documents how to detect these in the Galaxy Repository database and maps Galaxy alarm properties to OPC UA Alarms & Conditions concepts. ## 1. Detection in the Galaxy Repository ### Alarm Detection An attribute is an alarm when a `primitive_instance` exists in the deployed package chain where: - `primitive_instance.primitive_name` matches the `dynamic_attribute.attribute_name` - `primitive_definition.primitive_name = 'AlarmExtension'` Example: `TestMachine_001.TestAlarm001` has a `primitive_instance` named `TestAlarm001` with `primitive_definition.primitive_name = 'AlarmExtension'`. ### History Detection Already implemented in the attributes queries. Same pattern but checking for `primitive_definition.primitive_name = 'HistoryExtension'`. ### Query Pattern Both use the same EXISTS subquery against the deployed package chain: ```sql CASE WHEN EXISTS ( SELECT 1 FROM deployed_package_chain dpc2 INNER JOIN primitive_instance pi ON pi.package_id = dpc2.package_id AND pi.primitive_name = da.attribute_name INNER JOIN primitive_definition pd ON pd.primitive_definition_id = pi.primitive_definition_id AND pd.primitive_name = 'AlarmExtension' -- or 'HistoryExtension' WHERE dpc2.gobject_id = dpc.gobject_id ) THEN 1 ELSE 0 END AS is_alarm ``` ## 2. Galaxy Alarm Properties (AlarmExtension) The `AlarmExtension` primitive exposes 24 public attributes per alarm. These are already returned by the extended attributes query as primitive child attributes (e.g., `TestMachine_001.TestAlarm001.Acked`). ### Key Properties and Runtime Values | Galaxy Attribute | Data Type | Runtime Example | Description | |---|---|---|---| | **InAlarm** | Boolean | `False` | Whether alarm condition is currently active | | **Acked** | Boolean | `False` | Whether alarm has been acknowledged | | **Condition** | Boolean | `False` | Raw condition value (input to alarm logic) | | **ActiveAlarmState** | Boolean | `True` | Active state of alarm processing | | **Priority** | Integer | `500` | Alarm priority (1-999, higher = more urgent) | | **Category** | Enum | `1` (Discrete) | Alarm category type | | **AlarmMode** | Enum | `1` (Enable) | Operational mode: 1=Enable, 2=Disable, 3=Silence | | **AckMsg** | String | `""` | Acknowledgment message/comment | | **TimeAlarmOn** | DateTime | | When alarm condition triggered | | **TimeAlarmOff** | DateTime | | When alarm condition cleared | | **TimeAlarmAcked** | DateTime | | When alarm was acknowledged | | **AlarmInhibit** | Boolean | | Inhibit alarm processing | | **AlarmShelved** | Boolean | `False` | Whether alarm is shelved | | **AlarmShelveNode** | String | | Node that shelved the alarm | | **AlarmShelveReason** | String | | Reason for shelving | | **AlarmShelveUser** | String | | User who shelved | | **AlarmShelveStartTime** | DateTime | | When shelve started | | **AlarmShelveStopTime** | DateTime | | When shelve ends | | **AlarmShelveCmd** | String | | Shelving command | | **AlarmModeCmd** | Enum | | Command to change alarm mode | | **AlarmSourceAttr** | Reference | | Source attribute reference | | **DescAttrName** | String | | Descriptive attribute name | | **Alarm.TimeDeadband** | ElapsedTime | | Time deadband for alarm | ### Alarm Enum Values **AlarmMode**: Enable (1), Disable (2), Silence (3) **Category**: Discrete (1), Value LoLo, Value Lo, Value Hi, Value HiHi, ROC, Deviation (and more) ## 3. Mapping Galaxy Alarm Properties to OPC UA ### OPC UA Alarm Type Hierarchy ``` ConditionType └─ AcknowledgeableConditionType └─ AlarmConditionType ├─ DiscreteAlarmType ← for Galaxy boolean alarms (Category=Discrete) ├─ OffNormalAlarmType ← alternative for boolean alarms └─ LimitAlarmType ← for analog alarms (Hi/Lo/HiHi/LoLo) ├─ ExclusiveLimitAlarmType └─ NonExclusiveLimitAlarmType ``` Galaxy boolean alarms (like TestAlarm001/002/003) map to **DiscreteAlarmType** or **OffNormalAlarmType**. ### Property Mapping | Galaxy Property | OPC UA Alarm Property | Notes | |---|---|---| | `InAlarm` | `ActiveState.Id` | Boolean: alarm is active | | `Acked` | `AckedState.Id` | Boolean: alarm acknowledged | | `Priority` | `Severity` | Galaxy 1-999 maps to OPC UA 1-1000 | | `AckMsg` | `Comment` | Acknowledgment message | | `Condition` | Source variable value | The boolean condition input | | `AlarmMode` (Enable/Disable) | `EnabledState.Id` | Enable=true, Disable/Silence=false | | `ActiveAlarmState` | `Retain` | Whether condition should be retained | | `TimeAlarmOn` | `ActiveState.TransitionTime` | When alarm became active | | `TimeAlarmOff` | `ActiveState.TransitionTime` | When alarm became inactive | | `TimeAlarmAcked` | `AckedState.TransitionTime` | When alarm was acknowledged | | `AlarmShelved` | `ShelvedState` (current state) | Maps to Unshelved/OneShotShelved/TimedShelved | | `AlarmShelveStartTime` | `ShelvingState.UnshelveTime` | Computed from start/stop times | | `Category` | `ConditionClassId` | Identifies condition class | | `AlarmInhibit` | `SuppressedState.Id` | Alarm suppression | | `DescAttrName` | `Message` | Description/message for alarm | ### Properties Not Available in Galaxy These OPC UA properties have no direct Galaxy equivalent and would use defaults: - `ConfirmedState` — Galaxy doesn't have a confirmed concept (default: true) - `BranchId` — Galaxy doesn't support branching (default: null) - `Quality` — Use the source variable's StatusCode ## 4. Implementation Approach ### OPC UA SDK Classes - `AlarmConditionState` — main class for alarm nodes - `TwoStateVariableType` — for ActiveState, AckedState, EnabledState, ShelvedState - `ShelvedStateMachineType` — for shelving state management ### Key Implementation Steps 1. **Detect alarms in the query** — add `is_alarm` column to attributes queries (same pattern as `is_historized`) 2. **Create alarm condition nodes** — for attributes where `is_alarm = 1`, create an `AlarmConditionState` instead of a plain `BaseDataVariableState` 3. **Map properties** — subscribe to the Galaxy alarm sub-attributes (InAlarm, Acked, Priority, etc.) and update the OPC UA alarm state 4. **Event notifications** — when alarm state changes arrive via MXAccess `OnDataChange`, raise OPC UA alarm events via `ReportEvent()` 5. **Condition refresh** — implement `ConditionRefresh()` to send current alarm states to newly subscribing clients 6. **Acknowledge method** — implement the OPC UA `Acknowledge` method to write back to Galaxy via MXAccess ### Galaxy Alarm Types in the Database 51 alarm-related primitive definitions exist. The main ones relevant to OPC UA mapping: | Galaxy Primitive | OPC UA Alarm Type | |---|---| | `AlarmExtension` (Boolean) | `DiscreteAlarmType` / `OffNormalAlarmType` | | `AnalogExtension.LevelAlarms.Hi/HiHi/Lo/LoLo` | `ExclusiveLimitAlarmType` or `NonExclusiveLimitAlarmType` | | `AnalogExtension.ROCAlarms` | `RateOfChangeAlarmType` | | `AnalogExtension.DeviationAlarms` | `DeviationAlarmType` | ### Files to Modify - `gr/queries/attributes_extended.sql` — add `is_alarm` column - `gr/queries/attributes.sql` — add `is_alarm` column - `src/.../Domain/GalaxyAttributeInfo.cs` — add `IsAlarm` property - `src/.../GalaxyRepository/GalaxyRepositoryService.cs` — read `is_alarm` from query results - `src/.../OpcUa/LmxNodeManager.cs` — create `AlarmConditionState` nodes for alarm attributes - New: alarm state update handler mapping MXAccess data changes to OPC UA alarm events - `tools/opcuacli-dotnet/Commands/AlarmsCommand.cs` — NEW CLI command - `tools/opcuacli-dotnet/README.md` — add `alarms` command documentation ## 5. OPC UA CLI Tool — Alarms Command Add an `alarms` command to `tools/opcuacli-dotnet/` for subscribing to and displaying OPC UA alarm events. ### Usage ```bash # Subscribe to all alarm events under a node (e.g., TestMachine_001) dotnet run -- alarms -u opc.tcp://localhost:4840/LmxOpcUa -n "ns=1;s=TestMachine_001" # Subscribe to all events under the root ZB node dotnet run -- alarms -u opc.tcp://localhost:4840/LmxOpcUa -n "ns=1;s=ZB" # Subscribe to all server events (Server node) dotnet run -- alarms -u opc.tcp://localhost:4840/LmxOpcUa # Request a condition refresh to get current alarm states immediately dotnet run -- alarms -u opc.tcp://localhost:4840/LmxOpcUa -n "ns=1;s=TestMachine_001" --refresh ``` ### Command Options | Flag | Description | |------|-------------| | `-u, --url` | OPC UA server endpoint URL (required) | | `-n, --node` | Node ID to monitor for events (default: Server node i=2253) | | `--refresh` | Request a ConditionRefresh after subscribing to get current states | | `-i, --interval` | Publishing interval in milliseconds (default: 1000) | ### Output Format ``` Subscribed to alarm events on ns=1;s=TestMachine_001 (interval: 1000ms). Press Ctrl+C to stop. [2026-03-26T04:30:12.000Z] ALARM TestMachine_001.TestAlarm001 State: Active, Unacknowledged Severity: 500 Message: Discrete alarm triggered Source: ns=1;s=TestMachine_001.TestAlarm001 Retain: True [2026-03-26T04:30:45.000Z] ALARM TestMachine_001.TestAlarm001 State: Active, Acknowledged Severity: 500 Message: Discrete alarm triggered AckUser: operator1 [2026-03-26T04:31:02.000Z] ALARM TestMachine_001.TestAlarm001 State: Inactive, Acknowledged Severity: 500 Retain: False ``` ### Implementation New file: `tools/opcuacli-dotnet/Commands/AlarmsCommand.cs` OPC UA alarm events are received through event-type monitored items, not regular data-change subscriptions. The key differences from the `subscribe` command: ```csharp // Create an event monitored item (not a data-change item) var item = new MonitoredItem(subscription.DefaultItem) { StartNodeId = nodeId, DisplayName = "AlarmMonitor", SamplingInterval = interval, NodeClass = NodeClass.Object, // Subscribe to events, not data changes AttributeId = Attributes.EventNotifier, // Select which event fields to return Filter = CreateEventFilter() }; ``` #### Event Filter Select the standard alarm fields to display: ```csharp private static EventFilter CreateEventFilter() { var filter = new EventFilter(); filter.AddSelectClause(ObjectTypeIds.BaseEventType, "EventId"); filter.AddSelectClause(ObjectTypeIds.BaseEventType, "EventType"); filter.AddSelectClause(ObjectTypeIds.BaseEventType, "SourceName"); filter.AddSelectClause(ObjectTypeIds.BaseEventType, "Time"); filter.AddSelectClause(ObjectTypeIds.BaseEventType, "Message"); filter.AddSelectClause(ObjectTypeIds.BaseEventType, "Severity"); filter.AddSelectClause(ObjectTypeIds.ConditionType, "ConditionName"); filter.AddSelectClause(ObjectTypeIds.ConditionType, "Retain"); filter.AddSelectClause(ObjectTypeIds.AcknowledgeableConditionType, "AckedState/Id"); filter.AddSelectClause(ObjectTypeIds.AlarmConditionType, "ActiveState/Id"); filter.AddSelectClause(ObjectTypeIds.AlarmConditionType, "EnabledState/Id"); filter.AddSelectClause(ObjectTypeIds.AlarmConditionType, "SuppressedOrShelved"); return filter; } ``` #### Event Notification Handler ```csharp item.Notification += (monitoredItem, e) => { if (e.NotificationValue is EventFieldList eventFields) { var time = eventFields.EventFields[3].Value as DateTime?; var sourceName = eventFields.EventFields[2].Value as string; var message = (eventFields.EventFields[4].Value as LocalizedText)?.Text; var severity = eventFields.EventFields[5].Value as ushort?; var ackedState = eventFields.EventFields[8].Value as bool?; var activeState = eventFields.EventFields[9].Value as bool?; var retain = eventFields.EventFields[7].Value as bool?; var stateDesc = FormatAlarmState(activeState, ackedState); Console.WriteLine($"[{time:O}] ALARM {sourceName}"); Console.WriteLine($" State: {stateDesc}"); Console.WriteLine($" Severity: {severity}"); if (!string.IsNullOrEmpty(message)) Console.WriteLine($" Message: {message}"); Console.WriteLine($" Retain: {retain}"); Console.WriteLine(); } }; ``` #### Condition Refresh When `--refresh` is specified, call `ConditionRefresh` after creating the subscription to receive the current state of all active alarms: ```csharp if (refresh) { await subscription.ConditionRefreshAsync(); await console.Output.WriteLineAsync("Condition refresh requested."); } ``` #### State Formatting ```csharp static string FormatAlarmState(bool? active, bool? acked) { var activePart = active == true ? "Active" : "Inactive"; var ackedPart = acked == true ? "Acknowledged" : "Unacknowledged"; return $"{activePart}, {ackedPart}"; } ```