#  _____               _   _____               _____         _         _
# |   __|_____ ___ ___| |_|  |  |___ _____ ___|   __|___ ___| |_ ___  |_|___
# |__   |     | .'|  _|  _|     | . |     | -_|  |  | -_| -_| '_|_ -|_| | . |
# |_____|_|_|_|__,|_| |_| |__|__|___|_|_|_|___|_____|___|___|_,_|___|_|_|___|
#
# https://www.smarthomegeeks.io/fingerprint-doorbell

substitutions:
  # GROW R503 password
  new_fingerprint_password: "0"        # Set a new password here only on first flash
  fingerprint_password: "0x00000000"  # Set new password here if needed

  # esphome
  friendly_name: "Fingerprint Doorbell"
  api_encryption_key: ""  # Set API encryption key here if desired
  ota_password: ""  # Set OTA password here if desired

  # GPIO Pins
  uart_tx_pin: GPIO25        # R503 green (or purple) wire
  uart_rx_pin: GPIO27        # R503 yellow wire
  sensing_pin: GPIO32        # R503 blue wire
  lock_relay_pin: GPIO13
  open_relay_pin: GPIO14
  chime_relay_pin: GPIO16
  door_sensor_pin: GPIO26

  # Manual Enroll (Short press) / Factory Reset (10 second press)
  gpioButton: GPIO0

  # Enroll animation
  led_enroll_state: BREATHING
  led_enroll_speed: "100"
  led_enroll_color: CYAN
  led_enroll_count: "0"

  # Enroll Scan animation
  led_enroll_scan_state: FLASHING
  led_enroll_scan_speed: "25"
  led_enroll_scan_color: BLUE
  led_enroll_scan_count: "2"

  # Enroll Failed animation
  led_enroll_fail_state: FLASHING
  led_enroll_fail_speed: "25"
  led_enroll_fail_color: RED
  led_enroll_fail_count: "4"

  # Enroll Done animation
  led_enroll_done_state: BREATHING
  led_enroll_done_speed: "100"
  led_enroll_done_color: BLUE
  led_enroll_done_count: "2"

  # Match animation
  led_match_on_state: ALWAYS_ON
  led_match_on_speed: "0"
  led_match_on_color: GREEN
  led_match_on_count: "0"
  led_match_off_state: GRADUAL_OFF
  led_match_off_speed: "100"
  led_match_off_color: GREEN
  led_match_off_count: "0"

  # Scan animation
  led_scan_state: BREATHING
  led_scan_speed: "50"
  led_scan_color: PURPLE
  led_scan_count: "2"

  # Open House Mode
  led_open_house_on_state: BREATHING
  led_open_house_on_speed: "100"
  led_open_house_on_color: GREEN
  led_open_house_on_count: "0"
  led_open_house_off_state: GRADUAL_OFF
  led_open_house_off_speed: "100"
  led_open_house_off_color: GREEN
  led_open_house_off_count: "0"

  # Night Light (warm white when idle)
  led_nightlight_state: ALWAYS_ON
  led_nightlight_speed: "0"
  led_nightlight_color: WHITE
  led_nightlight_count: "0"

esphome:
  on_boot:
    priority: 600
    then:
      - lambda: |-
          nvs_flash_init();
          id(fingerprint_username).publish_state("");
          id(door_sensor).publish_state(id(door_sensor).state);
      - script.execute: update_user_list
      - binary_sensor.template.publish:
          id: fingerprint_scanning
          state: OFF
      - text_sensor.template.publish:
          id: fingerprint_state
          state: "Idle"
  name: ${name}
  friendly_name: ${friendly_name}
  project:
    name: SmartHomeGeeks.Fingerprint Doorbell
    version: "2026.01.15"
  includes:
    - "<nvs_flash.h>"
    - "<nvs.h>"
    - "<set>"

esp32:
  board: esp32dev
  framework:
    type: arduino

logger:

ota:
  - platform: esphome
    password: ${ota_password}

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  ap:
    ssid: "SHGio_Fingerprint_Doorbell"
    password: "SmartHomeGeeks"

captive_portal:

globals:
  - id: script_username
    type: std::string
    restore_value: no
    initial_value: '""'
  - id: fingerprint_delete_id
    type: int
    restore_value: no
    initial_value: '0'
  - id: enroll_fid
    type: int
    restore_value: no
    initial_value: '-1'

