Monitoring JSON Structures
This tutorial shows you how to monitor multiple OPC UA variables using a JSON structure with named properties. This provides better readability and maintainability than arrays, especially for complex monitoring scenarios.
This approach is ideal when:
- You want self-documenting code with named properties
- You're monitoring complex equipment with many variables
- You need hierarchical organization of monitored data
- You want to preserve the structure in the output
Prerequisites
- An active connection to an OPC UA server (see Create a connection)
- A subscription configured in the connection endpoint
- NodeIds of the variables you want to monitor
Configuration
Monitor Node Setup
- Start Immediately: Uncheck this option
- NodeId Field: Leave empty
- Configure Monitoring Parameters:
- Sample Interval:
1000ms - Queue Size:
1000 - Discard Oldest:
true
- Sample Interval:
Basic JSON Structure
Inject a JSON object where each property contains a NodeId:
{
"payload": {
"temperature": "ns=1;s=Temperature",
"pressure": "ns=1;s=Pressure",
"flowRate": "ns=1;s=FlowRate"
}
}
Example Flow:
[
{
"id": "inject1",
"type": "inject",
"name": "Monitor Process Variables",
"payload": {
"temperature": "ns=1;s=Temperature",
"pressure": "ns=1;s=Pressure",
"flowRate": "ns=1;s=FlowRate"
},
"payloadType": "json"
},
{
"id": "monitor1",
"type": "OpcUa-Monitor",
"name": "JSON Monitor",
"endpoint": "opcua_endpoint1",
"subscription": "subscription1",
"startImmediately": false,
"nodeId": ""
},
{
"id": "debug1",
"type": "debug",
"name": "Show Structured Data"
}
]
Output Format
The output preserves the JSON structure, replacing NodeIds with actual values:
Value Format (Default):
Input:
{
"temperature": "ns=1;s=Temperature",
"pressure": "ns=1;s=Pressure",
"flowRate": "ns=1;s=FlowRate"
}
Output:
{
"payload": {
"temperature": 23.5,
"pressure": 101.3,
"flowRate": 45.2
}
}
DataValue Format:
Set msg.outputType = "DataValue" for full metadata:
{
"payload": {
"temperature": {
"value": 23.5,
"statusCode": {"value": 0, "description": "Good"},
"sourceTimestamp": "2024-11-24T10:30:45.123Z",
"serverTimestamp": "2024-11-24T10:30:45.125Z"
},
"pressure": {
"value": 101.3,
"statusCode": {"value": 0, "description": "Good"},
"sourceTimestamp": "2024-11-24T10:30:45.124Z",
"serverTimestamp": "2024-11-24T10:30:45.126Z"
},
"flowRate": {
"value": 45.2,
"statusCode": {"value": 0, "description": "Good"},
"sourceTimestamp": "2024-11-24T10:30:45.125Z",
"serverTimestamp": "2024-11-24T10:30:45.127Z"
}
}
}
Nested Structures
Create hierarchical structures that mirror your equipment organization:
{
"payload": {
"reactor1": {
"process": {
"temperature": "ns=2;s=Reactor1.Process.Temperature",
"pressure": "ns=2;s=Reactor1.Process.Pressure",
"pH": "ns=2;s=Reactor1.Process.pH"
},
"cooling": {
"inletTemp": "ns=2;s=Reactor1.Cooling.InletTemp",
"outletTemp": "ns=2;s=Reactor1.Cooling.OutletTemp",
"flowRate": "ns=2;s=Reactor1.Cooling.FlowRate"
},
"status": {
"running": "ns=2;s=Reactor1.Status.Running",
"mode": "ns=2;s=Reactor1.Status.Mode"
}
}
}
}
Output Structure:
{
"payload": {
"reactor1": {
"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: Multi-Zone Temperature Monitoring
Monitor temperatures across multiple zones with clear organization:
// Function node: Build monitoring structure
const zones = ["North", "South", "East", "West"];
const structure = {};
zones.forEach(zone => {
structure[zone.toLowerCase()] = {
temperature: `ns=1;s=Zone${zone}.Temperature`,
humidity: `ns=1;s=Zone${zone}.Humidity`,
setpoint: `ns=1;s=Zone${zone}.Setpoint`
};
});
msg.payload = structure;
return msg;
Generated Structure:
{
"north": {
"temperature": "ns=1;s=ZoneNorth.Temperature",
"humidity": "ns=1;s=ZoneNorth.Humidity",
"setpoint": "ns=1;s=ZoneNorth.Setpoint"
},
"south": {
"temperature": "ns=1;s=ZoneSouth.Temperature",
"humidity": "ns=1;s=ZoneSouth.Humidity",
"setpoint": "ns=1;s=ZoneSouth.Setpoint"
},
"east": {
"temperature": "ns=1;s=ZoneEast.Temperature",
"humidity": "ns=1;s=ZoneEast.Humidity",
"setpoint": "ns=1;s=ZoneEast.Setpoint"
},
"west": {
"temperature": "ns=1;s=ZoneWest.Temperature",
"humidity": "ns=1;s=ZoneWest.Humidity",
"setpoint": "ns=1;s=ZoneWest.Setpoint"
}
}
Output Processing:
// Function node: Process zone data
const zones = msg.payload;
// Find zones out of range
const alerts = [];
for (const [zoneName, data] of Object.entries(zones)) {
const tempDiff = Math.abs(data.temperature - data.setpoint);
if (tempDiff > 2.0) {
alerts.push({
zone: zoneName,
temperature: data.temperature,
setpoint: data.setpoint,
deviation: tempDiff
});
}
}
if (alerts.length > 0) {
msg.payload = alerts;
return msg;
}
return null; // All zones OK
Example: Production Line Monitoring
Monitor multiple production lines with comprehensive data:
// Function node: Build production line structure
const lineCount = 3;
const structure = {};
for (let i = 1; i <= lineCount; i++) {
structure[`line${i}`] = {
status: {
running: `ns=1;s=Line${i}.Status.Running`,
speed: `ns=1;s=Line${i}.Status.Speed`,
mode: `ns=1;s=Line${i}.Status.Mode`
},
production: {
count: `ns=1;s=Line${i}.Production.Count`,
target: `ns=1;s=Line${i}.Production.Target`,
quality: `ns=1;s=Line${i}.Production.Quality`
},
maintenance: {
hours: `ns=1;s=Line${i}.Maintenance.Hours`,
nextService: `ns=1;s=Line${i}.Maintenance.NextService`
}
};
}
msg.payload = structure;
return msg;
Using with Explore Node
The Explore node can generate JSON structures automatically! See Monitor Subtree for details.
Quick Example:
- Use Explore node with
outputType=NodeID - Connect Explore output directly to Monitor input
- Monitor automatically receives a JSON structure matching the OPC UA tree
[
{
"id": "explore1",
"type": "OpcUa-Explore",
"name": "Explore Equipment",
"nodeId": "ns=2;s=Equipment.Reactor1",
"outputType": "NodeID"
},
{
"id": "monitor1",
"type": "OpcUa-Monitor",
"name": "Monitor Equipment",
"startImmediately": false
}
]
Processing Structured Output
Example: Dashboard Display
Format the structured data for a dashboard:
// Function node: Format for UI
const data = msg.payload;
// Create dashboard-friendly format
msg.payload = {
title: "Reactor Status",
timestamp: new Date(),
metrics: [
{
label: "Temperature",
value: data.reactor1.process.temperature,
unit: "°C",
status: data.reactor1.process.temperature > 80 ? "warning" : "ok"
},
{
label: "Pressure",
value: data.reactor1.process.pressure,
unit: "bar",
status: data.reactor1.process.pressure > 3 ? "warning" : "ok"
},
{
label: "pH",
value: data.reactor1.process.pH,
unit: "",
status: Math.abs(data.reactor1.process.pH - 7) > 1 ? "warning" : "ok"
}
]
};
return msg;
Example: Database Storage
Store structured data in a database:
// Function node: Prepare for database
const data = msg.payload;
const timestamp = new Date();
// Flatten structure for database
msg.payload = {
timestamp: timestamp,
reactor1_process_temp: data.reactor1.process.temperature,
reactor1_process_pressure: data.reactor1.process.pressure,
reactor1_process_ph: data.reactor1.process.pH,
reactor1_cooling_inlet: data.reactor1.cooling.inletTemp,
reactor1_cooling_outlet: data.reactor1.cooling.outletTemp,
reactor1_cooling_flow: data.reactor1.cooling.flowRate,
reactor1_status_running: data.reactor1.status.running,
reactor1_status_mode: data.reactor1.status.mode
};
return msg;
Example: Alarm Detection
Implement sophisticated alarm logic using structured data:
// Function node: Complex alarm detection
const { reactor1 } = msg.payload;
const alarms = [];
// Temperature alarm
if (reactor1.process.temperature > 90) {
alarms.push({
severity: "high",
type: "HighTemperature",
location: "Reactor1.Process",
value: reactor1.process.temperature,
threshold: 90
});
}
// Pressure alarm with cooling check
if (reactor1.process.pressure > 3.0) {
const coolingEfficiency =
reactor1.cooling.inletTemp - reactor1.cooling.outletTemp;
if (coolingEfficiency < 5) {
alarms.push({
severity: "critical",
type: "HighPressureWithPoorCooling",
location: "Reactor1",
details: {
pressure: reactor1.process.pressure,
coolingDelta: coolingEfficiency
}
});
}
}
// pH deviation
const phDeviation = Math.abs(reactor1.process.pH - 7);
if (phDeviation > 1.5) {
alarms.push({
severity: "medium",
type: "pHDeviation",
location: "Reactor1.Process",
value: reactor1.process.pH,
deviation: phDeviation
});
}
if (alarms.length > 0) {
msg.payload = alarms;
return msg;
}
return null; // No alarms
Dynamic Structure Updates
Change the monitored structure at runtime:
// Function node: Switch monitoring profiles
const profile = flow.get("monitoring_profile") || "basic";
let structure;
if (profile === "basic") {
structure = {
temperature: "ns=1;s=Temperature",
pressure: "ns=1;s=Pressure"
};
} else if (profile === "detailed") {
structure = {
process: {
temperature: "ns=1;s=Process.Temperature",
pressure: "ns=1;s=Process.Pressure",
flowRate: "ns=1;s=Process.FlowRate",
pH: "ns=1;s=Process.pH"
},
cooling: {
inletTemp: "ns=1;s=Cooling.InletTemp",
outletTemp: "ns=1;s=Cooling.OutletTemp"
}
};
} else if (profile === "diagnostic") {
structure = {
vibration: "ns=1;s=Diagnostics.Vibration",
power: "ns=1;s=Diagnostics.Power",
runtime: "ns=1;s=Diagnostics.Runtime",
errors: "ns=1;s=Diagnostics.ErrorCount"
};
}
msg.payload = structure;
return msg;
Tips & Best Practices
Use Descriptive Property Names
✅ Good:
{
temperature: "ns=1;s=Temp",
pressure: "ns=1;s=Pressure",
flowRate: "ns=1;s=Flow"
}
❌ Avoid:
{
t: "ns=1;s=Temp",
p: "ns=1;s=Pressure",
f: "ns=1;s=Flow"
}
Mirror Equipment Hierarchy
Organize your JSON structure to match physical equipment:
{
building1: {
floor1: {
zone1: { ... },
zone2: { ... }
},
floor2: {
zone1: { ... },
zone2: { ... }
}
}
}
Document Your Structure
Always document the structure for maintainability:
/**
* Monitoring Structure:
* {
* reactor1: {
* process: { temperature, pressure, pH },
* cooling: { inletTemp, outletTemp, flowRate },
* status: { running, mode }
* }
* }
*/
const structure = buildReactorStructure();
Validate Structure
Validate the structure before sending to Monitor:
// Function node: Validate structure
function hasNodeIds(obj) {
for (const value of Object.values(obj)) {
if (typeof value === 'string') {
// Check if it looks like a NodeId
if (!value.includes('ns=') && !value.includes('/')) {
return false;
}
} else if (typeof value === 'object') {
if (!hasNodeIds(value)) {
return false;
}
}
}
return true;
}
if (!hasNodeIds(msg.payload)) {
node.error("Invalid monitoring structure");
return null;
}
return msg;
Performance Considerations
Structure Complexity
- Flat structures (1 level): Fastest processing
- Nested structures (2-3 levels): Good balance
- Deep nesting (4+ levels): May impact performance
Memory Usage
Large structures with many variables consume more memory. Monitor Node-RED's memory usage if monitoring hundreds of variables.
Notification Size
Each notification includes the entire structure. Consider:
- Network bandwidth
- Message processing time
- Node-RED message size limits
Troubleshooting
Structure Not Preserved
Symptom: Output is an array instead of structure
Cause: Incorrect input format
Solution: Ensure NodeIds are leaf values in the structure:
// ✅ Correct
{ temp: "ns=1;s=Temp" }
// ❌ Wrong
{ temp: { nodeId: "ns=1;s=Temp" } }
Missing Values in Output
Symptom: Some properties are missing
Cause: Invalid NodeIds or access denied
Solution: Validate each NodeId individually using the Read node
Memory Errors
Symptom: Node-RED crashes with large structures
Solution:
- Split into smaller structures
- Use multiple Monitor nodes
- Increase Node-RED memory limit
Next Steps
- Monitor Subtree - Auto-generate structures using Explore
- Deadband Filtering - Reduce notification frequency
- Monitor Events - Monitor OPC UA events