Attempting to read an Elster ACM-250 dumb gas meter with a QMC5883L

So these things have no magnets on the dials or numbers, that much was a bust.

But I experimented holding the sensor all around the body of the meter while it was running and three spots had a sinusoidal signal that correlated with the rhythmic sound of the bellows. Spots A, B and C all look promising.

Weirdly the strongest signals are around +25 to -10µT (35 range) with only a few spots tested. My meter faces West, I had presumed it was the Z-axis looking inside the case, but it was actually the X-axis, facing North, parallel to the case that had a signal. I don’t really get it.

Magnetic North’s strength ranges from 0.25 to 0.65 gauss or 25µT to 65µT, I get about 50µT here from the North, and it was reading around 35µT from the north pressed against the case.

It can’t really be a reflection of magnetic North, as there is an actual zero-crossing, where it switches poles. The way the sensor works, is whichever axis has the largest positive number, is facing north. Between 3-axis, you can find North regardless of the orientation of the sensor. This is how the compass in your phone works.

I don’t understand what magnetic field I am reading to be honest, but it is a signal that correlates with the internal bellow noises, has a zero-crossing and is well within the specs of the hall-effect sensor (0.73 to 800µT, with up to 2µT noise).

I think this is a useable signal. Read the meter, count the pulses, read the meter again. Figure out the cubic meter/pulse relationship.

Here’s what I did for mine:

I’ve had one issue where the sensor just died after a few months. No evidence of moisture, it just started giving nonsensical readings. Swapped it out and it’s worked exactly the same as before.

I selected the axis that had the largest difference between peaks (one of the other’s I think had a ‘prettier’ sinusoid), specifically to make it easier to detect cycles and avoid noise in the data potentially causing issues.

1 Like

Brilliant! I guess I should have searched the forums a little more before digging into this myself.

I love your idea of putting it in roughly the same place, but on the rear of the meter. No reason to tip-off the meter reader that I’m doing things.

I currently have two QMC5883L Magnetometer devices, as I plan on trying to read the water meter as well. Based on one just dying on you, I’ll be sure to order a spare now so I have one should one die early.

Since they’re cheap, and I’ll have a spare, I suspect I’ll just stick mine in a piece of heat-shrink tubing, fold over the top, stick another piece of heat shrink over that one and call it a day. Then tape or double-sided tape it to the back of the meter.

Have you had luck with this yet? I’ve considered doing the same on my water meter, but mine is in a pit (so it’s hard to access and by the road) and when I tried to use my phone magnetometer, I got pretty inconsistent results which kind of deflated my enthusiasm to keep testing stuff out since I wasn’t confident it was even going to really work.

No, I haven’t started yet. The sensors just arrived yesterday, and I played with the gas meter first.

My water meter is a Neptune T-10 and the city has a transmitter attached to the wires under the cover (should look like the picture) which is sealed shut with an anti-tamper wire. I won’t mess with it.

But based on the replacement plastic bayonet meter heads, it HAS to be magnetic.

Interesting. Mine unfortunately has a digital readout (but requires solar power to display and then cycles through different values, so a camera isn’t really an option). I THINK it’s all an integrated setup as opposed to a retrofit as yours looks like (they redid the water lines relatively recently and the meter looks fairly new).

Let me know how it goes! If you find a good location for the magnetometer to pickup rotations, that will inspire me to spend some more time on it.

So a quick update on the outcome, this turned out fantastically well. The sensor test on the front of the meter went great.

I was able to see the internal bellows of the meter quite clearly, both rapid motions when gas was being used, and very slowly when just the hot water tank pilot light was on.

For the more permanent install, I covered the sensor in heat-shrink tubing, folded the top over, slipped another piece over that, then stuck it to the meter with double-sided foam tape, and covered it with a small piece of Gorilla tape.

You can’t really see it from the front of the meter, and it’s 100% non-invasive and passively reading the meter internals.

Many thanks to @brooksben11 for sharing his code, it helped speed me along.

