Node-RED

Node-RED OEE Dashboard from MQTT Machine Data

Calculate OEE in Node-RED from two MQTT topics, machine state and part counters, then display availability, performance, quality and OEE on Dashboard 2.0 gauges and republish the KPIs for other systems.

Node-RED OEE dashboard with four gauges for availability, performance, quality and OEE fed by MQTT machine data
Run this on your own MQTT brokerGet a private, managed MQTT broker on the Free plan and start from ready-made code samples.
Try it free Code samples No card required

A Node-RED OEE dashboard is one of the fastest ways to put a real manufacturing KPI in front of a team without buying an MES. In this tutorial you will compute availability, performance and quality from two MQTT topics, machine state and part counters, and show live OEE on Dashboard 2.0 gauges. You will also republish the KPIs to MQTT so other tools can use them, and learn the edge cases that make naive OEE numbers wrong.

OEE in One Minute

Overall Equipment Effectiveness multiplies three ratios, each between 0 and 1:

Factor Formula What it loses
Availability Run time / planned production time Breakdowns, changeovers, waiting for material
Performance (Ideal cycle time x total count) / run time Slow cycles, micro-stops
Quality Good count / total count Scrap and rework
OEE Availability x Performance x Quality Everything above

The beauty of the formula is that it needs very little data: when the machine was running, how many parts it made, and how many were good. That maps neatly onto MQTT. For a deeper look at the definitions, including planned versus unplanned stops, see how to calculate OEE.

The MQTT Data Model

The machine, or a gateway in front of it, publishes two topics:

Topic Payload Retained When
connectmq/demo/a7f3k2/plant1/line1/press1/state RUNNING, IDLE or DOWN Yes On every change
connectmq/demo/a7f3k2/plant1/line1/press1/count {"total": 1234, "good": 1201} No Every few seconds

Two design choices make this robust:

  • State is event-driven and retained. Node-RED learns the current state immediately after connecting and accumulates run time from the change events.
  • Counters are cumulative, not per part. If one message is lost, the next one still carries the correct total. Node-RED computes the deltas, and a counter that goes backwards is treated as a PLC reset rather than negative production.

The topic follows an ISA-95 style hierarchy (site, line, machine), behind a demo prefix. Replace a7f3k2 with your own random string, because the public broker is readable by anyone.

Simulate a Machine

This Python script behaves like a press with a 2-second ideal cycle, occasional stops and about 3 percent scrap. It publishes to broker.connectmq.com, the public MQTT test broker.

# pip install "paho-mqtt>=2.0"
import json
import random
import time
import uuid

import paho.mqtt.client as mqtt

BASE = "connectmq/demo/a7f3k2/plant1/line1/press1"

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2,
                     client_id=f"press-sim-{uuid.uuid4().hex[:8]}")
client.connect("broker.connectmq.com", 1883, keepalive=60)
client.loop_start()

state = None
total = 0
good = 0
next_cycle = time.time()
last_count_pub = 0.0


def set_state(new_state):
    global state
    if new_state != state:
        state = new_state
        client.publish(f"{BASE}/state", state, qos=1, retain=True)
        print("State:", state)


set_state("RUNNING")
try:
    while True:
        now = time.time()
        if state == "RUNNING":
            if random.random() < 0.004:
                set_state(random.choice(["IDLE", "DOWN"]))
            elif now >= next_cycle:
                total += 1
                if random.random() > 0.03:
                    good += 1
                # Real cycles are a bit slower than ideal
                next_cycle = now + random.uniform(2.0, 2.6)
        elif random.random() < 0.02:
            set_state("RUNNING")
            next_cycle = time.time() + 2.0

        if now - last_count_pub >= 5:
            client.publish(f"{BASE}/count",
                           json.dumps({"total": total, "good": good}), qos=1)
            last_count_pub = now
        time.sleep(0.2)
except KeyboardInterrupt:
    pass
finally:
    client.loop_stop()
    client.disconnect()

Build the Node-RED OEE Dashboard Flow

