Reading a 5/8" Neptune T-10 Water Meter with a QMC5883L

Hi all,

Here’s how I passively read my water meter. The water company already uses the reed switch terminals, and have it all sealed-up tamper-proof.

I’m using a cheap 3-axis magnetometer (digital compass) to read the rotating magnet that the nutating disc spins.


Here’s my dirty, annotated, but functional code:

substitutions:
  device_name: "esp8266-qmc5883l-water"
  device_label: "Digital Magnetometer Water"
  device_nickname: "Magnetometer"
  device_static_ip: 192.168.xx.xxx

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

esp8266:
  board: d1_mini
  restore_from_flash: true

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

preferences:
  flash_write_interval: 60min

# Enable Home Assistant API
api:
  encryption:
    key: !secret api_encryption_key

ota:
  password: !secret ota_password

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  manual_ip:
    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
  ap:
    ssid: "Water Meter Fallback"
    password: "your_password"

captive_portal:
    
# Creates the device page
web_server:
  port: 80

number:
  - 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'
    set_action:
      then:
        - lambda: |-
            int d;
            d = floor(x);
            id(water_counter_total) = d ;

# QMC5883L Sensor Configuration #
# https://esphome.io/components/sensor/qmc5883l.html

globals:
  - id: water_counter_total # increasing count
    type: long
    restore_value: yes
    initial_value: '0'
  - id: water_counter  # current count
    type: long
    restore_value: no
    initial_value: '0'
  - id: water_high # high/low threshold for count
    type: bool
    restore_value: no
    initial_value: 'false'

interval: # set both the high and low thresholds below for a trigger.  Y-Axiz range -69 to -121 when cycling slowly.  Using -90 to -100.
  - interval: 10ms # this is why the water meter was slipping, 250 ms was way too slow.
    then:
    - lambda: |-
       if (id(qmc5883l_y).state > -90 && !id(water_high)) {
          id(water_counter_total) += 1;
          id(water_counter) += 1; 
          id(water_high) = true;
          id(led).turn_on();
        } else if (id(qmc5883l_y).state < -100 && id(water_high)) {
          id(water_high) = false;
          id(led).turn_off();
        } 

i2c:
  sda: GPIO4 #D2, SDA
  scl: GPIO5 #D1, SCL
  frequency: 100kHz # Supports both 100kHz and 400kHz.  Fastest sensor reading is 200 Hz, so low-speed is just fine.
  # scan: true # Probably don't need to scan, the address is known at 0x0D
  id: bus_a # Others omitted this line?

sensor:
  - platform: qmc5883l
    address: 0x0D
#    field_strength_x:
#      name: "QMC5883L X-axis" # Not using.
#      id: qmc5883l_x
    field_strength_y:
      name: "QMC5883L Y-axis" # Using this one, hide others.
      id: qmc5883l_y
      internal: true # Change to false if you need to see readings.  This will log a ton of data.
#    field_strength_z:
#      name: "QMC5883L Z-axis" # Not using.
#      id: qmc5883l_z
  # heading:
   #  name: "qMC5883L Heading" # once I have the best X, Y, or Z sensor for reading the meter, I can comment out the other two sensors as well.
    oversampling: 128x # 512x (default), 256x, 128x, 64x (give other settings a try, see if it reduces noise)
    range: 200uT # 200 uT, 800 uT
    update_interval: 10ms # Make sure to also set calculation interval above too!

# 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:
# 200 Hz = 5ms
# 100 Hz = 10ms
# 50 Hz = 20ms
# 10 Hz = 100ms

# “maximum 75 pulses/second on 5/8” T10" (another source below claims 77 pulses/sec)
# The reed switch in the meter generates 2X pulses per rotation.
# My magnetometer generates 1X waveform per rotation.  I get half the pulses of a reed switch.
# 77 pulses per second /2 x 60 = 2310 rpm.
# 75 pulses per second /2 x 60 = 2250 rpm.

