HomeGuides › Water Meter Monitoring

Water meter monitoring with Zabbix and AI on the Edge

Most water meters are still a dial behind a plastic window, and most providers still expect you to walk over, read it, and type the number into a form inside a three-day window once a month. The meter is not going to become digital. But a two-centimetre camera pointed at it can read the digits, publish them as JSON, and let Zabbix poll that endpoint like any other HTTP target.

This is the whole chain, end to end: what the hardware is, how to get the device reading digits reliably, what the Zabbix template actually collects, and the two failure modes that will stop your data arriving - both of which look like a broken template and are not.

What is actually doing the reading

The clever part is not Zabbix, and it is not the kit. It is AI-on-the-edge-device, an open-source firmware for the ESP32-CAM that runs a TensorFlow Lite digit-recognition model on the microcontroller itself. No cloud, no companion server, no image ever leaving your network. The board takes a photo, crops the regions you told it the digits live in, classifies each one, assembles a number, sanity-checks it against the previous reading, and serves the result on a small HTTP API.

It works on anything with a numeric dial - water, gas, electricity - and it is a mature project rather than a weekend experiment: 8,600 stars and a current release of 16.1.0. What the initMAX Monitoring AI Kit 1.0 adds is packaging: the boards, a pre-flashed SD card, the mount, and - the part that matters here - a maintained Zabbix template. You can build the same thing from parts for well under ten euros if you would rather.

What is in the box

Part What it is for
ESP32-CAM moduleThe camera and the processor. Ships with the lens glued for transport.
ESP32-CAM-MB boardCarrier board. Supplies power over USB and doubles as the serial console for flashing.
micro SD cardPre-loaded base image: firmware, digit models, web UI, config.
USB-C cable, USB-A adapterPower and data.
Lens toolSmall plastic key for turning the lens to focus. You will need it first.
Coloured insertMount. The hole in the top is where the camera looks through.

The two boards press together pin-for-pin and only fit one way round. Seat them fully - a partially inserted header is the most common reason the device never appears on the serial port.

Step 1: put it on your Wi-Fi before anything else

The SD card ships configured for the SSID of a conference room at a Zabbix Summit workshop, which is unlikely to be your network. Pull the card out, put it in your machine, and edit wlan.ini in the root:

[WLAN]
SSID = "your-network"
PASSWORD = "your-password"

The five-minute trap. The card ships inside a full-size SD adapter with the write-protect switch in the locked position. Your editor will appear to save the file and the device will keep looking for the conference Wi-Fi. Slide the switch before you edit, not after.

The device is 2.4 GHz only, like most ESP32 hardware. If your access point publishes one SSID across both bands and steers clients, that usually still works, but a band-locked guest network will not.

Step 2: connect the console and find the IP

Everything else happens over the network, so you need the address the device picked up. The quickest route is the project's browser-based installer and console at jomjol.github.io/AI-on-the-edge-device, which speaks Web Serial to the board over USB. Plug the board in, click Connect, and pick the serial port.

If no port is listed, the driver is missing rather than the board being dead. These carrier boards use a CH340C USB-to-serial chip, which Windows and macOS do not ship a driver for. The installer offers the download when the port list comes back empty; install the CH340 driver for your OS, replug, and try again. Web Serial also means Chrome or Edge - Firefox and Safari do not implement it.

With the SD card already flashed there is nothing to install. Open the console view and watch the boot log: the device prints the address it got from DHCP. Give that address a DHCP reservation now, while you are thinking about it. The Zabbix template addresses the device by IP, so a lease change is a silent outage three months from now.

Step 3: three settings, before the wizard

Go to http://<device-ip>/editconfig.html. The configuration UI is served off the SD card by a microcontroller and it is slow - clicks take a second to register. That is normal, and clicking again makes it worse.

Section Parameter Default Set to
1.1 TakeImage LEDIntensity 50 see below
1.5 PostProcessing DecimalShift 0 -3
1.11 AutoTimer Interval 5 min 1 min

Parameter names, sections and defaults from the AI-on-the-edge-device parameter reference.

LED intensity is not a "set it to zero" setting

Workshop instructions for this kit tell you to set LEDIntensity to 0, and that is correct for the workshop, where the practice target is a video of a meter playing on a phone screen and the flash blows the image out with glare. On a real meter in a dark cupboard behind a glass cover you want the opposite: enough light to expose the digits, low enough not to reflect off the glass back into the lens. Start at the default of 50 and adjust while watching the reference image. This is one setting where copying the tutorial exactly is the wrong move.