api:
  on_client_disconnected:
    then:
      - script.execute: disable_manual_mode_after_timeout

  on_client_connected:
    then:
      - script.stop: disable_manual_mode_after_timeout

  encryption:
    key: ${api_encryption_key}

  services:
    - service: delete
      variables:
        finger_id: int
      then:
        - fingerprint_grow.delete:
            finger_id: !lambda 'return finger_id;'
        - script.execute:
            id: erase_fingerprint_id
            finger_id: !lambda 'return finger_id;'
        - script.execute: update_user_list

    - service: cancel_enroll
      then:
        - script.execute: trigger_cancel_enroll

    - service: enroll
      variables:
        num_scans: int
        user_name: string
      then:
        - lambda: |-
            id(script_username) = user_name;
            id(trigger_enroll).execute();

    - service: delete_all
      then:
        - fingerprint_grow.delete_all:
        - lambda: |-
            nvs_handle_t handle;
            if (nvs_open("storage", NVS_READWRITE, &handle) == ESP_OK) {
              for (int i = 0; i < 200; ++i) {
                std::string key = "user_" + std::to_string(i);
                nvs_erase_key(handle, key.c_str());
              }
              nvs_commit(handle);
              nvs_close(handle);
            }
        - script.execute: update_user_list

    - service: delete_user
      variables:
        user_name: string
      then:
        - lambda: |-
            std::string target = user_name;
            std::transform(target.begin(), target.end(), target.begin(), ::tolower);
            for (int i = 0; i < 200; ++i) {
              nvs_handle_t handle;
              if (nvs_open("storage", NVS_READWRITE, &handle) == ESP_OK) {
                std::string key = "user_" + std::to_string(i);
                size_t required_size = 0;
                if (nvs_get_str(handle, key.c_str(), nullptr, &required_size) == ESP_OK && required_size > 1) {
                  std::vector<char> buf(required_size);
                  if (nvs_get_str(handle, key.c_str(), buf.data(), &required_size) == ESP_OK) {
                    std::string found = std::string(buf.data());
                    std::transform(found.begin(), found.end(), found.begin(), ::tolower);
                    if (found == target) {
                      id(fingerprint_delete_id) = i;
                      id(fingerprint_delete_script).execute();
                    }
                  }
                }
                nvs_close(handle);
              }
            }
        - script.execute: update_user_list

# Example configuration entry
lock:
  - platform: template
    name: ${friendly_name}
    lambda: |-
      if (id(door_sensor).state) {
        return LOCK_STATE_UNLOCKED;
      } else {
        return LOCK_STATE_LOCKED;
      }
    lock_action:
      - script.execute: lock_door
    unlock_action:
      - script.execute: unlock_door
    open_action:
      - button.press: open_door

