Wire Galaxy security_classification to OPC UA AccessLevel (ReadOnly for SecuredWrite/VerifiedWrite/ViewOnly). Use deployed package chain for attribute queries to exclude undeployed attributes. Group primitive attributes under their parent variable node (merged Variable+Object). Add is_historized and is_alarm detection via HistoryExtension/AlarmExtension primitives. Implement OPC UA HistoryRead backed by Wonderware Historian Runtime database. Implement AlarmConditionState nodes driven by InAlarm with condition refresh support. Add historyread and alarms CLI commands for testing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
13 KiB
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_namematches thedynamic_attribute.attribute_nameprimitive_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:
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 nodesTwoStateVariableType— for ActiveState, AckedState, EnabledState, ShelvedStateShelvedStateMachineType— for shelving state management
Key Implementation Steps
- Detect alarms in the query — add
is_alarmcolumn to attributes queries (same pattern asis_historized) - Create alarm condition nodes — for attributes where
is_alarm = 1, create anAlarmConditionStateinstead of a plainBaseDataVariableState - Map properties — subscribe to the Galaxy alarm sub-attributes (InAlarm, Acked, Priority, etc.) and update the OPC UA alarm state
- Event notifications — when alarm state changes arrive via MXAccess
OnDataChange, raise OPC UA alarm events viaReportEvent() - Condition refresh — implement
ConditionRefresh()to send current alarm states to newly subscribing clients - Acknowledge method — implement the OPC UA
Acknowledgemethod 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— addis_alarmcolumngr/queries/attributes.sql— addis_alarmcolumnsrc/.../Domain/GalaxyAttributeInfo.cs— addIsAlarmpropertysrc/.../GalaxyRepository/GalaxyRepositoryService.cs— readis_alarmfrom query resultssrc/.../OpcUa/LmxNodeManager.cs— createAlarmConditionStatenodes 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 commandtools/opcuacli-dotnet/README.md— addalarmscommand 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
# 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:
// 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:
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
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:
if (refresh)
{
await subscription.ConditionRefreshAsync();
await console.Output.WriteLineAsync("Condition refresh requested.");
}
State Formatting
static string FormatAlarmState(bool? active, bool? acked)
{
var activePart = active == true ? "Active" : "Inactive";
var ackedPart = acked == true ? "Acknowledged" : "Unacknowledged";
return $"{activePart}, {ackedPart}";
}