Why the decimal shift is negative

The device does not know where the decimal point is. It reads each digit region you define, concatenates them into one integer, and then DecimalShift tells it how far to move the point. A typical European water meter has black digits for whole cubic metres and three red digits for thousandths. If your regions cover both - and they should, because that is the precision you are paying for - the device reads 103472 and you want 103.472. Three places left, so -3.

The documentation's own example runs the other way: +3 converts cubic metres to litres. So the rule is not "always -3". Count the digit regions you defined that sit to the right of the real decimal point, and make the shift negative that many places.

The two intervals are independent

Interval under AutoTimer controls how often the device takes a photo and runs a recognition round. The Zabbix template polls every 2 minutes. These do not need to match and neither one is the sampling rate you get. If the device runs at 1 minute and Zabbix polls at 2, you keep every second reading, which for a water meter is more than enough. Setting the device faster than the poll only heats it up.

Step 4: focus and the digit regions

Now the setup wizard at http://<device-ip>/setup.html. It walks through alignment in order, and the order matters, because every later step depends on the image being sharp.

  1. Focus. Turn the lens with the supplied plastic tool. It is glued for shipping and needs a firm first turn to break loose. Judge it on the digits, not the meter body.
  2. Exposure. LED intensity, brightness, contrast and saturation, with a live reference image. You are aiming for digits that a person could read at a glance, not a pretty photo.
  3. Rotation. Straighten the image so the digits sit on a horizontal line. The model expects upright digits and a few degrees of tilt costs you accuracy on the rolling ones.
  4. Regions of interest. Drag one box per digit, snug around each numeral. This is the step worth being slow and fussy about - every misread later traces back to a lazy box here.

A meter's least significant digit is usually a continuously rolling drum rather than a clean digit, so it will spend most of its life halfway between two numerals. That is expected. The firmware handles it, and it is also why the consistency checks in the next section exist.

Step 5: read the endpoint yourself before involving Zabbix

Do not skip this. If /json does not look right from the command line, no amount of Zabbix configuration will help, and you will spend the next hour debugging the wrong layer.

curl http://<device-ip>/json
{
  "main": {
    "value": "521.17108",
    "raw": "521.17108",
    "pre": "521.17108",
    "error": "no error",
    "rate": "0.023780",
    "timestamp": "2023-01-13T16:00:42+0100"
  }
}
Field Meaning
rawWhat the model read this round, before consistency checks.
valueThe reading after post-processing and plausibility checks.
preThe last value accepted as valid. The reference for the next check.
errorno error, or a description of why this round was rejected.
rateChange since the previous reading.
timestampWhen the round ran.

Structure from the AI-on-the-edge-device REST API documentation. The object is keyed by meter name - main here - and a device configured with several number sequences returns one object each.

Check the number against the physical dial before going further. If they disagree, the problem is a region of interest or the decimal shift, and it is much easier to fix now.

Step 6: the Zabbix side

The template lives in initMAX's public template repository. Two variants exist - Meter_ESP32_by_HTTP and Meter_ESP32_by_MQTT. The HTTP one needs nothing but network reachability, so start there.

Requires Zabbix 7.0 or newer. The published template is exported at format version 7.0 and will not import into 6.4 or 6.0 without editing. The template is vendored as initMAX s.r.o., version 7.0-1.

  1. Download Template_Meter_ESP32_by_HTTP.yaml. Note the default branch is production/free, not main.
  2. In Zabbix, Data collection → Templates → Import, choose the file, import. It lands in the template group Templates/initMAX.
  3. Create a host - Data collection → Hosts → Create host. Name it for the meter. It needs a host group, but no interface: an HTTP agent item carries its own URL.
  4. Link the template Meter ESP32 by HTTP.
  5. Set the macro {$ESP32.IP} on the host to the device's IP address, with no scheme and no path - just 192.168.1.30.
  6. Open Latest data, find Meter HTTP RAW, and use Execute now rather than waiting out the interval.

What the template collects

Item Key Type Interval
Meter HTTP RAW meter HTTP agent, text 2m
Meter HTTP Status meter.status Dependent, float, m³ on master
Meter HTTP stream m3 / s meter.stream Dependent, float on master
Meter HTTP - daily consumption meter.daily Calculated, float, m³ 0;h0

Item names, keys and intervals read from the published template rather than transcribed from the screen, so they match what you will import.

