Skip to main content

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

  1. Start Immediately: Uncheck this option
  2. NodeId Field: Leave empty
  3. Configure Monitoring Parameters:
    • Sample Interval: 1000 ms
    • Queue Size: 1000
    • Discard Oldest: true

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:

  1. Use Explore node with outputType=NodeID
  2. Connect Explore output directly to Monitor input
  3. 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:

  1. Split into smaller structures
  2. Use multiple Monitor nodes
  3. Increase Node-RED memory limit

Next Steps

See Also