Debouncing an ultrasonic distance sensor for Car presence detection

Hey there!

I’d like to use an ultrasonic sensor (the ubiquitous HC-SR04) to detect the presence of a car, after every closing of my garage door.

It’s also battery powered, so I’m looking to conserve juice. I used an on_boot lambda to disable the automatic interval on the ultrasonic sensor, and trigger it manually. That part works well.

How I did the manual polling on my sensor
esphome:
  on_boot:
    - priority: 590 # "600.0: This is where most sensors are set up."
      then:
        - lambda: !lambda |-
            ESP_LOGI("main", "Disabling polling on the ultrasonic sensor.");
            App.scheduler.cancel_interval(id(ultrasonic_sensor), "update");

I can still trigger it on-demand:

button:
  - name: "Measure distance"
    platform: template
    on_press:
      lambda: !lambda |-
        ESP_LOGI("main", "Button pressed in HA");
        id(ultrasonic_sensor).update();
        float distance = id(ultrasonic_sensor).get_state();
        ESP_LOGI("main", "Distance measured: %f", distance);

Now the issue is that the sensor values can be a bit noisy. I want to trigger automations on the rising/falling edges (e.g. auto-close the garage door X seconds after the car pulls out), so I have an appetite for latency, but not for any false transitions from present to absent or vice versa.

I tried several approaches, but I hit a dead end on each of them.

Attempt 1: a template `binary_sensor` with a `delayed_on` filter

This seemed promising, but I can’t figure out a way to express “repeat this loop until you hit a stable value”

button:
  # For testing only. I'll replace this with a lambda triggered by `cover.on_closed` on my Garage Door
  - name: "Check car presence"
    platform: template
    on_press:
      - repeat: 
          count: 10 # Sample 10 times
          then:
            - logger.log: "Sampling ultrasonic sensor"
            - component.update: ultrasonic_sensor
            - if:
                condition:
                  sensor.in_range: { id: ultrasonic_sensor, below: 1.0 } # < 1 meter away, car is present
                then:
                  - logger.log:
                      format: "Sampled the car as being present (distance: %f)"
                      args: ['id(ultrasonic_sensor).state']
                  - binary_sensor.template.publish: { id: garage_car_presence, state: ON }
                else:
                  - logger.log:
                      format: "Sampled the car as being absent (distance: %f)"
                      args: ['id(ultrasonic_sensor).state']
                  - binary_sensor.template.publish: { id: garage_car_presence, state: OFF }

            - delay: 1s

sensor:
  - name: "Ultrasonic demo"
    id: ultrasonic_sensor
    platform: ultrasonic
    trigger_pin: GPIO14
    echo_pin: GPIO12
    timeout: 3m # My sensor can measure up to 3 meters
    update_interval: 4294967295ms # UInt32.max
    filters:
      - lambda: !lambda "return isnan(x) ? 3.0 : x;" # Treat a timeout as a measumrent of 3 meters

binary_sensor:
  - name: "Car presence"
    id: garage_car_presence
    platform: template
    filters:
      - delayed_on: 10s

Attempt 2: Implement a custom lambda to the looping
button:
  # For testing only. I'll replace this with a lambda triggered by `cover.on_closed` on my Garage Door
  - name: "Check car presence"
    platform: template
    on_press:
      lambda: !lambda |-
        ESP_LOGI("main", "Button pressed in HA");

        auto numSamples = 10;
        auto numShortSamples = 0;
        auto carPresenceDistance = 1.0; // If less than 1 meter, consider the car present.

        for (auto i = 0; i < 10; i++) {
          id(ultrasonic_sensor).update();
          auto distance = id(ultrasonic_sensor).get_state();
          if (distance < carPresenceDistance) {
            ESP_LOGI("main", "Ultrasonic sensor sample %i: present", i);
            numShortSamples += 1;
          } else {
            ESP_LOGI("main", "Ultrasonic sensor sample %i: absent", i);
          }

          // ℹ️ How do I delay here? There's no easy way to capture this into a lambda to use with a DelayAction
        }

        // TODO: this only considers consensus for presence. Alter it to check for 7/10 consensusr for both presence and absense.
        auto consensusThreshold = 7;
        auto carIsPresent = consensusThreshold <= numShortSamples;
        id(garage_car_presence).publish_state(carIsPresent);
```yaml

<hr>

I’m not scared to write lambdas or even a custom virtual component that manages the ultrasonic sensor, but if possible I’m trying to stay “on the beaten path” so future updates are less likely to break anything.

Any tips on how I should approach this?

Couple of thoughts…

You can also set the update interval to “never” to disable time based updates.

Then you can use a moving median to smooth noise. Manually request updates 10 times in a repeat like you’ve done and then push it out every 10th value.

Then use the result to feed a binary sensor (detected/not detected).

Have a look at some of my WIP config here, I’m doing something similar.