One HTTP agent item does the fetching and everything else hangs off it. That is the right shape: the device is a small microcontroller and you do not want four items independently waking it up.

The error gate, which is the part worth understanding

meter.status runs two preprocessing steps in order. First a JavaScript step:

data = JSON.parse(value);
if ( data.main.error == "no error" ) return value;
else return data.main.error;

If the device reported a clean round, the whole JSON document passes through untouched. If it did not, the step returns the error string instead. The second step is a JSONPath query for $.main.raw with the error handler set to discard value. That query cannot match a bare string, so it fails, and the failure discards the sample rather than storing garbage.

The effect is a clean gate: bad rounds leave a gap in the history instead of a wrong number. It also explains a symptom that reads like a broken template - if the device is reporting an error, meter.status stays empty, and because meter.stream and meter.daily both depend on it, the whole host looks dead. One upstream error, three silent items. Always check main.error at the device before suspecting Zabbix.

Note also that the item stores raw, not value - the reading before the firmware's own plausibility correction. Combined with the error gate that is a deliberate choice: let the device decide whether the round was valid at all, but keep the unmassaged number when it was.

The trigger is a leak detector

monoinc(/Meter ESP32 by HTTP/meter.status,#25,"strict")=1

"Continuous consumption detected", high severity. It fires when the last 25 readings were strictly increasing, every single one. At a 2-minute poll that is roughly 48 minutes without the meter ever standing still.

Normal household use does not look like that. Showers and washing machines produce bursts with flat stretches between them, and any flat stretch resets it. Water moving continuously for the better part of an hour is a running toilet, a garden tap left on, or a leak. It is a genuinely well-chosen trigger, and it is the reason to put this on a meter rather than just reading it once a month - the monthly number tells you what you spent, this tells you tonight.

The two things that will break it

Daily consumption is unsupported for the first day, and that is correct

meter.daily is a calculated item:

last(//meter.status,#1) - last(//meter.status,#1:now-1d)

Today's reading minus the reading from 24 hours ago. On a host you built ten minutes ago there is no value from 24 hours ago, so it goes unsupported. Nothing is wrong. Leave it alone and it starts working on its own.

Its interval is 0;h0 - no regular interval, plus a scheduling interval that fires at midnight. So it produces one value a day at 00:00, and this is not something Execute now will usefully test. Expect the first number the morning after the second midnight.

A negative rate poisons the whole chain

This is the failure you will actually hit. The firmware sanity-checks each reading against the previous accepted value, and a water meter only counts up - so if the new reading is lower than the stored previous value, the firmware rejects the round and puts a negative-rate error in main.error. The Zabbix error gate does its job, discards the sample, and every item on the host goes quiet.

It happens more often than you would think: a misread on a rolling digit, a bumped camera, or a device that rebooted and picked up a stale previous value. The stored previous value is now ahead of reality, and it will never recover on its own, because every subsequent real reading is also "too low".

The fix is to tell the device to trust what it can currently see. In the web UI it is the set previous value control. Over HTTP it is one request:

# adopt the current raw reading as the previous value
curl "http://<device-ip>/setPreValue?numbers=main&value=-1"

# or set it explicitly, if you know the true reading
curl "http://<device-ip>/setPreValue?numbers=main&value=1234.5678"

The negative value=-1 is a flag, not a value: it means "take the raw reading". Because this is a plain GET, it can be automated - a Zabbix script item or a remote command on the trigger for the meter going stale, if you would rather not fix it by hand at midnight. Related: PreValueUse in section 1.5 controls the check itself, and PreValueAgeStartup (default 720 minutes) decides how long a stored previous value stays trusted across a reboot. Shortening the latter makes reboot-induced versions of this problem expire by themselves.

Two smaller things worth knowing

Watch the walkthrough

Fifteen minutes: unboxing the kit, assembling the boards, the config screens in the order above, aligning the digit regions against a live meter, and linking the template in Zabbix until the first value lands.

Names on this page come from the published template, not the video. The template is Meter ESP32 by HTTP and the macro is {$ESP32.IP}. The device parameters are LEDIntensity, DecimalShift and Interval, in sections 1.1, 1.5 and 1.11. Use these when searching either UI - they are what you will actually see.

Related tools

Monitoring something that was never meant to be monitored?

Meters, sensors, machines with a serial port and a manual from 1998 - most estates have something that matters and has no API. Usually there is a way in, and usually it is less work than people expect. Tell me what the device is and what you need out of it, and I will tell you how I would approach it.