script:
  - id: update_fingerprint_state
    mode: restart
    parameters:
      new_state: string
    then:
      - lambda: |-
          std::string s = new_state;
          for (char &c : s) {
            unsigned char uc = (unsigned char)c;
            if (uc < 32 || uc > 126) c = '?';
          }
          id(fingerprint_state).publish_state(s.c_str());
      - delay: 5s
      - text_sensor.template.publish:
          id: fingerprint_state
          state: "Idle"

  - id: reset_fingerprint_scanning
    mode: restart
    then:
      - delay: 1s
      - binary_sensor.template.publish:
          id: fingerprint_scanning
          state: OFF

  - id: disable_manual_mode_after_timeout
    mode: restart
    then:
      - delay: 60s
      - lambda: |-
          if (id(manual_mode).state) {
            ESP_LOGI("main", "Still disconnected after 60s – turning off Manual Mode");
            id(manual_mode).turn_off();
          }

  - id: erase_fingerprint_id
    mode: queued
    parameters:
      finger_id: int
    then:
      - lambda: |-
          nvs_handle_t handle;
          if (nvs_open("storage", NVS_READWRITE, &handle) == ESP_OK) {
            std::string key = "user_" + std::to_string(finger_id);
            nvs_erase_key(handle, key.c_str());
            nvs_commit(handle);
            nvs_close(handle);
          }

  - id: fingerprint_delete_script
    then:
      - fingerprint_grow.delete:
          finger_id: !lambda 'return id(fingerprint_delete_id);'
      - script.execute:
          id: erase_fingerprint_id
          finger_id: !lambda 'return id(fingerprint_delete_id);'

  - id: update_user_list
    then:
      - lambda: |-
          auto ascii_sanitize = [](std::string s) {
            for (char &c : s) {
              unsigned char uc = (unsigned char)c;
              if (uc < 32 || uc > 126) c = '?';
            }
            return s;
          };
          auto json_escape = [&](const std::string &s) {
            std::string out; out.reserve(s.size()+8);
            for (char c : s) {
              if (c == '\\' || c == '\"') { out += '\\'; out += c; }
              else if ((unsigned char)c < 32 || (unsigned char)c > 126) { out += '?'; }
              else { out += c; }
            }
            return out;
          };

          nvs_handle_t handle;
          if (nvs_open("storage", NVS_READONLY, &handle) == ESP_OK) {
            int count = 0;
            std::set<std::string> user_set;
            for (int i = 0; i < 200; ++i) {
              std::string key = "user_" + std::to_string(i);
              size_t required_size = 0;
              if (nvs_get_str(handle, key.c_str(), nullptr, &required_size) == ESP_OK && required_size > 1) {
                ++count;
                std::vector<char> buf(required_size);
                if (nvs_get_str(handle, key.c_str(), buf.data(), &required_size) == ESP_OK) {
                  user_set.insert(ascii_sanitize(std::string(buf.data())));
                }
              }
            }
            nvs_close(handle);
            id(fingerprint_count).publish_state(count);

            std::string json = "[";
            bool first = true;
            for (const auto &u : user_set) {
              if (!first) json += ",";
              first = false;
              json += "\"";
              json += json_escape(u);
              json += "\"";
            }
            json += "]";
            id(fingerprint_user_list).publish_state(json);
          }

  - id: activate_doorbell_chime
    mode: single
    then:
      - if:
          condition:
            switch.is_on: doorbell_enabled
          then:
            - switch.turn_on: chime_relay
            - delay: !lambda 'return (uint32_t) id(chime_ring_time).state;'
            - switch.turn_off: chime_relay

  - id: restore_led_state
    mode: restart
    then:
      - delay: 500ms  # Brief delay to let animations complete
      - if:
          condition:
            switch.is_on: open_house_mode
          then:
            - fingerprint_grow.aura_led_control:
                state: ${led_open_house_on_state}
                speed: ${led_open_house_on_speed}
                color: ${led_open_house_on_color}
                count: ${led_open_house_on_count}
          else:
            - if:
                condition:
                  switch.is_on: night_light_mode
                then:
                  - fingerprint_grow.aura_led_control:
                      state: ${led_nightlight_state}
                      speed: ${led_nightlight_speed}
                      color: ${led_nightlight_color}
                      count: ${led_nightlight_count}
                else:
                  - fingerprint_grow.aura_led_control:
                      state: ALWAYS_OFF
                      speed: "0"
                      color: WHITE
                      count: "0"

  - id: unlock_door
    then:
      - fingerprint_grow.aura_led_control:
          state: ${led_match_on_state}
          speed: ${led_match_on_speed}
          color: ${led_match_on_color}
          count: ${led_match_on_count}
      - switch.turn_on: lock_relay
      - delay: 1s
      - script.execute: restore_led_state

  - id: lock_door
    then:
      - switch.turn_off: open_relay
      - switch.turn_off: lock_relay
      - fingerprint_grow.aura_led_control:
          state: ${led_match_off_state}
          speed: ${led_match_off_speed}
          color: ${led_match_off_color}
          count: ${led_match_off_count}
      - delay: 1s
      - script.execute: restore_led_state

  - id: trigger_enroll
    then:
      - script.execute:
          id: update_fingerprint_state
          new_state: "Scan finger..."
      - fingerprint_grow.aura_led_control:
          state: ${led_enroll_state}
          speed: ${led_enroll_speed}
          color: ${led_enroll_color}
          count: ${led_enroll_count}
      - lambda: |-
          int fid = -1;
          nvs_handle_t handle;
          if (nvs_open("storage", NVS_READONLY, &handle) == ESP_OK) {
            for (int i = 0; i < 200; i++) {
              std::string key = "user_" + std::to_string(i);
              size_t required = 0;
              if (nvs_get_str(handle, key.c_str(), nullptr, &required) == ESP_ERR_NVS_NOT_FOUND) {
                fid = i; break;
              }
            }
            nvs_close(handle);
          }
          id(enroll_fid) = fid;
      - if:
          condition:
            lambda: 'return id(enroll_fid) >= 0;'
          then:
            - fingerprint_grow.enroll:
                finger_id: !lambda 'return id(enroll_fid);'
                num_scans: 3
            - lambda: |-
                std::string name = id(script_username).empty() ? id(fingerprint_username).state : id(script_username);
                // ASCII sanitize before storing
                for (char &c : name) {
                  unsigned char uc = (unsigned char)c;
                  if (uc < 32 || uc > 126) c = '?';
                }
                nvs_handle_t handle;
                if (nvs_open("storage", NVS_READWRITE, &handle) == ESP_OK) {
                  std::string key = "user_" + std::to_string(id(enroll_fid));
                  nvs_set_str(handle, key.c_str(), name.c_str());
                  nvs_commit(handle);
                  nvs_close(handle);
                }
            - script.execute: update_user_list
          else:
            - script.execute:
                id: update_fingerprint_state
                new_state: "No free fingerprint slots available"

  - id: trigger_cancel_enroll
    then:
      - fingerprint_grow.cancel_enroll:
      - script.execute:
          id: erase_fingerprint_id
          finger_id: !lambda 'return id(enroll_fid);'
      - script.execute: update_user_list
      - script.execute:
          id: update_fingerprint_state
          new_state: "Enrollment canceled"
      - delay: 2s
      - script.execute: restore_led_state

  - id: trigger_delete_user
    then:
      - lambda: |-
          std::string target = id(fingerprint_username).state;
          std::transform(target.begin(), target.end(), target.begin(), ::tolower);
          for (int i = 0; i < 200; ++i) {
            nvs_handle_t handle;
            if (nvs_open("storage", NVS_READWRITE, &handle) == ESP_OK) {
              std::string key = "user_" + std::to_string(i);
              size_t required_size = 0;
              if (nvs_get_str(handle, key.c_str(), nullptr, &required_size) == ESP_OK && required_size > 1) {
                std::vector<char> buf(required_size);
                if (nvs_get_str(handle, key.c_str(), buf.data(), &required_size) == ESP_OK) {
                  std::string found = std::string(buf.data());
                  std::transform(found.begin(), found.end(), found.begin(), ::tolower);
                  if (found == target) {
                    id(fingerprint_delete_id) = i;
                    id(fingerprint_delete_script).execute();
                  }
                }
              }
              nvs_close(handle);
            }
          }
      - script.execute: update_user_list

