Hey everyone,
I wanted to share a small DIY project I recently finished: a fingerprint reader built on an ESP32 with ESPHome, which unlocks my LOQED Touch Smart Lock through a Homey Flow. No cloud dependency, no proprietary app — just ESPHome talking to Homey over the native API.
What it does
- Enroll fingerprints from a button in Homey (or from a built-in web dashboard)
- Recognizes an enrolled finger and briefly triggers an “Access Granted” sensor
- That sensor drives a Homey Flow that unlocks the smart lock
- A live status log (readable/unmatched/misplaced/etc.) shows up both in Homey and on a simple web dashboard on the device’s own IP
- Adjustable match sensitivity (the R503’s security level, 1–5) via two extra buttons, since ESPHome doesn’t expose that setting out of the box
Hardware
- ESP32 dev board
- Grow R503 capacitive fingerprint sensor (6-pin JST)
- A decent 5V/2A USB power source (a laptop USB port isn’t always enough — the sensor plus WiFi TX spikes can brown out a weak supply)
Wiring
| R503 pin | Name | ESP32 pin | Notes |
|---|---|---|---|
| 1 | VCC | 3V3 | Check your module’s silkscreen — some R503 variants need 3.6–6V instead of 3.3V |
| 2 | GND | GND | |
| 3 | TXD | GPIO16 (RX2) | Crossed: sensor TX → ESP32 RX |
| 4 | RXD | GPIO17 (TX2) | Crossed: sensor RX → ESP32 TX |
| 5 | WAKEUP | GPIO4 | Important — without this wired, ESPHome’s sensing_pin gate silently skips all polling and the sensor never detects a finger at all |
| 6 | 3.3VT (touch power) | 3V3 | Separate power line for the capacitive touch/wake circuit |
That WAKEUP pin (5) tripped me up for a while — without it, the sensor compiles and boots fine, reports its capacity correctly, but simply never reacts to a finger. If you’re debugging a “nothing happens” symptom, check that wire first.
Software / setup
- Flash with ESPHome (
python -m esphome run fingerprint-lock.yaml, USB the first time, OTA afterwards) - Add each entity in Homey via the ESPHome Controller app (IP + port 6053 + the API encryption key)
- Build a Flow: WHEN “Access Granted” turns ON → THEN unlock the smart lock
Full yaml config, a secrets.yaml template, and a more detailed README are attached below if anyone wants to replicate this or adapt it for a different lock/sensor combo. Happy to answer questions if you get stuck on the wiring or the ESPHome side.
fingerprint-lock_14.yaml
esphome:
name: fingerprint-lock
friendly_name: Fingerprint Reader
esp32:
board: esp32dev
framework:
type: arduino
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
api:
encryption:
key: !secret api_key
ota:
- platform: esphome
password: !secret ota_password
logger:
web_server:
port: 80
uart:
id: uart_bus
tx_pin: GPIO17
rx_pin: GPIO16
baud_rate: 57600
fingerprint_grow:
id: fingerprint_reader
uart_id: uart_bus
sensing_pin: GPIO4 # connect to WAKEUP (pin 5) of the R503
on_finger_scan_start:
- text_sensor.template.publish:
id: access_log
state: "Finger detected, scanning..."
on_finger_scan_matched:
- logger.log:
format: "Match! ID %d, confidence %d"
args: [ 'finger_id', 'confidence' ]
- text_sensor.template.publish:
id: access_log
state: !lambda |-
char buf[64];
snprintf(buf, sizeof(buf), "Access granted - ID %d (confidence %d)", finger_id, confidence);
return std::string(buf);
- binary_sensor.template.publish:
id: access_granted
state: ON
- delay: 1s
- binary_sensor.template.publish:
id: access_granted
state: OFF
on_finger_scan_unmatched:
- logger.log: "No match found"
- text_sensor.template.publish:
id: access_log
state: "Unknown finger - access denied"
on_finger_scan_invalid:
- logger.log: "Invalid scan"
- text_sensor.template.publish:
id: access_log
state: "Invalid scan - try again"
on_finger_scan_misplaced:
- text_sensor.template.publish:
id: access_log
state: "Finger misplaced - try again"
on_enrollment_scan:
- logger.log:
format: "Enrollment scan, step %d"
args: [ 'scan_num' ]
- sensor.template.publish:
id: enroll_status
state: !lambda "return scan_num;"
- text_sensor.template.publish:
id: enroll_message
state: !lambda |-
char buf[64];
if (scan_num < 2) {
snprintf(buf, sizeof(buf), "Scan %d ok - place your finger again", scan_num);
} else {
snprintf(buf, sizeof(buf), "Scan %d ok - please wait...", scan_num);
}
return std::string(buf);
on_enrollment_done:
- logger.log:
format: "Finger successfully enrolled with ID %d"
args: [ 'finger_id' ]
- sensor.template.publish:
id: enroll_status
state: 100
- text_sensor.template.publish:
id: enroll_message
state: !lambda |-
char buf[48];
snprintf(buf, sizeof(buf), "Done! Finger enrolled (ID %d)", finger_id);
return std::string(buf);
on_enrollment_failed:
- logger.log: "Enrollment failed"
- sensor.template.publish:
id: enroll_status
state: -1
- text_sensor.template.publish:
id: enroll_message
state: "Failed - try again"
text_sensor:
- platform: template
name: "Enroll Message"
id: enroll_message
- platform: template
name: "Access Log"
id: access_log
binary_sensor:
- platform: template
name: "Access Granted"
id: access_granted
sensor:
- platform: fingerprint_grow
fingerprint_count:
name: "Fingerprint Count"
status:
name: "Fingerprint Status"
capacity:
name: "Fingerprint Capacity"
security_level:
name: "Security Level"
- platform: template
name: "Enroll Status"
id: enroll_status
accuracy_decimals: 0
icon: "mdi:fingerprint"
# 0 = idle, 1-2 = scan X of 2 done, 100 = success, -1 = failed
button:
- platform: restart
name: "Restart"
- platform: template
name: "Enroll Fingerprint"
on_press:
- sensor.template.publish:
id: enroll_status
state: 0
- text_sensor.template.publish:
id: enroll_message
state: "Starting... place your finger on the sensor"
- fingerprint_grow.enroll:
finger_id: 1
num_scans: 2
- platform: template
name: "Cancel Enroll"
on_press:
- fingerprint_grow.cancel_enroll:
- platform: template
name: "Delete All Fingerprints"
on_press:
- fingerprint_grow.delete_all:
- platform: template
name: "Set Security Level Lenient (2)"
on_press:
- lambda: |-
// Raw R503/AS608 command: PS_SetSysPara (0x0E), parameter 5 = security level, value 2
uint8_t packet[] = {0xEF, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x05, 0x0E, 0x05, 0x02, 0x00, 0x1B};
id(uart_bus).write_array(packet, sizeof(packet));
- platform: template
name: "Set Security Level Default (3)"
on_press:
- lambda: |-
// Raw R503/AS608 command: PS_SetSysPara (0x0E), parameter 5 = security level, value 3
uint8_t packet[] = {0xEF, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x05, 0x0E, 0x05, 0x03, 0x00, 0x1C};
id(uart_bus).write_array(packet, sizeof(packet));
secrets.yaml.example
wifi_ssid: "YOUR_WIFI_SSID"
wifi_password: "YOUR_WIFI_PASSWORD"
api_key: "GENERATE_WITH: python -c \"import base64, os; print(base64.b64encode(os.urandom(32)).decode())\""
ota_password: "choose_your_own_ota_password"