An office occupancy sensor network tells you which rooms and desks are actually used, and when. In this use case you will compare PIR, mmWave and CO2-based sensing, publish occupancy over MQTT with a clean topic design, and use the data for space utilization reports and demand-based HVAC control on a ConnectMQ broker.
Why Measure Occupancy
Hybrid work has made office use unpredictable. Meeting rooms booked in the calendar sit empty, while small focus rooms are always full. Floors are heated and cooled to full schedule even on days when a fraction of staff come in. Occupancy data answers practical questions: which rooms are oversized, whether a floor can be closed on Fridays, and whether ventilation can be reduced in empty zones.
Booking systems and badge data give part of the picture, but they record intent and entry, not actual presence in a room. Sensors fill that gap, and MQTT is a natural way to collect their data across many buildings.
Choosing the Right Sensing Technology
Each technology has a characteristic failure mode, and the best deployments combine two.
| Technology | How it works | Strengths | Weaknesses | Best for |
|---|---|---|---|---|
| PIR | Detects moving heat sources | Cheap, low power, mature | Misses people sitting still, so rooms look empty during focused work | Corridors, toilets, lighting control |
| mmWave radar | Detects tiny movements including breathing | Detects stationary people, works through thin covers | Can see through walls and pick up neighbors if not tuned; needs mains or large battery | Meeting rooms, focus rooms, desks |
| CO2 | Rising CO2 concentration indicates people breathing | Gives a rough count and air quality at the same time | Slow (minutes), depends on ventilation rate | Ventilation control, large rooms |
| Desk sensors | PIR or thermal under the desk | Desk-level utilization | Many devices to maintain | Hot-desking analytics |
| Thermal array / people counters | Low-resolution heat image or doorway counting | Counts people without images | Higher cost, careful placement | Large open spaces, entrances |
A common and robust combination for meeting rooms is mmWave for presence plus CO2 for confirmation and ventilation. PIR alone tends to report a room as empty during long video calls because people barely move.
Privacy first
Occupancy data is about people. Use sensors that do not capture images, publish presence or counts rather than identities, and never put names or badge IDs in topics. Aggregate desk data before sharing it widely, and check local regulations and works council agreements before deployment.
MQTT Topic and Payload Design
Model topics on the physical hierarchy so wildcards answer real questions:
acme/ist01/hq/f03/meeting-301/occupancy presence state (retained)
acme/ist01/hq/f03/meeting-301/co2 CO2 ppm
acme/ist01/hq/f03/desk-3-14/occupancy desk presence (retained)
A presence message should carry the state, the source and when it last changed:
{
"ts": "2026-10-16T09:41:07Z",
"occupied": true,
"source": "mmwave",
"since": "2026-10-16T09:12:30Z",
"count_estimate": null
}
Publish occupancy as retained so a dashboard that connects mid-day shows current state immediately. Publish on change plus a heartbeat every few minutes, which keeps traffic tiny and still lets you detect a dead sensor. With the folder-style hierarchy, acme/ist01/hq/f03/+/occupancy returns every room and desk on floor three, and the ConnectMQ devices view can group the same topics by site and area.
A Simple mmWave Sensor Node
Popular low-cost mmWave modules such as the LD2410 family expose a digital output pin that goes high when presence is detected. An ESP32 can debounce that pin and publish changes. The sketch uses PubSubClient and the public broker for testing.
#include <WiFi.h>
#include <PubSubClient.h>
const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
const char* BROKER = "broker.connectmq.com";
const int PORT = 1883;
const char* TOPIC = "connectmq/demo/occ-5t2w/f03/meeting-301/occupancy";
const int PRESENCE_PIN = 27; // LD2410 OUT pin
const unsigned long HOLD_MS = 60000; // stay "occupied" 60 s after last detection
const unsigned long HEARTBEAT_MS = 300000;
WiFiClient net;
PubSubClient mqtt(net);
bool occupied = false;
unsigned long lastSeen = 0, lastPublish = 0;
void publishState() {
char msg[96];
snprintf(msg, sizeof(msg), "{\"occupied\":%s,\"source\":\"mmwave\",\"uptime_s\":%lu}",
occupied ? "true" : "false", millis() / 1000);
mqtt.publish(TOPIC, msg, true); // retained
lastPublish = millis();
}
void connectMqtt() {
while (!mqtt.connected()) {
String id = "occ-" + String((uint32_t)ESP.getEfuseMac(), HEX);
if (mqtt.connect(id.c_str())) {
publishState();
} else {
delay(2000);
}
}
}
void setup() {
pinMode(PRESENCE_PIN, INPUT);
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) delay(250);
mqtt.setServer(BROKER, PORT);
}
void loop() {
if (!mqtt.connected()) connectMqtt();
mqtt.loop();
if (digitalRead(PRESENCE_PIN) == HIGH) lastSeen = millis();
bool now = (millis() - lastSeen) < HOLD_MS && lastSeen != 0;
if (now != occupied) {
occupied = now;
publishState();
} else if (millis() - lastPublish > HEARTBEAT_MS) {
publishState();
}
delay(100);
}
The hold time is the most important tuning parameter. Too short and the room flickers between occupied and empty; too long and the room looks busy after people leave. Sixty seconds to a few minutes works well for meeting rooms. Tune the radar's detection range with the vendor tool so it does not see into the corridor.
From Raw Presence to Utilization
Raw on/off messages become useful once aggregated. A small subscriber can compute, per room and per day, the share of working hours that the room was occupied. A minimal Python version:
import json
import time
import uuid
from collections import defaultdict
import paho.mqtt.client as mqtt
PREFIX = "connectmq/demo/occ-5t2w"
occupied_since = {}
occupied_seconds = defaultdict(float)
def on_connect(client, userdata, flags, reason_code, properties):
client.subscribe(f"{PREFIX}/+/+/occupancy", qos=1)
def on_message(client, userdata, msg):
room = "/".join(msg.topic.split("/")[-3:-1])
data = json.loads(msg.payload)
now = time.time()
if data["occupied"] and room not in occupied_since:
occupied_since[room] = now
elif not data["occupied"] and room in occupied_since:
occupied_seconds[room] += now - occupied_since.pop(room)
hours = occupied_seconds[room] / 3600
client.publish(f"{PREFIX}/{room}/utilization",
json.dumps({"occupied_hours_today": round(hours, 2)}), retain=True)
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=f"occ-agg-{uuid.uuid4().hex[:6]}")
client.on_connect = on_connect
client.on_message = on_message
client.connect("broker.connectmq.com", 1883, 60)
client.loop_forever()
In production, store the transitions in a database so you can report weekly peaks and compare rooms by size.
Using Occupancy for HVAC Control
Occupancy data pays back fastest when it drives ventilation and temperature setbacks:
- When a zone has been empty for a set period, a gateway can widen its temperature deadband or reduce airflow.
- When CO2 rises above your target, increase ventilation regardless of presence state.
- Before scheduled occupancy, pre-condition the space as usual; occupancy only trims, never replaces, the schedule.
- Always keep local control safe: the HVAC controller should fall back to its schedule if occupancy data stops arriving.
The write-back path to the BMS is the same one described in BACnet to MQTT, and the HVAC side of the story is covered in HVAC monitoring with IoT sensors. CO2 measurement doubles as an air quality signal; see our indoor air quality monitor build.
Validating Sensor Accuracy
Before anyone builds a report on occupancy data, check that the sensors tell the truth. A simple validation week works well. Pick a handful of rooms and ask a colleague to note actual use at fixed times, or compare sensor data with calendar bookings and look for disagreements. Typical findings are mmWave sensors that report a room occupied because they see movement in the corridor, PIR sensors that report empty rooms during long calls, and CO2 sensors that lag by ten minutes or more after people arrive.
Fix each finding at the source: reduce the radar's detection range, move a PIR so it faces the seating area rather than the door, or use CO2 only as a confirmation signal. Record the final settings per room type so new installations start from a known-good configuration. Validation also builds trust with the people whose rooms are being measured, which matters as much as technical accuracy.
Deployment Checklist
- Survey rooms and decide technology per room type.
- Mount mmWave sensors facing into the room, away from doors and windows.
- Give each sensor a unique client ID and credentials on a private broker.
- Commission each room by walking in and out while watching the topic explorer.
- Run for two weeks before drawing utilization conclusions.
Testing on the public MQTT test broker is fine with a demo prefix, but real occupancy data reveals when people are in the building, so it belongs on a private broker over TLS. A managed MQTT broker from ConnectMQ gives every sensor its own credentials, and a few dozen sensors fit comfortably into a pilot. For more building projects, see the IoT automation guide.
Run Your Occupancy Sensors on ConnectMQ
Sign up at app.connectmq.com and create a free Serverless broker, no credit card required. Connect your first sensors, watch presence change live in the topic explorer, and add threshold alert rules for CO2 in busy rooms. ConnectMQ handles the broker so you can focus on the space.
Frequently Asked Questions
Which office occupancy sensor is most accurate for meeting rooms?
mmWave radar is usually the best single choice because it detects people sitting still. Combining it with CO2 adds confirmation and supports ventilation control.
Do occupancy sensors capture personal data?
PIR, mmWave and CO2 sensors do not capture images or identities, but occupancy patterns are still sensitive. Publish only presence or counts and keep the data on a private broker.
How long should the hold time be?
Start with 60 seconds for small rooms and up to a few minutes for large ones, then adjust after watching real data for a week.
Can occupancy data reduce HVAC energy use?
Yes, by setting back temperature and ventilation in empty zones. Keep the controller's schedule as a fallback so comfort never depends on the sensor network.