uart:
  tx_pin: ${uart_tx_pin}
  rx_pin: ${uart_rx_pin}
  baud_rate: 57600

fingerprint_grow:
  sensing_pin: ${sensing_pin}
  password: ${fingerprint_password}
  new_password: ${new_fingerprint_password}

  on_finger_scan_misplaced:
    then:
      - script.execute: activate_doorbell_chime
      - script.execute:
          id: update_fingerprint_state
          new_state: "Finger Misplaced"
      - script.execute: reset_fingerprint_scanning
      - script.execute: restore_led_state

  on_finger_scan_invalid:
    then:
      - script.execute: activate_doorbell_chime
      - script.execute:
          id: update_fingerprint_state
          new_state: "Finger Invalid"
      - script.execute: reset_fingerprint_scanning
      - script.execute: restore_led_state

  on_finger_scan_start:
    then:
      - binary_sensor.template.publish:
          id: fingerprint_scanning
          state: ON
      - if:
          condition:
            switch.is_on: open_house_mode
          then:
            - button.press: open_door
          else:
            - script.execute:
                id: update_fingerprint_state
                new_state: "Scanning"
            - fingerprint_grow.aura_led_control:
                state: ${led_scan_state}
                speed: ${led_scan_speed}
                color: ${led_scan_color}
                count: ${led_scan_count}

  on_finger_scan_matched:
    then:
      - lambda: |-
          int fid = id(fingerprint_grow_last_finger_id).state;
          std::string name = "Authorized finger";
          nvs_handle_t handle;
          if (nvs_open("storage", NVS_READONLY, &handle) == ESP_OK) {
            std::string key = "user_" + std::to_string(fid);
            size_t required_size = 0;
            if (nvs_get_str(handle, key.c_str(), nullptr, &required_size) == ESP_OK && required_size > 1) {
              std::vector<char> buf(required_size);
              if (nvs_get_str(handle, key.c_str(), buf.data(), &required_size) == ESP_OK) {
                name = std::string(buf.data());
              }
            }
            nvs_close(handle);
          }
          // ASCII sanitize
          for (char &c : name) {
            unsigned char uc = (unsigned char)c;
            if (uc < 32 || uc > 126) c = '?';
          }
          id(last_authenticated_user).publish_state(name.c_str());
      - script.execute:
          id: update_fingerprint_state
          new_state: "Fingerprint matched"
      - if:
          condition:
            and:
              - switch.is_off: open_house_mode
              - switch.is_off: manual_mode
          then:
            - button.press: open_door
      - script.execute: reset_fingerprint_scanning

  on_finger_scan_unmatched:
    then:
      - if:
          condition:
            switch.is_off: open_house_mode
          then:
            - script.execute:
                id: update_fingerprint_state
                new_state: "Unauthorized finger"
            - script.execute: activate_doorbell_chime
      - script.execute: reset_fingerprint_scanning
      - script.execute: restore_led_state

  on_enrollment_scan:
    then:
      - fingerprint_grow.aura_led_control:
          state: ${led_enroll_scan_state}
          speed: ${led_enroll_scan_speed}
          color: ${led_enroll_scan_color}
          count: ${led_enroll_scan_count}
      - fingerprint_grow.aura_led_control:
          state: ${led_enroll_state}
          speed: ${led_enroll_speed}
          color: ${led_enroll_color}
          count: ${led_enroll_count}
      - script.execute:
          id: update_fingerprint_state
          new_state: "Finger scanned"

  on_enrollment_done:
    then:
      - fingerprint_grow.aura_led_control:
          state: ${led_enroll_done_state}
          speed: ${led_enroll_done_speed}
          color: ${led_enroll_done_color}
          count: ${led_enroll_done_count}
      - script.execute:
          id: update_fingerprint_state
          new_state: "Enrollment done"
      - delay: 3s
      - script.execute: restore_led_state

  on_enrollment_failed:
    then:
      - fingerprint_grow.aura_led_control:
          state: ${led_enroll_fail_state}
          speed: ${led_enroll_fail_speed}
          color: ${led_enroll_fail_color}
          count: ${led_enroll_fail_count}
      - script.execute:
          id: update_fingerprint_state
          new_state: "Failed to enroll fingerprint"
      - delay: 3s
      - script.execute: restore_led_state

