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.
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.
| Part | What it is for |
|---|---|
| ESP32-CAM module | The camera and the processor. Ships with the lens glued for transport. |
| ESP32-CAM-MB board | Carrier board. Supplies power over USB and doubles as the serial console for flashing. |
| micro SD card | Pre-loaded base image: firmware, digit models, web UI, config. |
| USB-C cable, USB-A adapter | Power and data. |
| Lens tool | Small plastic key for turning the lens to focus. You will need it first. |
| Coloured insert | Mount. 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.
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.
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.
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.
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.
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.
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.
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.
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.
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 |
|---|---|
raw | What the model read this round, before consistency checks. |
value | The reading after post-processing and plausibility checks. |
pre | The last value accepted as valid. The reference for the next check. |
error | no error, or a description of why this round was rejected. |
rate | Change since the previous reading. |
timestamp | When 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.
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.
Template_Meter_ESP32_by_HTTP.yaml.
Note the default branch is production/free, not main.Templates/initMAX.{$ESP32.IP} on the host to the device's IP address, with
no scheme and no path - just 192.168.1.30.| 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.
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.
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.
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.
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.
http:/{$ESP32.IP}/json with one slash. libcurl
normalises it, so it works, and it is not worth "fixing" a vendored template over.
But if you copy that item as the basis for something else, correct it.Meter_ESP32_by_MQTT collects from there instead.
That inverts the direction - device pushes, no polling - which is the better shape
if the meter sits behind a flaky link or you want sub-minute resolution without
hammering it.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.
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.