After becoming inspired by @parrel 's ESPHome garage door opener thread I decided to build my own solution. As I only have a 220V connection on the place where I want to mount the sensor and no room to route eny UTP cable I have to come up with a solution which enables me to put the sensor and the NodeMCU in one waterproof box and mount it in a more or less public space (my driveway). I learned a lot from the thread I mentioned, so a huge thanks to overyone who contributed there.
The safety features of the solution:
- Sabotage alarm when opening the box
- Automatic destruction of all fingerprints saved when opening the box (when powered)
- Alert on too many tries to scan fingerprint
- Scanner enters temporal lock state on too many tries
- Need to authorize the scanner after a power cycle (to prevent powered off tampering)
The physical side
Please see the garage door opener thread on how to connect the Grow R503 scanner to a NodeMCU. I used the same hardware.
On top of that I added a sabotage switch to the nodeMCU. To do this I connected a 3V output of the nodeMCU to a 10kOhm resistor to a microswitch (normally closed) to ground of the NodeMCU. I attached terminal D5 of the NodeMCU to the terminal of the microswitch that is also connected to the resistor.
Like this
3V -------[10kOhm]----|---[NC microswitch]---- Ground
D5
See the microswitch glued to the right inside of the box. It is pressed when mounting the lid. Doesn’t look like much, but it works. The cover for the fingerprint sensor is still in backorder.
The software side
The YAML file for ESPHome
esphome:
name: toegang
on_boot:
# Cycle colors after finishing boot to show boot is done.
priority: -100
then:
- fingerprint_grow.aura_led_control:
state: BREATHING
speed: 100
color: RED
count: 1
- fingerprint_grow.aura_led_control:
state: BREATHING
speed: 100
color: GREEN
count: 1
- fingerprint_grow.aura_led_control:
state: BREATHING
speed: 100
color: BLUE
count: 1
- text_sensor.template.publish:
id: toegang_state
state: "READY"
esp8266:
board: nodemcuv2
# Global variable to store the state of the disarm_sabotage switch
# Restore the value to be able to reboot and not have the disarm switch switch
# to off and erasing all fingerprints
globals:
- id: disarm_state
type: int
restore_value: yes
initial_value: '0'
# Enable logging
logger:
# level: DEBUG
level: WARN
# Enable Home Assistant API
api:
password: !secret api_password
encryption:
key: !secret api_encryption
services:
# Scan new fingerprints
# Variables:
# finger_id: memory position to store fingerprint in. If the slot is already
# taken, it will be overwritten.
# num_scans: Number of scans to make of the finger before storing it.
# 2 should be enough for most purposes.
- service: enroll
variables:
finger_id: int
num_scans: int
then:
- fingerprint_grow.aura_led_control:
state: ALWAYS_ON
speed: 0
color: PURPLE
count: 0
- text_sensor.template.publish:
id: toegang_state
state: "SCAN"
- fingerprint_grow.enroll:
finger_id: !lambda 'return finger_id;'
num_scans: !lambda 'return num_scans;'
# Abort scanning of new fingerprints
- service: cancel_enroll
then:
- fingerprint_grow.cancel_enroll:
# Delete a fingerprint
# finger_id: Memory position to erase
- service: delete
variables:
finger_id: int
then:
- fingerprint_grow.delete:
finger_id: !lambda 'return finger_id;'
# Delete all fingerprints stored
# I see ro real usecase for this service. I leave it commented out to prevent
# triggering it accidentally.
#- service: delete_all
# then:
# - fingerprint_grow.delete_all:
# Push a password that will disappear on power cycle this way tampering with
# the scanner can be detected
# Variables:
# set_authorized: Set this to a password generated by HA
- service: set_authorized
variables:
auth_password: string
then:
- text_sensor.template.publish:
id: toegang_auth_password
state: !lambda 'return auth_password;'
# Turn on the light. Usefull for when it is dark. E.g. call this service when
# motion is detected by a nearby motion sensor.
- service: scanner_light_on
then:
# If the scanner is not authorized, use a different color so the user can
# see that the scanner will not work.
- if:
condition:
binary_sensor.is_on: is_not_autorised
then:
- fingerprint_grow.aura_led_control:
state: GRADUAL_ON
speed: 100
count: 0
color: CYAN
else:
- fingerprint_grow.aura_led_control:
state: GRADUAL_ON
speed: 100
count: 0
color: WHITE
# And a service to turn the light off again. Not implemented as a light. If
# the scanner is used with the light on, the light will be left in an
# undetermined state.
- service: scanner_light_off
then:
- if:
condition:
binary_sensor.is_on: is_not_autorised
then:
- fingerprint_grow.aura_led_control:
state: GRADUAL_OFF
speed: 100
count: 0
color: CYAN
else:
- fingerprint_grow.aura_led_control:
state: GRADUAL_OFF
speed: 100
count: 0
color: WHITE
ota:
password: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
wifi:
networks:
- ssid: !secret wifi_ssid
password: !secret wifi_password
# Do not start a wifi network when there is none (fallback hotspot). This
# creates additional attack surface to hack the module.
#ap:
# ssid: "Toegang Fallback Hotspot"
# password: !secret esp_toegang_ap
# ap_timeout: 5min
# Do not start the captive portal to limit the attack surface
#captive_portal:
sensor:
- platform: wifi_signal
name: "WiFi Signal Toegang"
update_interval: 300s
icon: mdi:signal
# Bultin sensors for the fingerprint_grow components
- platform: fingerprint_grow
fingerprint_count:
name: "Toegang Fingerprint Count"
last_finger_id:
name: "Toegang Fingerprint Last Finger ID"
last_confidence:
name: "Toegang Fingerprint Last Confidence"
status:
name: "Toegang Fingerprint Status"
capacity:
name: "Toegang Fingerprint Capacity"
security_level:
name: "Toegang Fingerprint Security Level"
number:
# Configure the maximum number of scan failures before throttling
- platform: template
name: "Max. mislukte pogingen"
id: toegang_maxfail
optimistic: true
min_value: 2
max_value: 15
mode: slider
initial_value: 5
step: 1
restore_value: true
icon: mdi:account-lock
# Counter for the failed attempts to scan a finger
- platform: template
name: "Aantal mislukte pogingen"
id: toegang_fail
internal: true
optimistic: true
min_value: 0
max_value: 21
initial_value: 0
step: 1
mode: box
text_sensor:
# Expose some Wifi information
- platform: wifi_info
ip_address:
name: "Toegang IP Address"
id: toegang_ip
icon: mdi:ip-network
mac_address:
name: "Toegang MAC Address"
id: toegang_mac
icon: mdi:expansion-card
# Scanner state. In this text sensor the result of a scan will be published.
# After a few seconds the state of this sensor will return to READY, to
# indicate the sensor is ready for the next scan. Use this sensor as the
# trigger in an HA script or automation.
- platform: template
id: toegang_state
name: "Status vingerafdruk scanner"
icon: mdi:fingerprint
# Text sensor to store a password than can be set from Home Assistant.
# The password will be empty after each boot. So the password needs to be
# input after each boot. This prevents tampering with the sensor.
# At each scan the password in the ESP is compared with the password HA has.
# Only if the passwords match, the fingerprint scanner will return a MATCH.
- platform: template
id: toegang_auth_password
name: "Toegang autorization password"
icon: mdi:form-textbox-password
# Button for restarting the ESP manually
button:
- platform: restart
name: "Toegang Restart"
# Fingerprint sensor communication pins.
uart:
tx_pin: D0
rx_pin: D1
baud_rate: 57600
fingerprint_grow:
password: !secret fingerprint_scanner
# new_password: !secret fingerprint_scanner
sensing_pin: D2
on_finger_scan_matched:
# Only accept if failcounter hasn't reached limit show and the scanner is in
# an autorised state. Show as an unmatched fingerprint else. Nothe that the
# states MATCHED and NOMATCH have the same number of characters so that the
# datastream is of equal size (it is encrypted, so the attacker is left in
# the dark if the scan was correct or not).
- if:
condition:
or:
- binary_sensor.is_on: toegang_alarm
- binary_sensor.is_on: is_not_autorised
then:
- fingerprint_grow.aura_led_control:
state: FLASHING
speed: 25
color: RED
count: 12
- text_sensor.template.publish:
id: toegang_state
state: "BLOCKED"
else:
- fingerprint_grow.aura_led_control:
state: BREATHING
speed: 150
color: GREEN
count: 2
- text_sensor.template.publish:
id: toegang_state
state: "MATCH"
- number.to_min: toegang_fail
- delay: 3s
- text_sensor.template.publish:
id: toegang_state
state: "READY"
# When a fingerprint scan fails it depends on the autorisation state and the
# number of failed scans what happens.
# The failcounter is only increased when we have not reached the limit yet to
# prevent the counter form cycling to its minimal value and to prevent a
# denial of service attack on the sensor.
on_finger_scan_unmatched:
- fingerprint_grow.aura_led_control:
state: FLASHING
speed: 25
color: RED
count: 12
- if:
condition:
or:
- binary_sensor.is_on: toegang_alarm
- binary_sensor.is_on: is_not_autorised
then:
- text_sensor.template.publish:
id: toegang_state
state: "BLOCKED"
else:
- text_sensor.template.publish:
id: toegang_state
state: "NOMATCH"
- number.increment: toegang_fail
- delay: 3s
- text_sensor.template.publish:
id: toegang_state
state: "READY"
# Show some feedback during enrollment of a new fingerprint
on_enrollment_scan:
- fingerprint_grow.aura_led_control:
state: FLASHING
speed: 25
color: BLUE
count: 5
- fingerprint_grow.aura_led_control:
state: ALWAYS_ON
speed: 0
color: PURPLE
count: 0
- text_sensor.template.publish:
id: toegang_state
state: "SCAN"
# Give feedback when enrollment is ready and succesfull
on_enrollment_done:
- fingerprint_grow.aura_led_control:
state: BREATHING
speed: 100
color: CYAN
count: 2
- text_sensor.template.publish:
id: toegang_state
state: "STORED"
- delay: 2s
- text_sensor.template.publish:
id: toegang_state
state: "READY"
# Give feedback when enrollment is ready and has failed
on_enrollment_failed:
- fingerprint_grow.aura_led_control:
state: FLASHING
speed: 25
color: RED
count: 4
- text_sensor.template.publish:
id: toegang_state
state: "FAILED"
- delay: 1s
- text_sensor.template.publish:
id: toegang_state
state: "READY"
binary_sensor:
# Sabotage sensor. This a a microswitch connected to pin D5. D3 and D4 are
# also available, but thos pins have other functions as well. Depending on the
# state of the microswitch, the NodeMCU might not boot when using these pins.
# The microswitch is normally closed (so open when pressed)
# +3V----[10KOhm resistor]----[pin D5]----/[switch]----Ground
- platform: gpio
pin:
number: D5
inverted: true
mode:
input: true
pullup: true
name: "Toegang sabotage sensor"
id: toegang_tamper
# Add a delay to bridge any instability in the readout of pin D5
filters:
- delayed_on: 200ms
# delay for turning off. This prevents setting of the alarm while screwing
# the lid on.
- delayed_off: 1000ms
device_class: tamper
icon: mdi:shield
# When the microswitch is released, delete all fingerprints in the sensor
# and deauthorize. Now the scanner is in an unusable state.
on_press:
then:
- if:
condition:
switch.is_off: disarm_sabotage
then:
- fingerprint_grow.delete_all:
- text_sensor.template.publish:
id: toegang_auth_password
state: "SABOTAGE"
else:
- text_sensor.template.publish:
id: toegang_auth_password
state: "MAINTENANCE"
on_release:
then:
# Disable the disarm switch after closing the box so we don't forget to
# arm the sabotage mechanism.
- if:
condition:
switch.is_off: disarm_sabotage
then:
- switch.turn_off: disarm_sabotage
# This sensor goes to true when the number of failed attempts exceeds the
# limit. In this state the scanner will not accept any fingerpint, valid or
# not.
- platform: template
id: toegang_alarm
name: "Toegang Alarm"
device_class: safety
icon: mdi:alarm-light
internal: false
lambda: 'return id(toegang_fail).state > id(toegang_maxfail).state;'
# This sensor catches if we have been autorised
# MAKE SURE THIS SENSOR IS DEFINED IN HOME ASSISTANT.
# ATTENTION: THIS SENSOR IS OF CLASS SAFETY AND IS OFF WHEN AUTORISED
- platform: homeassistant
id: is_not_autorised
entity_id: binary_sensor.autorisatie_vingerafdruk_sensor
switch:
# Switch to disable the sabotage sensor. Use this switch when maintenance is
# needed. It allows for opening the lid without erasing all fingerprints.
- platform: template
name: "Toegang disarm sabotage sensor"
id: disarm_sabotage
icon: mdi:shield-off
lambda: |-
if (id(disarm_state) == 1) {
return true;
} else {
return false;
}
internal: false
turn_on_action:
- lambda: |-
id(disarm_state) = 1;
turn_off_action:
- lambda: |-
id(disarm_state) = 0;
# Decrease the fail timer every 10 minutes, thereby throttling new attempts
time:
- platform: homeassistant
id: homeassistant_time
on_time:
- seconds: 0
minutes: /10
then:
- number.decrement:
id: toegang_fail
cycle: false
Note of warning. Set the password for the fingerprint reader like documented here. There are also some catches to it causing me to almost brick my sensor like described here
This should lead to a functioning scanner that can be connected to Home Assistant.
Scanner authorisation mechanism
To prevent offline tampering I store a password in the volatile memory of the NodeMCU and rotate that password every 30 minutes. When a finger is scanned, Home Assistant will only open something is the password stored in the NodeMCU is the same as the password stored by Home Assistant (and the tamper switch is safe). To do this I first need a sensor in HA with a rotating password. I created that like this:
- platform: command_line
command: openssl rand -base64 32
name: random_password
scan_interval: 1800
value_template: "{{ value }}"
The I created a helper (input_text, 50 characters) to store and keep the password over a Home Assistant reboot.
The script to authorise the scanner:
alias: Authorise Toegang
sequence:
- service: esphome.toegang_set_authorized
data:
auth_password: '{{ states("sensor.random_password") }}'
- service: input_text.set_value
data:
value: '{{ states("sensor.random_password") }}'
target:
entity_id: input_text.old_random_password
mode: single
icon: mdi:fingerprint
The script to rotate the password:
alias: Update Toegang Autorisation
description: ''
trigger:
- platform: state
entity_id:
- sensor.random_password
action:
- if:
- alias: Only if we are authorized
condition: template
value_template: >-
{{ states('input_text.old_random_password') ==
states('sensor.toegang_autorization_password') }}
then:
- alias: Update the autorization password
service: esphome.toegang_set_authorized
data:
auth_password: '{{ states("sensor.random_password") }}'
- service: input_text.set_value
data:
value: '{{ states("sensor.random_password") }}'
target:
entity_id: input_text.old_random_password
mode: single
The script to authorise the scanner automatically uphome HA reboot
alias: On start of Home Assisant
description: Things to do when Home Assistant starts
trigger:
- event: start
platform: homeassistant
condition: []
action:
- condition: template
value_template: >-
{{ states('sensor.toegang_autorization_password') ==
states('input_text.old_random_password') }}
- service: script.authorise_toegang
data: {}
mode: single
And the template binary sensor which indicates the current authorisation state:
template:
- binary_sensor:
- name: "Autorisatie vingerafdruk sensor"
device_class: safety
# Do not change state when rolling over the password
delay_off: 00:00:01
state: >-
{% if states("sensor.random_password") == states("sensor.toegang_autorization_password") %}
false
{% else %}
true
{% endif %}
icon: >-
{% if states("sensor.random_password") == states("sensor.toegang_autorization_password") %}
mdi:lock-check
{% else %}
mdi:lock-remove
{% endif %}
On the first start of the sensor you should run the “Authorise toegang” script and then the binary sensor will turn off, till someone takes the sensor offline or reboots it.
I also created a number of other scripts, among which the script to add new fingerprints:
alias: Add fingerprint
sequence:
- service: esphome.toegang_enroll
data:
num_scans: 2
finger_id: '{{ states("sensor.toegang_fingerprint_count") }}'
mode: single
icon: mdi:fingerprint
Now, when a finger is scanned the sensor.toegang_state is set to MATCH for 3 seconds which triggers a script that executes the following workflow:
- Is it between 06:00 and 23:00
- Is the scanner authorised
- Is the sabotage switch off
- Is the alarm off
- At least one occupant (the occupants phone to be exact) is detected at home (as soon as someone is within a 100m radius of the home, zone.home increased value from 0 to the number os persons detected.
- Get the value of
sensor.toegang_fingerprint_last_finger_id
and dofinger_id mod 3
. As I have 3 actions connected tot the scanner for three different fingers of each person.
When either binary_sensor.toegang_sabotage_sensor
or binary_sensor.toegang_alarm
turns on, I turn on the floodlights on the driveway and enable the alarm sirens on my Reolink cams (which are also recording the event by the way).
Any tips to make it even more secure?