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?