A water meter MQTT reader turns the dial in your basement into live consumption data: litres per day, current flow rate and an early warning when water keeps running when it should not. In this tutorial you will pick the right sensor for your meter, count pulses with an ESP32, keep the total safe across power cuts, and publish it to an MQTT broker. You will also add leak detection logic that catches a dripping tap or a burst pipe.
Choosing a Sensor for Your Water Meter
Most household water meters are mechanical and do not have a data port. You read them by detecting something that moves once per litre (or per ten litres). Check your meter before buying parts:
| Meter feature | Sensor | How it works | Notes |
|---|---|---|---|
| Pulse output cable or "pulse-ready" marking | Reed contact module from the meter vendor | Closes once per litre or per 10 litres | Most reliable, clean signal |
| Rotating magnet (some meters) | Reed switch or hall sensor taped on top | Magnet passes the sensor each rotation | Check with a phone compass app |
| Metal half-disc on the litre dial | Inductive proximity sensor (e.g. LJ18A3-8-Z/BX) | Detects the metal as it passes | Common choice; usually needs 5-12 V supply and a level shifter or divider |
| Reflective marker on the dial | Infrared reflective sensor (TCRT5000 style) | Detects the bright/dark segment | Sensitive to alignment and ambient light |
| No usable feature | Camera with digit recognition (ESP32-CAM) | Reads the digits optically | The "AI-on-the-edge-device" project publishes to MQTT itself |
Never open or modify the meter itself; it usually belongs to the utility. All the sensors above clip or tape onto the outside.
Wiring the ESP32
This tutorial uses a pulse signal on GPIO 25 that goes low once per litre. For a reed switch, connect it between GPIO 25 and GND and use the internal pull-up. For an NPN inductive sensor powered from 5 to 12 V, connect its output to GPIO 25 with the internal pull-up enabled; NPN open-collector outputs pull the line low and never drive it above 3.3 V, but check your sensor's datasheet, since some modules include a pull-up to their supply voltage, and then a voltage divider is required.
Design Decisions That Matter
A water meter reader looks simple, but three details decide whether its numbers can be trusted:
- The total must survive reboots. If the ESP32 restarts and counts from zero, your daily consumption chart breaks. The sketch stores the total in flash (NVS) and lets you set it to match the physical dial.
- Flash wear. Writing flash on every pulse wears it out quickly. Save when the count has changed and at most every few minutes, accepting that a power cut may lose a few litres.
- Debounce. Mechanical contacts bounce, and an inductive sensor can chatter at the edge of the metal. A short software debounce prevents double counts.
Topics for this build:
connectmq/demo/<random-id>/water/main/telemetry JSON every 60 s, retained
connectmq/demo/<random-id>/water/main/status online/offline (Last Will), retained
connectmq/demo/<random-id>/water/main/set/total_l command: set the total to match the dial
The ESP32 Water Meter MQTT Sketch
Install PubSubClient by Nick O'Leary from the Library Manager. Preferences is part of the ESP32 Arduino core.
#include <WiFi.h>
#include <PubSubClient.h>
#include <Preferences.h>
const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
const char* MQTT_HOST = "broker.connectmq.com";
const uint16_t MQTT_PORT = 1883;
const char* PREFIX = "connectmq/demo/w8m3d1/water/main";
const int PIN_PULSE = 25;
const float LITRES_PER_PULSE = 1.0; // 10.0 for many vendor pulse modules
const uint32_t SAVE_INTERVAL_MS = 5UL * 60 * 1000;
const uint32_t PUBLISH_INTERVAL_MS = 60UL * 1000;
WiFiClient net;
PubSubClient mqtt(net);
Preferences prefs;
volatile uint32_t pulses = 0;
volatile uint32_t lastPulseMs = 0;
uint64_t baseLitresX10 = 0; // stored total, in 0.1 L units
uint32_t savedPulses = 0, lastSaveMs = 0, lastPublishMs = 0, pulsesAtPublish = 0;
char clientId[32], topicStatus[96], topicTelemetry[96], topicSet[96];
void IRAM_ATTR onPulse() {
uint32_t now = millis();
if (now - lastPulseMs > 50) { pulses++; lastPulseMs = now; } // 50 ms debounce
}
double totalLitres() {
noInterrupts(); uint32_t p = pulses; interrupts();
return baseLitresX10 / 10.0 + p * LITRES_PER_PULSE;
}
void saveTotal() {
noInterrupts(); uint32_t p = pulses; interrupts();
if (p == savedPulses) return; // nothing new, no flash write
prefs.putULong64("total_x10", baseLitresX10 + (uint64_t)(p * LITRES_PER_PULSE * 10));
savedPulses = p;
}
void onMessage(char* topic, byte* payload, unsigned int len) {
if (strcmp(topic, topicSet) != 0) return;
char buf[24];
len = min(len, (unsigned int)sizeof(buf) - 1);
memcpy(buf, payload, len); buf[len] = 0;
double litres = atof(buf);
if (litres <= 0) return;
noInterrupts(); pulses = 0; interrupts();
baseLitresX10 = (uint64_t)(litres * 10);
savedPulses = 0; pulsesAtPublish = 0;
prefs.putULong64("total_x10", baseLitresX10);
Serial.printf("Total set to %.1f L\n", litres);
}
void connectMqtt() {
while (!mqtt.connected()) {
if (mqtt.connect(clientId, nullptr, nullptr, topicStatus, 1, true, "offline")) {
mqtt.publish(topicStatus, "online", true);
mqtt.subscribe(topicSet, 1);
} else {
Serial.printf("MQTT failed, state=%d\n", mqtt.state());
delay(5000);
}
}
}
void setup() {
Serial.begin(115200);
prefs.begin("water", false);
baseLitresX10 = prefs.getULong64("total_x10", 0);
pinMode(PIN_PULSE, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(PIN_PULSE), onPulse, FALLING);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
snprintf(clientId, sizeof(clientId), "water-%llx", ESP.getEfuseMac());
snprintf(topicStatus, sizeof(topicStatus), "%s/status", PREFIX);
snprintf(topicTelemetry, sizeof(topicTelemetry), "%s/telemetry", PREFIX);
snprintf(topicSet, sizeof(topicSet), "%s/set/total_l", PREFIX);
mqtt.setServer(MQTT_HOST, MQTT_PORT);
mqtt.setCallback(onMessage);
connectMqtt();
lastSaveMs = lastPublishMs = millis();
}
void loop() {
if (!mqtt.connected()) connectMqtt();
mqtt.loop();
uint32_t now = millis();
if (now - lastSaveMs >= SAVE_INTERVAL_MS) { saveTotal(); lastSaveMs = now; }
if (now - lastPublishMs >= PUBLISH_INTERVAL_MS) {
noInterrupts(); uint32_t p = pulses; interrupts();
float litresInterval = (p - pulsesAtPublish) * LITRES_PER_PULSE;
float flowLpm = litresInterval / ((now - lastPublishMs) / 60000.0);
char payload[160];
snprintf(payload, sizeof(payload),
"{\"total_l\":%.1f,\"flow_lpm\":%.2f,\"interval_l\":%.1f,\"rssi\":%d}",
totalLitres(), flowLpm, litresInterval, WiFi.RSSI());
mqtt.publish(topicTelemetry, payload, true);
Serial.println(payload);
pulsesAtPublish = p;
lastPublishMs = now;
}
}
Flash the board, open a tap and watch the serial monitor. Then subscribe from your computer:
mosquitto_sub -h broker.connectmq.com -p 1883 -i water-view-2207 -t "connectmq/demo/w8m3d1/water/#" -v
Read the physical dial and set the total to match it, in litres:
mosquitto_pub -h broker.connectmq.com -p 1883 -i water-set-2208 -t "connectmq/demo/w8m3d1/water/main/set/total_l" -m "482137"
The set command is deliberately not retained, so it applies once and does not reset the total every time the ESP32 reconnects. If anything fails to connect, the ESP32 MQTT tutorial has a step-by-step troubleshooting section.
Detecting Leaks from Water Meter MQTT Data
Consumption data becomes valuable when it warns you. Two patterns catch most leaks:
- Continuous flow. In most homes there are periods with zero flow every hour. If
interval_lhas been above zero for every one-minute interval over, say, two hours, something is running: a toilet fill valve, a dripping tap or an irrigation zone stuck open. - High flow for too long. A burst hose or pipe shows as a high
flow_lpmthat continues far longer than a shower or bath. Pick a threshold based on your normal peaks after a week of data.
The first pattern needs state over time and fits well in Node-RED or a short Python subscriber. The second is a simple threshold; in the ConnectMQ portal you can create a threshold alert rule on flow_lpm and see triggered alerts in the alerts log. For point detection under sinks and washing machines, combine this with dedicated sensors from the water leak detection guide; the meter tells you that water is flowing, the floor sensor tells you where.
Daily and Monthly Consumption
The ESP32 publishes a running total rather than daily figures, and that is deliberate: totals are easy to keep correct, and any subscriber can derive daily, weekly or monthly consumption by taking the difference between the first and last total in each period. Telegraf with InfluxDB, Home Assistant's utility meter helper or a small script writing to SQLite all work this way. Keep the device simple and do the calendar arithmetic downstream, where time zones and billing periods are easier to handle than on a microcontroller.
Accuracy and Reliability Checklist
- Compare against the dial weekly for the first month and adjust
LITRES_PER_PULSEor sensor position if the readings drift. - Check for double counting by drawing a measured 10 litres from a bucket; the count should increase by exactly 10 pulses at 1 L per pulse.
- Protect the sensor from condensation. Meter pits and cold-water pipes sweat; use a sealed sensor and keep the ESP32 in a dry enclosure.
- Mind Wi-Fi in basements. Weak signals cause reconnects; the retained telemetry and Last Will status make outages visible.
- Use your plumber or utility for anything involving the meter itself.
Moving to a Private Broker
Water usage reveals daily routines and when the house is empty, and anyone could publish to your set topic on a shared broker. Once the reader works, switch to a private broker with credentials and TLS. A managed MQTT broker such as ConnectMQ gives you that without hosting anything at home. Use WiFiClientSecure with the ISRG Root X1 certificate for Let's Encrypt, port 8883 and your credentials from the portal:
#include <WiFiClientSecure.h>
WiFiClientSecure net; // instead of WiFiClient
// setup(): net.setCACert(ISRG_ROOT_X1_PEM);
// mqtt.setServer("broker.connectmq.com", 8883);
// mqtt.connect("YOUR_CLIENT_ID", "YOUR_USERNAME", "YOUR_PASSWORD", topicStatus, 1, true, "offline");
Water consumption fits naturally beside your electricity data from a home energy monitor. For more automation ideas, see the IoT automation guide, and for hosting trade-offs, the MQTT broker in the cloud overview.
Track Your Water Usage with ConnectMQ
ConnectMQ gives your meter reader a private, TLS-secured MQTT broker with a live topic explorer and threshold alert rules for flow-based leak warnings. Create a free Serverless broker at app.connectmq.com, no credit card required, and have your first readings arriving within minutes.
Frequently Asked Questions
Can I read any water meter with MQTT?
Most mechanical meters can be read from the outside with a reed, inductive or optical sensor, or with a camera that reads the digits. Smart meters with wireless protocols need a matching receiver instead.
How many litres does one pulse represent?
It depends on the meter and sensor. A sensor on the litre dial typically gives one pulse per litre, while vendor pulse modules often give one pulse per 10 litres. Check against the dial to confirm.
Will the total be lost after a power cut?
Not with the sketch above. The total is saved to the ESP32's flash every few minutes when it changes, so at most a few minutes of pulses can be lost, and you can resynchronize with the set command.
How do I detect a slow leak?
Look for flow in every interval over a long period, such as two hours at night. Normal households have regular zero-flow periods, so continuous small flow is a strong leak indicator.