binary_sensor:
  - platform: gpio
    pin:
      number: ${door_sensor_pin}
      mode: INPUT_PULLUP
      inverted: false
    filters:
      - delayed_on: 100ms
      - delayed_off: 100ms
    name: "Door Sensor"
    device_class: lock
    id: door_sensor
    internal: True

  - platform: template
    name: "Fingerprint Scanning Active"
    id: fingerprint_scanning

text_sensor:
  - platform: template
    id: fingerprint_state
    name: "State"
    icon: mdi:list-status
  - platform: template
    id: fingerprint_user_list
    name: "Fingerprint User List"
    icon: mdi:account-group
  - platform: template
    id: last_authenticated_user
    name: "Last Authenticated User"
    icon: mdi:account-check

sensor:
  - platform: template
    name: "Fingerprint Count"
    id: fingerprint_count
    accuracy_decimals: 0
    icon: mdi:fingerprint
  - platform: fingerprint_grow
    last_finger_id:
      name: "Last Finger ID"
      id: fingerprint_grow_last_finger_id
    last_confidence:
      name: "Last Confidence"
    status:
      name: "Status"
      disabled_by_default: True
      icon: mdi:gesture-tap
    security_level:
      name: "Security Level"
      disabled_by_default: True

text:
  - platform: template
    name: "Action User Name"
    id: fingerprint_username
    entity_category: config
    optimistic: true
    mode: text
    max_length: 32
    initial_value: ""

