Pulse counter overcounting

Hello everyone,

I am trying to get the gas values in real time, I am using the following hardware: ESP8266 d1 mini, with contact reed . if the mechanical gas counter shows 1.005 m³ ≈ 100 pulses , the reed contact is registering 1.470 m³ ≈ 147 pulses (≈ +46 %). I tried to place the contact reed further but I was able to get to 28% error and I set the pulse to 4000 ms. Any idea, if I can reach my goal with this reed contact or is it the code that is not adequate? Thank in advance for the help.
My gas meter is a Dresser G4 NPL12/110 EI (1 imp = 0.01 m³, Qmax = 6 m³/h), I am using the following code for the pulse counter.

substitutions:
  devicename: gas-meter
  # Dresser G4 label: 1 imp = 10 dm³ = 0.01 m³
  m3_per_pulse: "0.01"

  # Start at 1500 ms; raise to 2000–3000 ms if you still see occasional double counts
  min_pulse_gap_ms: "5500"

esphome:
  name: ${devicename}

esp8266:
  board: d1_mini

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

logger:
  level: DEBUG   # VERY_VERBOSE only for short tests

api:

ota:
  platform: esphome

preferences:
  flash_write_interval: 60s

# --- Debug helpers (no extra pin ownership) ---
globals:
  - id: last_accept_ms
    type: uint32_t
    restore_value: no
    initial_value: '0'
  - id: accepted_no
    type: uint32_t
    restore_value: no
    initial_value: '0'

sensor:
  - platform: pulse_meter
    id: gas_flow_pulse
    pin:
      number: D1          # GPIO5
      mode:
        input: true
        pullup: true
      inverted: true
    name: "Gas Flow"
    unit_of_measurement: "m³/h"
    device_class: gas
    state_class: measurement
    accuracy_decimals: 3

    # KEY: merge/ignore pulses closer than the min gap (bounce / double-edge)
    internal_filter: ${min_pulse_gap_ms}ms

    # Low flows: don't drop to zero too quickly
    timeout: 120s

    # pulses/min  ->  m³/h
    filters:
      - multiply: !lambda "return (${m3_per_pulse}) * 60.0;"

    # Log each ACCEPTED pulse timing (debug; safe to keep or remove later)
    on_value:
      then:
        - lambda: |-
            const uint32_t now = millis();
            const uint32_t dt  = (id(last_accept_ms) == 0) ? 0 : (now - id(last_accept_ms));
            id(last_accept_ms) = now;
            id(accepted_no)++;
            // x is the current sensor state (m³/h) after this accepted pulse
            ESP_LOGI("pulse", "ACCEPTED #%u  gap=%u ms (%.3f s), flow=%.2f m3/h",
                     id(accepted_no), dt, dt/1000.0, x);

    total:
      name: "Gas Total"
      unit_of_measurement: "m³"
      device_class: gas
      state_class: total_increasing
      accuracy_decimals: 3
      filters:
        - multiply: ${m3_per_pulse}

  # Optional health metrics
  - platform: wifi_signal
    name: "Wi-Fi Signal Strength"
    update_interval: 3600s
  - platform: uptime
    name: "Device Uptime"
    update_interval: 3600s

switch:
  - platform: restart
    name: "Restart ${devicename}"

Have you tried fiddling with the internal_filter: values? What you are seeing sounds like debounce issues - that setting is to help cope with debounce.

  • internal_filter (Optional, Time): If a pulse shorter than this time is detected, it is discarded. Defaults to 13us.This acts as a debounce filter to eliminate input noise, so choose a value a little less than your expected minimum pulse width.
  • internal_filter_mode (Optional, string): Determines how the internal filter is applied. One of EDGE or PULSE. Defaults to EDGE.
    • In EDGE mode, subsequent rising edges are compared and if they fall into an interval lesser than the internal filter value, the last one is discarded. This is useful if your input signal bounces, but is otherwise clean.
    • In PULSE mode, the rising edge is discarded if any further interrupts are detected before the internal_filter time has passed. In other words, a high pulse must be at least internal_filter long to be counted. This is useful if you have a noisy input signal that may have bounces before and/or after the main pulse.

No I did not try this, would this approach be enough?

sensor:
  - platform: pulse_counter
    pin:
      number: GPIO33           
      mode:
        input: true
        pullup: true           #  for reed switch
    name: "Gas pulses"
    internal_filter: 5ms       # cos 13 µs is far too short for mechanical bounce (often 1–10 ms)
    internal_filter_mode: PULSE
    update_interval: 10s
    count_mode:
      rising_edge: INCREMENT
      falling_edge: DISABLE
    accuracy_decimals: 0

It’s a start. Try various values until you get the result that matches the meter. 5ms is a longish time - make sure the reed switch is always closed for at least that long each pulse.

There is a little known “gotcha” that trips a lot of people up when using the pulse counter.

The pulse counter updates every minute by default, but…

The first reading after a restart is offset by some random amount of time (so multiple sensors aren’t all time synchronised and don’t spam the cpu at once).

Try waiting for a few updates (minutes) after a restart before taking your calibration measurement. This confused the heck out of me when setting up my rain gauge pulse counter with a 15 minute update interval. I had to wait >15 minutes after a restart then do the calibration test.

1 Like

Thank you @tom_l for the tip, I will keep it in mind when I do the next update.
Ok @zoogara , will start doing this. Just waiting to be able to physically take pictures and test.

Do you think replacing the reed by a bipolar hall sensor US1881 would solve the problem and provide better accuracy?

No idea sorry. I would think it would solve the debounce issue but I have no experience with hall sensors.

You could use external pullup, 10k for example.
Also, if your Qmax is 6m3/h the shortest possible pulse interval is 6s. That gives you plenty of room to play with the internal_filter…

Thank you everyone for chipping in, it worked as recommended by @zoogara . Here is the clean code if anyone needs it.

substitutions:
  devicename: gas-meter
  m3_per_pulse: "0.01"
  pulse_debounce_ms: "10"

esphome:
  name: ${devicename}

esp8266:
  board: d1_mini

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

logger:
  level: INFO        # change from DEBUG to INFO for quiet operation

api:
ota:
  platform: esphome

preferences:
  flash_write_interval: 60s

sensor:
  - platform: pulse_meter
    pin:
      number: D1
      mode:
        input: true
        pullup: true
      inverted: true
    name: "Gas Flow"
    unit_of_measurement: "m³/h"
    device_class: gas
    state_class: measurement
    accuracy_decimals: 3
    internal_filter: ${pulse_debounce_ms}ms
    internal_filter_mode: PULSE
    timeout: 120s
    filters:
      - multiply: !lambda "return (${m3_per_pulse}) * 60.0;"

    total:
      name: "Gas Total"
      unit_of_measurement: "m³"
      device_class: gas
      state_class: total_increasing
      accuracy_decimals: 3
      filters:
        - multiply: ${m3_per_pulse}

  - platform: wifi_signal
    name: "Wi-Fi Signal Strength"
    update_interval: 3600s
  - platform: uptime
    name: "Device Uptime"
    update_interval: 3600s

switch:
  - platform: restart
    name: "Restart ${devicename}"

2 Likes