My initial calibration attempt based on just the 0.01 m3 dial went better than planned, I got so close with the initial calibration.

I plan to let both counters wind on some m3 for further adjustments later.

When I’m happy with the bellow counts vs. meter total accuracy, I’ll share the updated code with everyone!

I’m struggling with getting readings from my meter, the AC-250 which appears identical to yours. Ordered some magnetometers today in hopes that will work for me as well.

As for the Neptune, these use the r900 protocol. Get an RTL-SDR and run rtlamr software, it should be able to pick up your meter readings wirelessly. I’ve currently been working with a local government to use an SDR to pick up the readings of a few meters in the community, because Neptune wants to charge an arm and a leg for their reader. :joy:

1 Like

I was actually just logging in to create a post about automating the water meter with the very same magnetometer. It works!

The issue with intercepting the water meter broadcast, is my utility transmits 4X a day and it only says if you used the next 0.5 m3 (500L) increment or not. So 0-499L used is 0. 500-999L is a 0.5 m3 reading increment.

The utility has a web portal that I scrape. It only updates every 24 hours, but then the past 4 readings are there. I’ll know soon if the timestamp is accurate.

This solution gives me real-time volume and flow data.

Awesome! Where exactly did you place the sensor on the water meter?

1 Like

Here’s where I got the best signal. The magnet is in the centre raised section of the base, as you can see in other pictures I found on the web.

For those that wish to give this a whirl, here’s my dirty annotated, but funtional code:

  device_name: "esp8266-qmc5883l-gas"
  device_label: "Digital Magnetometer Gas"
  device_nickname: "Gas Meter"

  name: ${device_name}
  friendly_name: ${device_label}

  board: d1_mini
  restore_from_flash: true

# Enable logging
logger: # One example disabled the logger and is using hardware UART2
  baud_rate: 0

  flash_write_interval: 60min

# Enable Home Assistant API
    key: !secret api_encryption_key

  password: !secret ota_password

  ssid: !secret wifi_ssid
  password: !secret wifi_password

    static_ip: ${device_static_ip}
    gateway: !secret gateway
    subnet: !secret subnet
    dns1: !secret dns1

# Turn Off Power Save Mode
 #power_save_mode: none # none is already the default for an ESP8266

  # Enable fallback hotspot (captive portal) in case wifi connection fails
    ssid: "Magnetometer Fallback"
    password: "password_you_like"

# Creates the device page
  port: 80

  - platform: template
    name: "${device_nickname} Set Reading"
    optimistic: True
    min_value: 0
    max_value: 99999
    step: 1
    id: act_reading
    entity_category: config
    mode: box
    icon: 'mdi:counter'
        - lambda: |-
            int d;
            d = floor(x);
            id(gas_counter_total) = d ;

# QMC5883L Sensor Configuration #

  - id: gas_counter_total # increasing count
    type: long
    restore_value: yes
    initial_value: '0'
  - id: gas_counter  # current count
    type: long
    restore_value: no
    initial_value: '0'
  - id: gas_high # high/low threshold for count
    type: bool
    restore_value: no
    initial_value: 'false'

