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
- An
mqtt innode on.../press1/state, output "a UTF-8 string". - An
mqtt innode on.../press1/count, output "a parsed JSON object". - An inject node named "tick" that repeats every 5 seconds with
msg.topicset totick. - A
ui-buttonlabelled "Start new shift" withmsg.topicset toshift/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-gaugenodes, 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 outnode with the topic and retain fields left empty, so it usesmsg.topicandmsg.retainfrom the message. Add aui-textnode after a change node that extractspayload.stateif 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
- 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.
- Counting planned stops as downtime. Breaks, planned maintenance and no-order periods are usually excluded from planned production time. Add a
PLANNED_STOPstate and leave that time out ofplannedMsif your plant uses this definition. - Losing state across restarts. Flow context is in memory by default. Store the
oeeobject in a file-backed context store so a Node-RED restart mid-shift does not reset the KPIs. - 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.
- 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.