# Sampling a waveform should be 2X or more, so at least 77 Hz which is 12.98ms
# The next compatible HARDWARE sample rate above 77 Hz is 100 Hz or 10ms.

  - platform: template
    name: "Nutating Disc Count" # This works, doesn't miss a drop from low to high flows.
    lambda: |-
      float temp1 = id(water_counter_total);
      return temp1;
    update_interval: 1s
    state_class: 'total_increasing'
    accuracy_decimals: 0

  - platform: template
    name: "Nutating Disc RPM" # this seems to work, number is plausible.
    lambda: |-
      int temp2 = id(water_counter);
      id(water_counter) -= temp2;
      return temp2 * (6);
    update_interval: 10s
    unit_of_measurement: "rpm"
    accuracy_decimals: 0

  - platform: template
    name: "Water Flow L/min"
    lambda: |-
      int temp3 = id(water_counter);
      id(water_counter) -= temp3;
      return temp3 * (6 * 0.032736241);
    update_interval: 10s
    unit_of_measurement: "L/min"
    accuracy_decimals: 2

  - platform: template
    name: "Water Total (L)" # This works, doesn't miss a drop from low to high flows.
    lambda: |-
      return ((float)id(water_counter_total) * (0.032736241));
    update_interval: 10s
    unit_of_measurement: 'L'
    accuracy_decimals: 2
    state_class: 'total_increasing'
    device_class: 'water'

  - platform: template
    name: "Water Total (m³)" # This works, doesn't miss a drop from low to high flows.
    lambda: |-
      return ((float)id(water_counter_total) * (0.032736241 * 0.001));
    update_interval: 60s
    unit_of_measurement: 'm³'
    accuracy_decimals: 3
    state_class: 'total_increasing'
    device_class: 'water'    

# 5/8" Neptune T-10 Calibration:
# Source: https://www.riotronics.com/wp-content/uploads/2019/11/NT10-4P-WaterRead-pdf3.01.pdf
# Riotronics makes a device that fits between the meter head and the base, they say:
# 0.004324 gallons per pulse
# 231.24 pulses per gallon
# Max 20 gpm or 77 pulses/sec
# My sensor gets 1/2 the pulses of a reed switch.  See below.
# In metric:
# 0.032736241 L per rotation
# 30.54718459 rotations per litre
# 0.000032736 m3 per rotation

# A rotating magnet next to a reed switch results in 2X closes per revolution.
# When the magnet axis is parallel, the switch closes.
# When the magnet axis is perpendicular, the switch opens.
# Although the poles reverse, they still induce opposite poles in the reed switch.
# A reed switch is an omni-polar device.

  - platform: wifi_signal
    name: "${device_nickname} WiFi Signal"
    update_interval: 60s
    entity_category: diagnostic
    filters:
      - 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
    on_raw_value:
      then:
        - 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")
              ).c_str();
              
text_sensor:
  - platform: template
    name: "${device_nickname} Uptime Human Readable"
    id: uptime_human
    icon: mdi:clock-start
    entity_category: diagnostic

# LED flashes faster/slower as magnet rotates on water meter.

light:

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

switch:

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

Happy metering!

Thanks for posting you water meter specific values and notes. Since mine is in a pit, I can’t get a good view but visually it looks to be the same model (and I would assume that most residential meters are the same for a given geographic region; no need for the manufacturer to reinvent the wheel).

Your success got me to go ahead and actually finish testing mine out/install it. Don’t seem to have any issues reading the magnet. A quick calibration with a garden hose and bucket with gallon markings gave me similar values to what you found posted for your meter. Still need to do some tweaking to the install (and finish another project that will house the ESP since it’s way out by the street), but will be nice to have all utilities covered.

1 Like

Awesome. You helped me with my gas meter a lot. I’m very happy I could help you.

Thank you! I’m new to add these, could you please provide some step by step instructions like showing an 8 years old?

This might be above the level of an 8-yo, and unfortunately, you’ll need to tune the sensor to read your magnet thresholds.

This is how you wire the sensor to an ESPHome device:
HMC5883L Magnetometer — ESPHome

I’ve shown you where to position the sensor on my type of water meter, and you have the entire working code above.

Thanks. After looking at the codes, I figured it will take me hours to tune the sensor. I ended up buying a Flume 2.

Fair. Yes, it took some time figuring out where to place the sensor, which axis was capturing the greatest change, and what thresholds to use.

it actually didn’t take many hours to tune, it’s pretty simple, just flash the code provided by this post into esp controller, and attach sensor to water meter, open browser, you can read , x, y, z magnetic values right away, open your faucet, let water run slowly, you can get the high value and low value of y axis, then, set the throttle holds, done! you can now see the water value in home assistant. It’s much more accurate than AI on the edge camera based reading. It’s very accurate. I have both setups, I now plan to throw the ai on the edge thing into the garbage.

did you try your code on gas meters?

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.

1 Like