Skip to main content

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

  1. Explore Node traverses the OPC UA address space starting from a root node
  2. Explore Node outputs a JSON structure with all discovered variable NodeIds
  3. Monitor Node receives this structure and monitors all variables
  4. 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:

  1. Inject triggers exploration
  2. Explore discovers subtree and outputs NodeIds
  3. Monitor receives NodeIds and starts monitoring
  4. 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:

  1. Explore a single piece of equipment first
  2. Verify the output structure
  3. Test monitoring behavior
  4. 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:

  1. Verify the root NodeId exists
  2. Check folder has child nodes
  3. Set followOrganizes to true
  4. Verify server permissions

Too Many Variables

Symptom: Performance degradation

Solution:

  1. Reduce exploration depth
  2. Filter output before monitoring
  3. Split into multiple Monitor nodes
  4. 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:

  1. Monitor fewer variables
  2. Increase Node-RED memory limit
  3. Use multiple smaller subtrees
  4. 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

See Also