interval: # set both the high and low thresholds here for a trigger.  Using 3 and -15 here.
  - interval: 100ms
    - lambda: |-
       if (id(qmc5883l_x).state > -20 && !id(gas_high)) {
          id(gas_counter_total) += 1;
          id(gas_counter) += 1;	
          id(gas_high) = true;
        } else if (id(qmc5883l_x).state < -30 && id(gas_high)) {
          id(gas_high) = false;

  sda: GPIO4 #D2
  scl: GPIO5 #D1
  # scan: true # Probably don't need to scan, the address is known at 0x0D
  id: bus_a # Others omitted this line?

  - platform: qmc5883l
    address: 0x0D
      name: "QMC5883L X-axis"
      id: qmc5883l_x
      internal: true # Set to false when you need to see the field strength.  This will log a ton of data.
     #name: "QMC5883L Field Strength Y" # Find best signal, hide others.
     #id: qmc5883l_y
     #name: "QMC5883L Field Strength Z" # Find best signal, hide others.
     #id: qmc5883l_z
  # heading:
   #  name: "qMC5883L Heading"
    oversampling: 512x # 512x (default), 256x, 128x, 64x
    range: 200uT # 200 uT, 800 uT
    update_interval: 100ms # Slowest hardware rate from below.

# Oversampling rate is tied to refresh:
# 512x = 10 Hz
# 256x = 50 Hz
# 128x = 100 Hz
# 64x = 200 Hz

# QMC5883L has a maximum output of 200 Hz.
# Can read at 10, 50, 100 or 200 Hz:
# 10 Hz = 100ms
# 50 Hz = 20ms
# 100 Hz = 10ms
# 200 Hz = 5ms

# An American meter is 0.111 ft³ per revolution.  That would be divide by 9.009009 recurring. 
# See below for metric conversions.

  - platform: template
    name: "Gas Bellow Count"
    lambda: |-
      float temp1 = id(gas_counter_total);
      return temp1;
    update_interval: 5s # tweak this once I figure out the max rate of gas my house can use.
    state_class: 'total_increasing'
    device_class: 'gas'
    accuracy_decimals: 0

  - platform: template
    name: "Gas Bellow Count/min"
    lambda: |-
      int temp2 = id(gas_counter);
      id(gas_counter) -= temp2;
      return temp2 * (1);
    update_interval: 1min
    unit_of_measurement: "cpm"
    accuracy_decimals: 1

  - platform: template
    name: "Gas Flow L/min"
    lambda: |-
      int temp3 = id(gas_counter);
      id(gas_counter) -= temp3;
      return temp3 * (3.143);
    update_interval: 30min # Needs to be quite high to capture pilot light use if I care.
    unit_of_measurement: "L/min"
    accuracy_decimals: 2

# From above, the bellows are 0.111 ft³.
# For a metric meter, each impulse is 0.03143 m³ (imperial conversion) or 0.00314 m³ if we take the value on the meter which is probably rounded.
# I'm going to assume the USA and Canada meters use the same bellows, but the gearing for the meter face is different.
# 0.111, 0.222 all the way to 0.999 (probably 0.999999 recurring) is a common weights and measures thing so you're never over-charged.

  - platform: template
    name: "Gas Total"
    lambda: |-
      return ((float)id(gas_counter_total) * (3.143));
    update_interval: 60s # No point reporting faster than the bellow cycle count.
    unit_of_measurement: 'L'
    accuracy_decimals: 2
    state_class: 'total_increasing'
    device_class: 'gas'

  - platform: template
    name: "Gas Total (m³)"
    lambda: |-
      return ((float)id(gas_counter_total) * (0.003143));
    update_interval: 60s # No point reporting faster than the bellow cycle count.
    unit_of_measurement: 'm³'
    accuracy_decimals: 3
    state_class: 'total_increasing'
    device_class: 'gas'    

  - platform: wifi_signal
    name: "${device_nickname} WiFi Signal"
    update_interval: 60s
    entity_category: diagnostic
      - sliding_window_moving_average:
          window_size: 6
          send_every: 6

  - platform: uptime
    name: "${device_nickname} Uptime"
    id: uptime_sensor
    update_interval: 60s
    entity_category: diagnostic
        - text_sensor.template.publish:
            id: uptime_human
            state: !lambda |-
              int seconds = round(id(uptime_sensor).raw_state);
              int days = seconds / (24 * 3600);
              seconds = seconds % (24 * 3600);
              int hours = seconds / 3600;
              seconds = seconds % 3600;
              int minutes = seconds /  60;
              seconds = seconds % 60;
              return (
                (days ? to_string(days) + "d " : "") +
                (hours ? to_string(hours) + "h " : "") +
                (minutes ? to_string(minutes) + "m " : "") +
                (to_string(seconds) + "s")

  - platform: template
    name: "${device_nickname} Uptime Human Readable"
    id: uptime_human
    icon: mdi:clock-start
    entity_category: diagnostic

# LED flashes faster/slower as interval meter on gas diaphragm signal.


  - platform: status_led
    name: "On Board LED"
      number: GPIO2
      inverted: true


  - platform: gpio
    id: led
      number: GPIO2
      inverted: true
      mode: OUTPUT
    restore_mode: ALWAYS_OFF

# Explore adding temperature compensation calculation.
# my meter (Canada) is definately temperature compensated.
# Natural gas has an approx. 1% change in volume for every 5F temperature change.

# VA (Actual Volume)
# VS (Volume at Standard Conditions)

# P1 V1 = P2 V2
# or

# P1 = V2
# P2   V1

# V1 = V2
# T1   T2
# or
# V1 T2 = V2 T1

# FT = 460 + Base Temperature in F (Mine is 15C or 59F)
#      460 + Line Temperature in F (Current temp in F)

# My meter specs #

# Make Elster American Meter (now Honeywell Elster)
# Model ACM250
# Max Operating Pressure: 35 kPa
# 3.14 dm³/Rev. (This could be valuable insight into the bellows volume)
# 3.14 dm³ is 0.00314 m³

# an American AC250 meter appears to say .111 Rev. (cubic feet per revolution)
# 3.14 dm³
# Temp. Comp. at 15C base (USA meters use 60F as a base)
# 3.14 dm³ = 0.1109 ft³ (pretty damn close, huh?)

# I wonder which is more accurate, the metric or imperial label?
# 0.111 ft³ is 3.143 dm³ or 0.003143 m³

# One revolution should be 0.00314 m³
# bellows signal should be one revolution from an animated gif I saw (don't necessarily rely on this)

# therefore the 0.1 m³ dial should be 31.847 bellow counts (0.1/0.00314)
# the 0.5 m³ dial should be 159.24 bellow counts (0.5/0.00314)
# 1 m³ number should be 318.471 bellow counts (1/0.00314)

Happy metering!

1 Like

Here’s the write-up for the water meter:

Interesting, my magnetometer arrived and I slapped it onto the side of my AC250 meter, I don’t know if I’m reading my meter right but a full bellows revolution is exactly 1ft3? I’ve got a dial for quarter cubic feet on the meter and it goes through 4 ticks while the magnetometer shows a full revolution.

Noticed the metal plate on my meter shows 0.111ft3/rev on it, matching the code you posted. Is this a full revolution of the bellows, or the drive that would attach to the index? Going with 1ft3 per diaphragm expansion and contraction seems to line up with my meters index correctly. I know your meter is in M3, but it’s essentially the same meter, with a different index. Does your reading in HA line up with what your meter actually shows?

EDIT: My bad, you’re totally right, I was misinterpreting my gauges on the meter, did not realize that one full rotation was equal to the unit specified. I had thought that one line was equal. Thanks to all who have been contributing to this.

I had requested the gas company come and install a pulser on my meter, but this is actually way more detailed than having a pulse sent every cubic foot!

1 Like

Sounds like you figured it out. The bellows are 0.111 per cycle. Imperial/metric meters are simply different geared displays connected to the rotating output.

Technically, the temperature compensated ones are 0.111 cu.ft. at the rated temperature, so I may wish to create a sensor that adjusts the volume based on the exterior temperature. When it’s brutally cold out, I can better observe how much my sensor is under-reporting.

1 Like

FYI, GitHub - tronikos/esphome-magnetometer-water-gas-meter: Using ESP8266 or ESP32 and QMC5883L, a triple-axis magnetometer, to read your water meter or gas meter supports both water and gas meters. As a bonus it exposes the temperature sensor that the QMC5883L has. I recently added support for that in Add temperature for QMC5883L by tronikos · Pull Request #6456 · esphome/esphome · GitHub so you will need esphome version of at least 2024.4.0.