Michał Jonik
2026-06-27

A magnetometer water meter: the hard part is keeping the count

My water meter has no pulse output, and I did not want to cut into the plumbing to add one. What it does have is a magnet that turns with its measuring mechanism. Every turn swings the magnetic field next to the meter from one sign to the other and back, and noticing that is what a magnetometer does.

So I taped a QMC5883L module to the meter body and wired it to an ESP32-CAM. The camera is unused; the board is simply a cheap ESP32 with free pins. Everything else is ESPHome configuration. The sensor is passive: it only measures the field and does not produce one, and the meter’s seals and housing stay untouched.

From a wobble to edges

The firmware reads one axis of the field every 50 ms. The raw value is noisy, so it goes through three filters before it counts anything:

field_strength_z:
  filters:
    - median: { window_size: 5, send_every: 1 }
    - delta: 20
    - lambda: |-
        if (x >  50) id(water_meter_edge).publish_state(true);
        if (x < -50) id(water_meter_edge).publish_state(false);
        return x;
    - throttle: 5s

The median drops single spikes. The delta filter ignores changes smaller than 20 µT. The lambda is the hysteresis: the edge goes high above +50 and low below −50, so a value hovering near one threshold cannot produce a stream of false edges. With a single threshold it would, and every false edge is water that was never used. The last filter only limits how often the field is sent to Home Assistant; the counter sees every sample that passes the first two.

Each edge, up or down, adds half a litre. Two edges make one turn, and on my meter one turn is one litre. On another model it can be a different volume, so check yours against the dial. That is the whole measurement, about a dozen lines of configuration. A device whose whole behaviour is declared in its configuration, with the firmware generated from it, is what I call Firmware-as-Config.

The count is the hard part

A counter that starts from zero after every reboot is useless. The total lives in a global variable with restore_value: yes, so it is read back from flash at boot. Writing to flash on every half litre would wear it out, so the device writes every 20 minutes. A clean reboot or a firmware upload saves the value first; a hard power cut loses whatever was counted since the last write. At my average flow that is a few litres.

Small losses add up, and the sensor can miss or double an edge, so the device also exposes an action that sets the total to a given value. I read the physical meter, send the number from Home Assistant, and counting continues from there.

The value also has to fit its type. I first stored it as a float, which has 24 bits of mantissa: somewhere around 8,400 m³ adding 0.5 stops changing the number. My meter has decades of margin, but the package uses a double. That fixes the stored total only. ESPHome publishes sensor values as float, so the entity in Home Assistant keeps the same limit; a count of half-litres in an integer would avoid both.

Home Assistant gets a sensor in litres with device_class: water and state_class: total_increasing, and the Energy dashboard accepts it as a water source without any templates. Over six weeks the house used roughly 400 litres a day. The trap in total_increasing is in the corrections. A small step down is logged as a warning and stays in the long-term statistics as a small negative step. A drop of more than 10 % counts as a meter reset: Home Assistant then adds the whole new value as consumption, so a typo in a correction can book the entire meter reading as one day of water. A correction downwards is safer done in the statistics themselves, under Developer tools.

A migration that must not lose a litre

ESPHome 2026.1 removed the old API password, and my devices had to move to an encryption key. For most of them that is a recompile. For the water meter, losing the count or the entity would break the history the Energy dashboard is built on.

The device name stayed the same, so the entity IDs did not change. In Home Assistant I used Reconfigure with the new key instead of removing the device and adding it again, which risked new entity IDs and a break in the statistics. Then I compared the total before and after the upload. The difference was half a litre, one edge between the two readings.

The universal version is in home-automation-snippets. Pins, thresholds, litres per edge and the flash interval are package parameters, and the correction action is part of it.

State that has to outlive the firmware

Any edge device that keeps state has the same problem, whether it is a counter, a PLC, an IoT gateway or an agent: its identity and data have to survive changes to the firmware and to the security mechanism around it. Keep the contract the consumers depend on (entity ID, unit, counter semantics) fixed. Migrate in place and re-authorise instead of recreating. Compare the numbers before and after, keep an explicit way to correct the state by hand, and decide on purpose how much data a power cut may cost.

esphomehome-assistantfirmware-as-config