You need Node-RED 4.x with Dashboard 2.0 (@flowfuse/node-red-dashboard) installed. If you have not used it yet, the Dashboard 2.0 tutorial covers installation and the page and group structure.

Inputs

  1. An mqtt in node on .../press1/state, output "a UTF-8 string".
  2. An mqtt in node on .../press1/count, output "a parsed JSON object".
  3. An inject node named "tick" that repeats every 5 seconds with msg.topic set to tick.
  4. A ui-button labelled "Start new shift" with msg.topic set to shift/reset, plus an inject node with the same topic scheduled at your shift change times (for example 06:00, 14:00 and 22:00).

Use two specific subscriptions rather than a press1/# wildcard. You will later publish KPIs under the same machine path, and a wildcard would feed your own output back into the calculation.

The OEE calculation

Wire all four inputs into one function node with five outputs:

// Function node "OEE calculator" - set Outputs to 5
const IDEAL_CYCLE_S = 2.0;         // ideal seconds per part for this machine
const now = Date.now();

function newShift() {
    return { shiftStart: now, state: "UNKNOWN", since: now, runMs: 0,
             lastTotal: null, lastGood: null, total: 0, good: 0 };
}

let s = flow.get("oee") || newShift();
const topic = String(msg.topic || "");

if (topic === "shift/reset") {
    const keepState = s.state;
    s = newShift();
    s.state = keepState;            // the machine is still in the same state
    flow.set("oee", s);
    return null;
}

if (topic.endsWith("/state")) {
    const st = String(msg.payload).trim().toUpperCase();
    if (s.state === "RUNNING") s.runMs += now - s.since;
    s.state = st;
    s.since = now;
    flow.set("oee", s);
    return null;
}

if (topic.endsWith("/count")) {
    const total = Number(msg.payload.total);
    const good = Number(msg.payload.good);
    if (!Number.isFinite(total) || !Number.isFinite(good)) return null;
    if (s.lastTotal !== null) {
        // A counter that goes backwards means the PLC reset it
        const dTotal = total >= s.lastTotal ? total - s.lastTotal : total;
        const dGood = good >= s.lastGood ? good - s.lastGood : good;
        s.total += dTotal;
        s.good += Math.min(dGood, dTotal);
    }
    s.lastTotal = total;
    s.lastGood = good;
    flow.set("oee", s);
    return null;
}

if (topic !== "tick") return null;

// Compute KPIs for the shift so far
const runMs = s.runMs + (s.state === "RUNNING" ? now - s.since : 0);
const plannedMs = now - s.shiftStart;
const availability = plannedMs > 0 ? runMs / plannedMs : 0;
const rawPerformance = runMs > 0 ? (IDEAL_CYCLE_S * 1000 * s.total) / runMs : 0;
const performance = Math.min(1, rawPerformance);
const quality = s.total > 0 ? s.good / s.total : 0;
const oee = availability * performance * quality;

if (rawPerformance > 1.05) {
    node.warn("Performance above 100%: check IDEAL_CYCLE_S");
}

const pct = v => Math.round(v * 1000) / 10;
node.status({ fill: s.state === "RUNNING" ? "green" : "red", shape: "dot",
              text: `${s.state} OEE ${pct(oee)}%` });

return [
    { payload: pct(availability) },
    { payload: pct(performance) },
    { payload: pct(quality) },
    { payload: pct(oee) },
    { topic: "connectmq/demo/a7f3k2/plant1/line1/press1/oee",
      payload: { availability: pct(availability), performance: pct(performance),
                 quality: pct(quality), oee: pct(oee), total: s.total, good: s.good,
                 state: s.state, shiftStart: new Date(s.shiftStart).toISOString() },
      retain: true }
];

Click Start new shift once after deploying so the shift starts from a known moment.

Widgets

  • Outputs 1 to 4 go to four ui-gauge nodes, range 0 to 100, unit %. A common colour scheme for OEE is red below 60, amber from 60 to 85 and green above 85, but pick bands that reflect your own baseline.
  • Output 5 goes to an mqtt out node with the topic and retain fields left empty, so it uses msg.topic and msg.retain from the message. Add a ui-text node after a change node that extracts payload.state if you want the machine state on screen as well.

