Wanted to share a project I’ve been working on for a couple of weeks (lots of banggood waiting time )
tl;dr: D1 Mini + ESPHome + magnetometer detects vehicle wanting to exit closed gate and triggers it to open.
My folks have a large metal gate at the entrance of their driveway with a keypad on the outside. Guests can come easily, but due to an auto close after a few minutes it has been an unfortunately manual process to let them exit either by push button on the inside of the gate itself or a garage remote with frustratingly limited range.
They were interested in a vehicle detection induction loop but after looking at the price of easily $150+, I knew I could do it for cheaper and have it done smarter. [enter the D1 mini…]
I’ve previously deployed a D1 Mini Pro (pro for the external antenna) to give them the luxury of a gate opener app with some basic telemetry (open/closed data only). This would be the starting point for my custom vehicle detector. I originally considered an ultrasonic sensor to detect presence but decided to forgo this route to have a cleaner (and hopefully more accurate) detection. After some basic research I came up mostly empty handed but stumbled on a forum suggesting the use of a QMC5883L magnetometer. $7 and a manual build of ESPHome later I had a working prototype!
As I’d suspected, when a vehicle or other large ferrous object (or even a hand placed closely) passed above the magnetometer, its readings would change significantly. Basic tests proved that I would be able to use this for my detection. After some hacking in ESPHome I eventually landed on creating a sensor with a 15 second moving average of heading readings, and triggering my relay if the current heading reading varied from the average for more than 3 seconds. Some additional logic was introduced to filter out sporadic bad readings and prevent gate from actuating too often (or closing on a car passing through!).
The below is what I came up with:
- Create sensor with average of last 15 heading headings (taken at 1s intervals)
- Create “automation safety” binary input to prevent rapid firing of gate open from readings
- If current reading varies ± 2.5° from average turn on internal binary sensor
- If binary sensor on for more than 3 seconds AND gate is closed AND our “automation safety” binary sensor is off: trigger the relay and turn automation safety on for 120s
In the above we have ensured that:
- Something (presumably a large metal object) has caused a change in heading and has stayed for some time
- The gate is closed and probably needs opening
- We won’t accidentally close the gate on a car passing through. (The average heading readings will start to move toward the new reading to the cars magnetic field manipulation, and will change again when the car moves)
Here’s the full ESPHome code:
esphome:
name: gate_actuator
platform: ESP8266
board: d1_mini_pro
on_boot:
then:
# Set/get initial states
- binary_sensor.template.publish:
id: automation_safety
state: OFF
- binary_sensor.template.publish:
id: heading_comparison
state: OFF
- component.update: battery_level
web_server:
port: 80
logger:
level: NONE
wifi:
networks:
- ssid: SSID1
password: Pass1
- ssid: SSID2
password: Pass2
ota:
safe_mode: True
password: esphome_recovery
i2c:
sda: D2
scl: D1
scan: False
script:
- id: gate_toggle
then:
if:
condition:
and:
# Make sure we aren't triggering too rapidly
- binary_sensor.is_off: automation_safety
# Make sure gate is closed or else we won't trigger
- binary_sensor.is_off: gate_status
then:
- binary_sensor.template.publish:
id: automation_safety
state: ON
- switch.toggle: gate
- delay: 120s
- binary_sensor.template.publish:
id: automation_safety
state: OFF
mqtt:
broker: VPS_IP
username: user
password: pass
log_topic:
# Allow the app to toggle the gate independent from any logic
on_message:
- topic: cmd/gate
then:
- switch.toggle: gate
sensor:
- platform: qmc5883l
address: 0x0D
heading:
id: heading
data_rate: 10Hz
range: 200uT
oversampling: 512x
update_interval: 1s
# Template sensor to keep average of last 15 heading
# readings so we can detect significant change
- platform: template
id: heading_average
update_interval: 1s
unit_of_measurement: "°"
lambda: return id(heading).state;
filters:
- sliding_window_moving_average:
window_size: 15
send_every: 1
# only trigger gate if heading comparison has been
# true for more than 5s and safety is off
on_value:
then:
if:
condition:
and:
- for:
time: 3s
condition:
binary_sensor.is_on: heading_comparison
# This is intentionally duplicated
- binary_sensor.is_off: automation_safety
then:
- script.execute: gate_toggle
- platform: adc
pin: A0
id: battery_level
name: "Gate Battery Voltage"
update_interval: 60s
accuracy_decimals: 4
filters:
- multiply: 15.43
- platform: uptime
name: "Uptime"
binary_sensor:
# Sensor to keep script from triggering in rapid succession
- platform: template
name: "Automation Safety"
id: automation_safety
# This is what is used to trigger the automation
- platform: template
name: Heading Comparison Trigger
id: heading_comparison
# Set our trigger to true if heading varies from avg more than 2.5deg
lambda: |-
if (id(heading).state > id(heading_average).state + 2.5) {
return true;
}
if (id(heading).state < id(heading_average).state - 2.5) {
return true;
}
else {
return false;
}
# Magnet sensor to get real-world state
- platform: gpio
pin:
number: D7
mode: INPUT_PULLUP
name: Gate Status
id: gate_status
device_class: door
filters:
- delayed_on: 10ms
switch:
- platform: shutdown
name: "Shut Down"
# Actual GPIO output
- platform: gpio
id: relay
pin:
number: D8
inverted: False
# Switch which toggles the relay on and off
- platform: template
id: gate
name: "Gate Remote"
icon: "mdi:gate"
turn_on_action:
- switch.turn_on: relay
- delay: 500ms
- switch.turn_off: relay
- binary_sensor.template.publish:
id: automation_safety
state: ON
- delay: 60s
- binary_sensor.template.publish:
id: automation_safety
state: OFF
text_sensor:
- platform: wifi_info
ip_address:
name: "IP Address"
In addition to vehicle detection there is a gate status sensor (open/closed), battery sensor (gate and microcontroller run on 12V battery), and functionality to open/close gate via app/mqtt. I wanted to be sure this could operate independently (I run Home Assistant at home but they live far away) and have built it so that no external connectivity is required but if present will add additional functionality.
Testing with magnetometer on end of long (30’) cat5 to ensure functionality and measure battery consumption:
How the QMC5883L was connected to Cat5:
Magnetometer mounted inside Sonoff IP66 junction box (this was buried underground inside 4" PVC for protection under rock driveway. Moisture absorbing packets optional, hopefully not needed! ):
Testing voltage divider for ADC. This later mated (upside down) to a D1 Proto Shield:
Completed “front” of stack:
Completed “back” of stack:
Wired up in gate electronics box:
Reed switch on swing arm to detect open/closed:
Wiring diagram:
Telemetry data and gate toggle Home Assistant:
Home Assistant Config:
sensor:
- platform: mqtt
state_topic: "gate_actuator/sensor/gate_battery_voltage/state"
name: gate_battery_voltage
unit_of_measurement: 'V'
- platform: mqtt
state_topic: "gate_actuator/sensor/ip_address/state"
name: gate_ip_address
- platform: mqtt
state_topic: "gate_actuator/sensor/uptime/state"
name: gate_uptime
unit_of_measurement: 's'
- platform: mqtt
state_topic: "gate_actuator/status"
name: gate_online_offline
binary_sensor:
- platform: mqtt
state_topic: "gate_actuator/binary_sensor/gate_status/state"
name: gate_status
device_class: door
- platform: mqtt
state_topic: "gate_actuator/binary_sensor/heading_comparison_trigger/state"
name: gate_heading_comparison
- platform: mqtt
state_topic: "gate_actuator/binary_sensor/automation_safety/state"
name: gate_automation_safety
automation:
- alias: "Gate Heading Comparison Action"
initial_state: 'on'
trigger:
- platform: state
entity_id: binary_sensor.gate_automation_safety
to: 'on'
action:
- service: notify.pushover
data_template:
message: >
Vehicle Sensor Automation Fired!
- alias: "Gate Battery Safety Notify"
initial_state: 'on'
trigger:
- platform: numeric_state
entity_id: sensor.gate_battery_voltage
below: 8
action:
- service: notify.pushover
data_template:
message: >
Battery Low: {{ states.sensor.gate_battery_voltage.state }}
data:
priority: 1
- alias: "Webhook Trigger"
initial_state: 'on'
trigger:
platform: webhook
webhook_id: !secret gate_webhook_id
action:
service: script.turn_on
entity_id: script.gate_toggle
script:
mom_and_dad_gate_toggle:
sequence:
- service: mqtt.publish
data:
topic: "cmd/gate"
payload: 'toggle'
Component Cost:
D1 Mini Pro: $4
D1 Mini Dual Base: $1.00
D1 Mini Relay Shield: $1.20
D1 Mini DC Power Shield: $1.40
D1 Mini Proto Board: $0.30
QMC5883L Triple Axis Compass Magnetometer: $6.59
Sonoff IP66 Junction Box: $6.99
Voltage Divider (to measure up to 25v): $1.38
Misc. cables: $0
Total: ~$22.96 USD.
Final thoughts:
This build works just as I’d expect, I’m very pleased with how it turned out. I do have a couple of concerns:
- The cat5 I’ve used is only plenum rated (if that). I didn’t bury it in any conduit so there’s a decent chance that it will degrade, ingest water, etc. We’ll see if this has near-term affects on the function, in the long term it should almost certainly be laid in a conduit, time will tell.
- The D1 Mini has a voltage divider on A0 already. It feels weird to stack the voltage dividers and my reading*15.43 was the result of some basic circuit diagram and real-world testing. In addition, the voltage sensor is not currently functioning as I’d expect (see the 0.2260V reading in the HomeAssistant screenshot?). I suspect the cable on the battery terminal is loose. In hindsight I should have soldered the sensor wire onto a washer to be tight alongside the other wiring on the battery + terminal. I also didn’t attach the GND to the voltage divider. I don’t think it’s necessary since the whole system is battery powered and should share the same ground but I’m not an electrical engineer. The D1 pulls power from the gate circuit board’s +12v and GND terminals.
- I’m not sure how well the IP66 Sonoff box will do underground in the rock driveway long term. The PVC pipe was a last minute addition and probably the saving grace. Even still I don’t know if the plexiglass cover on the junction box will eventually get cracked from pressure or have water seep in by another means.
I know this was a wall of text, thanks for reading! Questions? Suggestions?