Monitoring an OPC UA Subtree
This tutorial shows you how to automatically monitor an entire branch of the OPC UA address space by combining the Explore node with the Monitor node. This is ideal for monitoring complete equipment hierarchies without manually specifying each variable.
The Explore node discovers all variables in a subtree, and the Monitor node automatically subscribes to all of them, preserving the hierarchical structure.
Prerequisites
- An active connection to an OPC UA server (see Create a connection)
- A subscription configured in the connection endpoint
- Knowledge of the root NodeId where the subtree starts
How It Works
- Explore Node traverses the OPC UA address space starting from a root node
- Explore Node outputs a JSON structure with all discovered variable NodeIds
- Monitor Node receives this structure and monitors all variables
- Monitor Node outputs values in the same hierarchical structure
Basic Setup
Step 1: Configure Explore Node
{
"id": "explore1",
"type": "OpcUa-Explore",
"name": "Explore Equipment",
"endpoint": "opcua_endpoint1",
"nodeId": "ns=2;s=Equipment.Reactor1",
"outputType": "NodeID"
}
Key Settings:
- NodeId: Root of the subtree to explore
- Output Type: Must be set to
NodeID
Step 2: Configure Monitor Node
{
"id": "monitor1",
"type": "OpcUa-Monitor",
"name": "Monitor Equipment",
"endpoint": "opcua_endpoint1",
"subscription": "subscription1",
"startImmediately": false,
"nodeId": "",
"samplingInterval": 1000
}
Key Settings:
- Start Immediately: Must be unchecked
- NodeId: Must be empty
- Monitoring parameters apply to all discovered variables
Step 3: Connect the Nodes
[
{
"id": "inject1",
"type": "inject",
"name": "Trigger Explore",
"once": true
},
{
"id": "explore1",
"type": "OpcUa-Explore"
},
{
"id": "monitor1",
"type": "OpcUa-Monitor"
},
{
"id": "debug1",
"type": "debug",
"name": "Show Structure"
}
]
Flow:
- Inject triggers exploration
- Explore discovers subtree and outputs NodeIds
- Monitor receives NodeIds and starts monitoring
- Monitor outputs value changes
Example: Monitor Complete Reactor
OPC UA Address Space:
Equipment.Reactor1
├── Process
│ ├── Temperature (ns=2;s=Equipment.Reactor1.Process.Temperature)
│ ├── Pressure (ns=2;s=Equipment.Reactor1.Process.Pressure)
│ └── pH (ns=2;s=Equipment.Reactor1.Process.pH)
├── Cooling
│ ├── InletTemp (ns=2;s=Equipment.Reactor1.Cooling.InletTemp)
│ ├── OutletTemp (ns=2;s=Equipment.Reactor1.Cooling.OutletTemp)
│ └── FlowRate (ns=2;s=Equipment.Reactor1.Cooling.FlowRate)
└── Status
├── Running (ns=2;s=Equipment.Reactor1.Status.Running)
└── Mode (ns=2;s=Equipment.Reactor1.Status.Mode)
Explore Output (NodeIds):
{
"Process": {
"Temperature": "ns=2;s=Equipment.Reactor1.Process.Temperature",
"Pressure": "ns=2;s=Equipment.Reactor1.Process.Pressure",
"pH": "ns=2;s=Equipment.Reactor1.Process.pH"
},
"Cooling": {
"InletTemp": "ns=2;s=Equipment.Reactor1.Cooling.InletTemp",
"OutletTemp": "ns=2;s=Equipment.Reactor1.Cooling.OutletTemp",
"FlowRate": "ns=2;s=Equipment.Reactor1.Cooling.FlowRate"
},
"Status": {
"Running": "ns=2;s=Equipment.Reactor1.Status.Running",
"Mode": "ns=2;s=Equipment.Reactor1.Status.Mode"
}
}
Monitor Output (Values):
{
"Process": {
"Temperature": 85.3,
"Pressure": 2.5,
"pH": 7.2
},
"Cooling": {
"InletTemp": 15.0,
"OutletTemp": 22.5,
"FlowRate": 120.0
},
"Status": {
"Running": true,
"Mode": "Auto"
}
}
Example: Monitor Multiple Equipment
Monitor several reactors by exploring each one:
// Function node: Explore multiple reactors
const reactors = ["Reactor1", "Reactor2", "Reactor3"];
const messages = [];
reactors.forEach(reactor => {
messages.push({
topic: "explore",
payload: {
nodeId: `ns=2;s=Equipment.${reactor}`
}
});
});
return [messages];
Alternatively, explore a parent folder containing all reactors:
// Explore "Equipment" folder containing all reactors
msg.payload = {
nodeId: "ns=2;s=Equipment"
};
return msg;
Filtering Variables
Use a function node to filter which variables to monitor:
Example: Only Monitor Temperature Variables
// Function node: Filter only temperature variables
function filterTemperatures(obj, result = {}) {
for (const [key, value] of Object.entries(obj)) {
if (typeof value === 'string') {
// Leaf node with NodeId
if (key.toLowerCase().includes('temp')) {
result[key] = value;
}
} else if (typeof value === 'object') {
// Nested object
const filtered = filterTemperatures(value);
if (Object.keys(filtered).length > 0) {
result[key] = filtered;
}
}
}
return result;
}
msg.payload = filterTemperatures(msg.payload);
return msg;
Example: Exclude Diagnostic Variables
// Function node: Exclude diagnostic/maintenance variables
function excludeDiagnostics(obj) {
const result = {};
for (const [key, value] of Object.entries(obj)) {
// Skip diagnostic folders
if (key === 'Diagnostics' || key === 'Maintenance') {
continue;
}
if (typeof value === 'string') {
result[key] = value;
} else if (typeof value === 'object') {
result[key] = excludeDiagnostics(value);
}
}
return result;
}
msg.payload = excludeDiagnostics(msg.payload);
return msg;
Processing Subtree Output
Example: Find Maximum Temperature
// Function node: Find max temperature in subtree
function findMaxTemp(obj, path = '') {
let max = { value: -Infinity, path: '' };
for (const [key, value] of Object.entries(obj)) {
const currentPath = path ? `${path}.${key}` : key;
if (typeof value === 'number' && key.toLowerCase().includes('temp')) {
if (value > max.value) {
max = { value: value, path: currentPath };
}
} else if (typeof value === 'object') {
const childMax = findMaxTemp(value, currentPath);
if (childMax.value > max.value) {
max = childMax;
}
}
}
return max;
}
const result = findMaxTemp(msg.payload);
msg.payload = {
maxTemperature: result.value,
location: result.path
};
return msg;
Example: Count Alarm Conditions
// Function node: Count variables in alarm state
function countAlarms(obj, alarms = []) {
for (const [key, value] of Object.entries(obj)) {
if (typeof value === 'number') {
// Check for alarm conditions
if (key.toLowerCase().includes('temp') && value > 80) {
alarms.push({ type: 'HighTemp', variable: key, value: value });
} else if (key.toLowerCase().includes('pressure') && value > 3) {
alarms.push({ type: 'HighPressure', variable: key, value: value });
}
} else if (typeof value === 'object') {
countAlarms(value, alarms);
}
}
return alarms;
}
const alarms = countAlarms(msg.payload);
if (alarms.length > 0) {
msg.payload = {
alarmCount: alarms.length,
alarms: alarms
};
return msg;
}
return null; // No alarms
Example: Create Flat List
Convert hierarchical structure to a flat list for databases:
// Function node: Flatten structure
function flatten(obj, prefix = '', result = []) {
for (const [key, value] of Object.entries(obj)) {
const path = prefix ? `${prefix}.${key}` : key;
if (typeof value === 'object' && !Array.isArray(value)) {
flatten(value, path, result);
} else {
result.push({
path: path,
value: value,
timestamp: new Date()
});
}
}
return result;
}
msg.payload = flatten(msg.payload);
return msg;
Output:
[
{ "path": "Process.Temperature", "value": 85.3, "timestamp": "..." },
{ "path": "Process.Pressure", "value": 2.5, "timestamp": "..." },
{ "path": "Process.pH", "value": 7.2, "timestamp": "..." },
{ "path": "Cooling.InletTemp", "value": 15.0, "timestamp": "..." },
...
]
Advanced: Periodic Re-exploration
Periodically re-explore to discover new variables added to the server:
[
{
"id": "inject1",
"type": "inject",
"name": "Re-explore Every Hour",
"repeat": "3600",
"once": true
},
{
"id": "explore1",
"type": "OpcUa-Explore"
},
{
"id": "function1",
"type": "function",
"name": "Compare Structures",
"func": `
// Compare with previous structure
const previous = flow.get('structure') || {};
const current = msg.payload;
// Store current structure
flow.set('structure', current);
// Detect new variables
const changes = findNewVariables(previous, current);
if (changes.length > 0) {
node.warn('New variables detected: ' + JSON.stringify(changes));
}
return msg;
`
},
{
"id": "monitor1",
"type": "OpcUa-Monitor"
}
]
Controlling Depth
Use specific NodeIds to control the scope of exploration and avoid monitoring too many variables:
Example:
msg.payload = {
nodeId: "ns=2;s=Equipment.Reactor1" // Specific equipment
};
Tips & Best Practices
Start with Small Subtrees
Begin with a small subtree to understand the structure:
- Explore a single piece of equipment first
- Verify the output structure
- Test monitoring behavior
- Expand to larger subtrees
Use Appropriate Depth
- Depth 2-3: Single equipment with subsystems
- Depth 4-6: Complete production line
- Depth 7-10: Entire plant (use with caution)
Monitor Subtree Size
// Function node: Count variables
function countVariables(obj) {
let count = 0;
for (const value of Object.values(obj)) {
if (typeof value === 'string') {
count++;
} else if (typeof value === 'object') {
count += countVariables(value);
}
}
return count;
}
const varCount = countVariables(msg.payload);
if (varCount > 100) {
node.warn(`Large subtree: ${varCount} variables`);
}
return msg;
Handle Exploration Errors
// Function node: Validate exploration result
if (!msg.payload || Object.keys(msg.payload).length === 0) {
node.error("Exploration returned no variables");
return null;
}
// Check for error status
if (msg.statusCode && msg.statusCode.value !== 0) {
node.error("Exploration failed: " + msg.statusCode.description);
return null;
}
return msg;
Save Structure for Reference
// Function node: Save structure for documentation
const structure = msg.payload;
const timestamp = new Date().toISOString();
// Save to file or context
flow.set('lastExploreStructure', {
timestamp: timestamp,
structure: structure,
variableCount: countVariables(structure)
});
return msg;
Performance Considerations
Large Subtrees
Monitoring hundreds of variables:
- Increases memory usage
- Generates more network traffic
- May impact Node-RED performance
Solutions:
- Filter variables before monitoring
- Use multiple Monitor nodes for different subtrees
- Increase sampling intervals
- Implement deadband filtering
Exploration Frequency
- One-time: Explore once at flow start
- Periodic: Re-explore hourly/daily
- Event-driven: Re-explore when address space changes
Notification Volume
Each change notification includes all values in the structure:
- Consider notification frequency
- Monitor network bandwidth
- Implement change detection
Troubleshooting
No Variables Found
Symptom: Explore returns empty structure
Solution:
- Verify the root NodeId exists
- Check folder has child nodes
- Set followOrganizes to true
- Verify server permissions
Too Many Variables
Symptom: Performance degradation
Solution:
- Reduce exploration depth
- Filter output before monitoring
- Split into multiple Monitor nodes
- Increase sampling interval
Structure Changes Not Detected
Symptom: New variables don't appear
Solution:
- Re-run exploration to discover new variables
- Implement periodic re-exploration
- Restart monitoring after structure changes
Memory Issues
Symptom: Node-RED becomes slow or crashes
Solution:
- Monitor fewer variables
- Increase Node-RED memory limit
- Use multiple smaller subtrees
- Implement pagination
Use Cases
Equipment Commissioning
Quickly monitor all variables on new equipment:
// Monitor everything during commissioning
msg.payload = { nodeId: "ns=2;s=NewEquipment" };
Troubleshooting
Monitor complete system during troubleshooting:
// Monitoring for diagnostics
msg.payload = {
nodeId: "ns=2;s=FailingSystem"
};
System Overview
Create plant-wide dashboards:
// Monitor all production lines
msg.payload = { nodeId: "ns=2;s=ProductionFloor" };
Next Steps
- Deadband Filtering - Reduce notification frequency
- Monitor Events - Monitor OPC UA events
- Explore Node - Learn more about exploration