Open /dashboard and watch the numbers settle over the first few minutes. Early in a shift, a single stop swings availability wildly; that is mathematically correct, and operators get used to it quickly.

Pitfalls That Distort OEE

  1. Wrong ideal cycle time. If performance sits above 100 percent, your ideal cycle is too slow. Do not just cap it and move on; the cap in the code hides a data problem.
  2. Counting planned stops as downtime. Breaks, planned maintenance and no-order periods are usually excluded from planned production time. Add a PLANNED_STOP state and leave that time out of plannedMs if your plant uses this definition.
  3. Losing state across restarts. Flow context is in memory by default. Store the oee object in a file-backed context store so a Node-RED restart mid-shift does not reset the KPIs.
  4. Out-of-order messages. Over QoS 1 with one publisher, MQTT preserves order per topic, but state and count are separate topics. Calculating on a timer, as above, makes small timing differences irrelevant.
  5. Counter rollover. 16-bit PLC counters wrap at 65,535. The reset logic treats a wrap like a reset, which loses at most one interval of counts.

Downtime reasons are the natural next step: add a reason code to the DOWN state and accumulate minutes per reason. The guide to machine downtime tracking covers how to structure those codes.

Keeping OEE History

The gauges show the current shift only. To compare shifts, write the retained KPI message to a time-series database at the end of each shift, or every minute. The Node-RED InfluxDB tutorial shows how to store exactly this kind of JSON with tags such as line and machine, and then chart it.

Scaling to a Whole Line or Plant

One function node per machine works for a pilot. For a full line, subscribe with a single-level wildcard such as plant1/line1/+/state, keep a context object per machine keyed by the topic level, and publish each machine's KPIs to its own topic. Because the calculation only depends on MQTT topics, it does not care whether the data comes from a PLC gateway, an ESP32 counter or an edge device.

That is also where the broker matters. A plant with dozens of machines, a Node-RED server, a historian and maybe a SCADA client all connected at once needs a reliable cloud MQTT broker with authentication, rather than a public test service. ConnectMQ recognises ISA-95 style topics like plant1/line1/press1/state and groups devices by those levels in its portal, which makes it easy to see which machines are reporting. Threshold alert rules in the ConnectMQ portal can also flag a numeric value that crosses a limit you choose, while Node-RED stays responsible for the calculation itself. If you prefer not to operate any of this infrastructure yourself, a fully managed MQTT service keeps the broker side out of your maintenance list. For broker and Node-RED fundamentals, start with the Node-RED MQTT tutorial.

Put Your OEE Data on ConnectMQ

Sign up at app.connectmq.com with the free Serverless plan, no credit card needed. Create a broker, use the generated username, password and client ID in your machines and Node-RED, and switch to TLS on port 8883. You will have a private OEE pipeline running before the next shift change.

Frequently Asked Questions

What data do I need to calculate OEE in Node-RED?

You need machine state over time, a total part count, a good part count and the ideal cycle time. With those four inputs Node-RED can compute availability, performance, quality and OEE.

Why should machines publish cumulative counters?

Cumulative counters make the calculation tolerant of lost or delayed messages, because each message carries the full total. Node-RED computes the difference and handles resets.

How often should the OEE dashboard update?

Every 5 to 30 seconds is enough for operators. Calculating on a timer instead of on every message also keeps the dashboard smooth when many machines publish at once.

Can I show OEE for several machines on one page?

Yes. Use a wildcard subscription, keep one context object per machine, and add a dashboard group per machine or a table built with a ui-template node.

Related articles

Node-RED OPC UA Client to MQTT with ISA-95 Topics

Use node-red-contrib-opcua to subscribe to OPC UA variables on a PLC or simulation server, map node IDs to ISA-95 style MQTT topics, and publish values with quality and timestamps.

9 min read

All articles →

Your private MQTT broker is a few clicks away

Create an account, get your broker credentials and connect your first device — no servers to install, patch or scale.