button:
  - platform: template
    id: open_door
    on_press:
      then:
        - switch.turn_off: lock_relay
        - delay: 250ms
        - switch.turn_on: open_relay
        - delay: !lambda 'return (uint32_t) id(lock_unlock_time).state;'
        - switch.turn_off: open_relay
        - switch.turn_on: lock_relay
        - delay: 1s
        - script.execute: restore_led_state

  - platform: template
    name: "Add Fingerprint"
    entity_category: config
    on_press:
      then:
        - lambda: |-
            if (!id(fingerprint_username).state.empty()) {
              id(script_username) = "";
              id(trigger_enroll).execute();
            }
    icon: mdi:account-plus

  - platform: template
    name: "Delete User"
    entity_category: config
    icon: mdi:account-remove
    on_press:
      then:
        - lambda: |-
            if (!id(fingerprint_username).state.empty()) {
              id(script_username) = "";
              id(trigger_delete_user).execute();
            }

  - platform: template
    name: "Cancel Enrollment"
    entity_category: config
    icon: mdi:cancel
    on_press:
      then:
        - script.execute: trigger_cancel_enroll

  - platform: template
    name: "Ring Chime"
    icon: "mdi:bell"
    on_press:
      then:
      - script.execute: activate_doorbell_chime

switch:
  - platform: gpio
    pin: ${lock_relay_pin}
    id: lock_relay
    inverted: true
    name: "Lock Relay"
    icon: "mdi:door-closed-lock"
    restore_mode: ALWAYS_OFF
    disabled_by_default: True

  - platform: gpio
    pin: ${open_relay_pin}
    id: open_relay
    inverted: true
    name: "Open Relay"
    icon: "mdi:door-closed-lock"
    restore_mode: ALWAYS_OFF
    disabled_by_default: True

  - platform: gpio
    pin: ${chime_relay_pin}
    id: chime_relay
    name: "Chime Relay"
    icon: "mdi:bell"
    restore_mode: ALWAYS_OFF
    disabled_by_default: True

  - platform: template
    name: "Doorbell Chime"
    id: doorbell_enabled
    optimistic: true
    icon: "mdi:alarm-bell"
    restore_mode: RESTORE_DEFAULT_ON

  - platform: template
    name: "Manual Mode"
    icon: "mdi:electric-switch"
    id: manual_mode
    optimistic: true
    restore_mode: RESTORE_DEFAULT_ON

  - platform: template
    name: "Night Light Mode"
    icon: "mdi:lightbulb-night"
    id: night_light_mode
    optimistic: true
    restore_mode: RESTORE_DEFAULT_OFF
    on_turn_on:
      then:
        - fingerprint_grow.aura_led_control:
            state: ${led_nightlight_state}
            speed: ${led_nightlight_speed}
            color: ${led_nightlight_color}
            count: ${led_nightlight_count}
    on_turn_off:
      then:
        - fingerprint_grow.aura_led_control:
            state: ALWAYS_OFF
            speed: "0"
            color: WHITE
            count: "0"

  - platform: template
    name: "Open House Mode"
    icon: "mdi:home-lock-open"
    id: open_house_mode
    optimistic: true
    restore_mode: RESTORE_DEFAULT_ON
    on_turn_on:
      then:
        - fingerprint_grow.aura_led_control:
            state: ${led_open_house_on_state}
            speed: ${led_open_house_on_speed}
            color: ${led_open_house_on_color}
            count: ${led_open_house_on_count}
    on_turn_off:
      then:
        - script.execute: restore_led_state

  - platform: factory_reset
    id: factrst

number:
  - platform: template
    name: "Unlock Duration"
    id: lock_unlock_time
    entity_category: config
    restore_value: true
    optimistic: true
    min_value: 100
    max_value: 10000
    step: 100
    unit_of_measurement: ms
    mode: box
    initial_value: 3000

  - platform: template
    name: "Chime Duration"
    id: chime_ring_time
    entity_category: config
    restore_value: true
    optimistic: true
    min_value: 100
    max_value: 10000
    step: 100
    unit_of_measurement: ms
    mode: box
    initial_value: 1000