# +----------------------------------------------------------------------------------------------------------------+ # | | # | EZHackLock | # | Smart Lock Controller Firmware | # | | # | This firmware is designed for a custom smart lock system built around the ESP32-S3. It integrates a | # | keypad, motor driver, limit switches, and status LEDs. The system communicates over a wired PoE | # | connection (W5500) and interacts with a remote door sensor via UDP for enhanced security logic, such as | # | auto-locking only when the door is confirmed closed. | # | | # | Core Features: | # | - Keypad entry with support for multiple user codes and a time-based one-time password (TOTP). | # | - Secure storage of user codes using AES-256 encryption. | # | - Remote management of user codes via a Home Assistant service call. | # | - MQTT integration for person detection (via Frigate Face Recognition) and car remote signals | # | (via rtl_433) to enable context-aware unlocking. | # | - Physical limit switches to provide definitive locked/unlocked state feedback. | # | - Audio-visual feedback using an RTTTL buzzer and multiple LEDs. | # | - Robust state machine to handle locking, unlocking, and jammed states. | # | - Failsafe logic, including motor timeouts and auto-locking. | # | | # +----------------------------------------------------------------------------------------------------------------+ # | | # | ESP32-S3-DEVKITC-1 Pinout Diagram | # | | # | +-----------------------------------------+ | # | o GND [COM] [USB-C] GND o | # | o GND 5Vin o | # | o GPIO19 +---------+ GPIO14 o Motor Left | # | o GPIO20 |5VIN[]OUT| GPIO13 o Motor Right | # | Motor Enable o GPIO21 | USB[]OTG| GPIO12 o Keypad Col 2 | # | o GPIO47 +---------+ GPIO11 o Keypad Col 3 | # | o GPIO48 [RGB] GPIO10 o Keypad Row 4 | # | o GPIO45 [LED] GPIO09 o Keypad Row 2 | # | o GPIO00 GPIO46 o | # | o GPIO35 GPIO03 o | # | o GPIO36 [ ] BOOT GPIO08 o Limit SW 2 | # | o GPIO37 GPIO18 o Keypad Row 1 | # | o GPIO38 [ ] RST GPIO17 o Keypad Row 3 | # | W5500 CS o GPIO39 GPIO16 o Keypad Col 1 | # | W5500 MISO o GPIO40 GPIO15 o Numpad Blue LED | # | W5500 INT o GPIO41 GPIO07 o RTTTL Buzzer | # | W5500 CLK o GPIO42 GPIO06 o Limit SW 1 | # | o GPIO02 +-------------+ GPIO05 o EZSET Green | # | o GPIO01 | ESP32-S3 | GPIO04 o EZSET Red | # | W5500 MOSI o RX GPIO44 | WROOM-1 | RST o | # | W5500 RST o TX GPIO43 | N16R8 | 3V3 o | # | o GND | | 3V3 o | # | +-------------+-------------+-------------+ | # | || | _ _ || | # | ||_|_| |_| |_|| | # | +-------------+ | # | | # +----------------------------------------------------------------------------------------------------------------+ globals: - id: entered_code type: std::string restore_value: false initial_value: '""' - id: last_door_update_time type: uint32_t initial_value: "0" - id: door_state_valid type: bool initial_value: "false" - id: person_detected_time type: uint32_t initial_value: "0" - id: known_person_name type: std::string initial_value: '""' - id: intentional_unlock type: bool initial_value: "false" - id: motor_active_time type: uint32_t initial_value: "0" - id: motor_direction type: std::string initial_value: '""' # This global variable will hold the encrypted list of codes in flash storage. - id: ${encrypted_storage_id} type: std::string restore_value: yes # +-----------------------------------+ # | Number of Codes by Length | # +-------------+---------------------+-------------------------------------+ # | Code Length | Max Number of Codes | Calculation Example (for max codes) | # +-------------+---------------------+-------------------------------------+ # | 4-Digit | 63B: 12 > 254B: 51 | (51 x 4) + 50 commas = 254 chars | # | 6-Digit | 63B: 9 > 254B: 36 | (36 x 6) + 35 commas = 251 chars | # | 7-Digit | 63B: 8 > 254B: 31 | (31 x 7) + 30 commas = 247 chars | # | 8-Digit | 63B: 7 > 254B: 28 | (28 x 8) + 27 commas = 251 chars | # +-------------+---------------------+-------------------------------------+ max_restore_data_length: 254 # 254 is the maximum allowed and safest choice for a long list. - id: aes_key_global type: uint8_t[32] # 32 for AES-256 or 16 for AES-128 restore_value: true - id: aes_iv_global type: uint8_t[16] # IV is always 16 bytes for AES restore_value: true substitutions: # This makes the global variable name easier to change if needed encrypted_storage_id: stored_encrypted_codes aes_key: !secret aes_key012 aes_iv: !secret aes_iv012 totp_secret: !secret totp_secret012 esp32: board: esp32-s3-devkitc-1 flash_size: 16MB framework: #type: arduino type: esp-idf version: 5.4.2 platform_version: 54.03.21 sdkconfig_options: CONFIG_COMPILER_OPTIMIZATION_SIZE: y CONFIG_LWIP_MAX_SOCKETS: "8" #CONFIG_MBEDTLS_HKDF_C: y esphome: name: ezhacklock comment: ESP32-S3 • RAM 8.4% (27,480/327,680) • Flash 13.4% (1,086,980/8,126,464) friendly_name: EZHackLock on_boot: priority: 600 then: - light.turn_on: motor_enable # Enable motor at startup - lambda: |- // Get the secret strings and copy them into the global byte arrays. const char* aes_key_str = "${aes_key}"; const char* aes_iv_str = "${aes_iv}"; memcpy(id(aes_key_global), aes_key_str, 32); memcpy(id(aes_iv_global), aes_iv_str, 16); platformio_options: build_flags: "-DBOARD_HAS_PSRAM" board_build.arduino.memory_type: qio_opi #lib_deps: # - "https://github.com/kokke/tiny-AES-c.git" includes: - aes.hpp - external_components: # - source: github://NonaSuomy/HAP-ESPHome@Fixes # refresh: 0s - source: type: local path: components psram: mode: octal speed: 80MHz debug: update_interval: 5s logger: level: DEBUG #logs: # udp: VERY_VERBOSE #sensor: NONE # api.service: NONE # api.connection: NONE # scheduler: NONE # ledc.output: NONE # light: NONE # rtttl: NONE # json: NONE # text_sensor: NONE # binary_sensor: VERY_VERBOSE api: encryption: key: !secret encryption_key012 services: - service: play_rtttl variables: song: string then: - rtttl.play: rtttl: !lambda 'return song;' # Service to update the list of user codes. It takes a comma-separated string, # encrypts it, and stores it on the device. # Developer Tools -> Actions -> ezhacklock_update_user_codes -> codes_list: 12345678,7654321 -> Perform action - service: update_user_codes variables: codes_list: string then: - lambda: |- // Validate input if (codes_list.empty()) { ESP_LOGE("main", "Error: codes_list cannot be empty"); return; } // Check for invalid characters (only digits and commas allowed) for (char c : codes_list) { if (c != ',' && (c < '0' || c > '9')) { ESP_LOGE("main", "Error: codes_list contains invalid character '%c'. Only digits and commas allowed.", c); return; } } // Check for consecutive commas or leading/trailing commas if (codes_list.front() == ',' || codes_list.back() == ',' || codes_list.find(",,") != std::string::npos) { ESP_LOGE("main", "Error: Invalid comma placement in codes_list"); return; } // Validate individual codes (4-8 digits each) std::stringstream ss(codes_list); std::string code; while (std::getline(ss, code, ',')) { if (code.length() < 4 || code.length() > 8) { ESP_LOGE("main", "Error: Code '%s' must be 4-8 digits long", code.c_str()); return; } } ESP_LOGI("main", "Validation passed. Encrypting codes..."); struct AES_ctx ctx; uint8_t key[32]; uint8_t iv[16]; memcpy(key, id(aes_key_global), 32); memcpy(iv, id(aes_iv_global), 16); size_t input_len = codes_list.length(); size_t padding = 16 - (input_len % 16); size_t buffer_size = input_len + padding; std::vector buffer(buffer_size); memcpy(buffer.data(), codes_list.c_str(), input_len); for (size_t i = 0; i < padding; i++) { buffer[input_len + i] = padding; } AES_init_ctx_iv(&ctx, key, iv); AES_CBC_encrypt_buffer(&ctx, buffer.data(), buffer.size()); std::string hex_string; hex_string.reserve(buffer.size() * 2); for (uint8_t byte : buffer) { char hex[3]; sprintf(hex, "%02x", byte); hex_string += hex; } id(stored_encrypted_codes) = hex_string; ESP_LOGI("main", "Successfully saved encrypted user codes. Size: %d bytes", buffer.size()); # Service to read and decrypt the stored codes for debugging purposes. # Developer Tools -> Actions -> ezhacklock_read_decrypted_codes -> Preform action # Codes will show in the ESPHome console. - service: read_decrypted_codes then: - lambda: |- if (id(stored_encrypted_codes).empty()) { ESP_LOGI("main", "No encrypted codes stored."); return; } std::string hex_string = id(stored_encrypted_codes); std::vector buffer; buffer.reserve(hex_string.length() / 2); for (size_t i = 0; i < hex_string.length(); i += 2) { std::string byte_string = hex_string.substr(i, 2); uint8_t byte = (uint8_t) strtol(byte_string.c_str(), nullptr, 16); buffer.push_back(byte); } struct AES_ctx ctx; uint8_t key[32], iv[16]; memcpy(key, id(aes_key_global), 32); memcpy(iv, id(aes_iv_global), 16); AES_init_ctx_iv(&ctx, key, iv); AES_CBC_decrypt_buffer(&ctx, buffer.data(), buffer.size()); size_t len = buffer.size(); if (len > 0) { size_t padding = buffer[len - 1]; if (padding > 0 && padding <= 16) { len -= padding; } } std::string decrypted(buffer.begin(), buffer.begin() + len); ESP_LOGI("main", "Decrypted codes: %s", decrypted.c_str()); ota: - platform: esphome password: !secret ota_pass012 #wifi: # ssid: !secret wifi_ssid2 # password: !secret wifi_password2 # use_address: !secret use_address_wifi2_012 #ap: # ssid: "EZHackLock Fallback Hotspot" # password: !secret fallbackhotspot012 ethernet: type: W5500 clk_pin: GPIO42 #GPIO12 mosi_pin: GPIO44 #GPIO21 miso_pin: GPIO40 #GPIO16 cs_pin: GPIO39 #GPIO10 interrupt_pin: GPIO41 #GPIO14 reset_pin: GPIO43 #GPIO09 use_address: !secret use_address_wired012 # Optional manual IP #manual_ip: # static_ip: 10.20.30.42 # gateway: 10.20.30.40 # subnet: 255.255.255.0 #captive_portal: #web_server: # port: 80 udp: - id: door_udp port: 18511 packet_transport: - platform: udp id: door_packet_transport udp_id: door_udp providers: - name: ttgo-poe-001 time: - platform: sntp id: sntp_time servers: - 0.pool.ntp.org - 1.pool.ntp.org - 2.pool.ntp.org timezone: America/Toronto sun: latitude: !secret home_latitude longitude: !secret home_longitude totp: secret: ${totp_secret} time_id: sntp_time id: my_totp totp: name: TOTP countdown: name: Countdown # spi: # clk_pin: GPIO18 # miso_pin: GPIO17 # mosi_pin: GPIO16 # pn532_spi: # id: nfc_spi_module # cs_pin: GPIO15 # update_interval: 100ms # on_tag: # then: # lambda: |- # ESP_LOGI("INFO", "My Tag ID is: %s", x.c_str()); # if(x == YOUR_TAG_ID) { # if (id(cupboard_lock).state == LOCK_STATE_LOCKED) { # id(cupboard_lock).unlock(); # } else { # id(cupboard_lock).lock(); # } # } #homekit_base: # setup_code: '133-71-337' #homekit: # lock: # - id: ez_hack_lock #nfc_id: nfc_spi_module #on_hk_success: # lambda: |- # ESP_LOGI("INFO", "IssuerID: %s", x.c_str()); # ESP_LOGI("INFO", "EndpointID: %s", y.c_str()); # if (id(ez_hack_lock).state == LOCK_STATE_LOCKED) { # id(ez_hack_lock).unlock(); # } else { # id(ez_hack_lock).lock(); # } #on_hk_fail: # lambda: |- # ESP_LOGI("ERROR", "Authorizing HomeKit lock failed"); #hk_hw_finish: "SILVER" mqtt: broker: !secret mqtt_broker username: !secret mqtt_username password: !secret mqtt_password client_id: !secret mqtt_client_id on_message: - topic: "double-take/cameras/hqbackyardcam" then: - lambda: |- auto json = parse_json(x, [](JsonObject root) { if (root["matches"].is()) { JsonArray matches = root["matches"].as(); if (matches.size() > 0) { JsonObject first_match = matches[0].as(); if (first_match["match"].is() && first_match["match"].as() && first_match["confidence"].is() && first_match["confidence"].as() > 95.0) { // Known person detected with high confidence id(person_detected_time) = millis(); id(person_at_door).publish_state(true); id(known_person_name) = first_match["name"].as(); id(detected_person_name).publish_state(id(known_person_name)); // Stop any existing timeout script id(detection_timeout).stop(); // Light up green LED for known person id(ezset_green_light).turn_on(); id(ezset_red_light).turn_off(); // Start the detection timeout id(detection_timeout).execute(); ESP_LOGI("MQTT", "Known person detected: %s (%.2f%%)", id(known_person_name).c_str(), first_match["confidence"].as()); } else { // Unknown person or low confidence match id(person_detected_time) = millis(); id(person_at_door).publish_state(false); id(known_person_name) = "unknown"; id(detected_person_name).publish_state("unknown"); // Keep normal lock indication (red if locked) id(update_lock_indicator).execute(); ESP_LOGI("MQTT", "Unknown person or low confidence detection"); } } } return true; }); - topic: "rtl_433/generic_car_remote/events" then: - lambda: |- auto json = parse_json(x, [](JsonObject root) { if (root["command_name"].is() && root["id"].is() && strcmp(root["command_name"], "LOCK") == 0 && strcmp(root["id"], "000022e1356c") == 0) { // Car remote lock detected id(person_detected_time) = millis(); id(person_at_door).publish_state(true); id(known_person_name) = "Car Remote"; id(detected_person_name).publish_state("Car Remote"); // Stop any existing timeout script id(detection_timeout).stop(); // Light up green LED for car remote lock id(ezset_green_light).turn_on(); id(ezset_red_light).turn_off(); // Start the detection timeout id(detection_timeout).execute(); ESP_LOGI("MQTT", "Car remote lock detected from device: %s", root["id"].as()); } return true; }); lock: - platform: template name: "EZ Hack Lock" id: ez_hack_lock icon: "mdi:lock" optimistic: false lock_action: - lambda: |- id(motor_active_time) = millis(); id(motor_direction) = "locking"; - script.execute: lock_door unlock_action: - lambda: |- id(motor_active_time) = millis(); id(motor_direction) = "unlocking"; - script.execute: unlock_door lambda: |- if (id(limitswitch1).state) { id(motor_direction) = ""; return LOCK_STATE_LOCKED; } else if (id(limitswitch2).state) { id(motor_direction) = ""; return LOCK_STATE_UNLOCKED; } else if (id(motor_direction) != "") { // Motor is active, check for timeout if ((millis() - id(motor_active_time)) > 5000) { // 5 second timeout id(motor_direction) = ""; return LOCK_STATE_JAMMED; } // Return appropriate transitional state if (id(motor_direction) == "locking") { return LOCK_STATE_LOCKING; } else { return LOCK_STATE_UNLOCKING; } } else { // No limit switch active and no motor movement return LOCK_STATE_JAMMED; } light: - platform: monochromatic name: "EZSET LED Green" output: ezset_led_green id: ezset_green_light restore_mode: ALWAYS_OFF default_transition_length: 0.5s - platform: monochromatic name: "EZSET LED Red" output: ezset_led_red id: ezset_red_light restore_mode: ALWAYS_OFF default_transition_length: 0.5s - platform: monochromatic name: "Numpad LED Blue" output: numpad_led_blue id: numpad_blue_light restore_mode: ALWAYS_OFF default_transition_length: 0.5s - platform: monochromatic name: "Enable Motor" output: motor_enable_pin id: motor_enable restore_mode: ALWAYS_ON # This ensures the motor is enabled at startup text_sensor: - platform: template name: "Person Detected Name" id: detected_person_name lambda: |- return id(known_person_name); sensor: - platform: uptime name: "Lock Uptime" update_interval: 60s #- platform: wifi_signal # name: "Lock WiFi Signal" # update_interval: 60s - platform: debug free: name: "Heap Free" #fragmentation: # name: "Heap Fragmentation" block: name: "Heap Max Block" loop_time: name: "Loop Time" psram: name: "Free PSRAM" - platform: packet_transport transport_id: door_packet_transport id: uptime_check name: "UDP Uptime Check" provider: ttgo-poe-001 internal: true on_value: then: - lambda: |- id(last_door_update_time) = millis(); id(door_state_valid) = true; //ESP_LOGI("UDP", "Door status update received, timestamp updated"); #- logger.log: # format: "Door state valid: %s, Time since last update: %dms" # args: ['id(door_state_valid) ? "true" : "false"', 'millis() - id(last_door_update_time)'] - platform: template name: "Time Since Last Update" id: last_update_time update_interval: 5s unit_of_measurement: "ms" icon: "mdi:timer" lambda: |- return millis() - id(last_door_update_time); binary_sensor: - platform: packet_transport transport_id: door_packet_transport id: zone_01 # This is the local ID for the received state name: "Remote Door State" provider: ttgo-poe-001 # Name of the provider device internal: false on_state: then: # - logger.log: # format: "Received UDP state change: %s" # args: ['ONOFF(x)'] # level: DEBUG # - logger.log: # format: "Door physical state: %s, zone_01 state: %s" # args: ['id(zone_01).state ? "OPEN" : "CLOSED"', 'id(zone_01).state ? "ON" : "OFF"'] - if: condition: and: - binary_sensor.is_off: zone_01 # Door is closed - binary_sensor.is_off: limitswitch1 # Lock is disengaged then: - delay: 2s # Ensure door remains closed - if: condition: and: - binary_sensor.is_off: zone_01 # Check again - binary_sensor.is_off: limitswitch1 # Ensure still unlocked then: - script.execute: lock_door # Left limit switch from knob side (On means microswitch is pressed) - platform: gpio pin: number: GPIO06 inverted: false name: "Limit Switch 1" id: limitswitch1 on_press: then: - output.turn_off: motor_left - output.turn_off: motor_right - rtttl.play: 'short_beep:d=4,o=5,b=100:16e6' - light.turn_on: id: ezset_red_light brightness: 100% transition_length: 0.5s - light.turn_off: id: ezset_green_light transition_length: 0.5s - delay: 5s - light.turn_off: id: ezset_red_light transition_length: 2s # Right limit switch from knob side (On means microswitch is pressed) - platform: gpio pin: number: GPIO08 inverted: false name: "Limit Switch 2" id: limitswitch2 on_press: then: - delay: 200ms - output.turn_off: motor_left - output.turn_off: motor_right - rtttl.play: 'short_beep:d=4,o=5,b=100:16e6' - light.turn_off: id: ezset_red_light transition_length: 0.5s - light.turn_on: id: ezset_green_light brightness: 100% transition_length: 0.5s - delay: 5s - light.turn_off: id: ezset_green_light transition_length: 2s # Matrix Keypad Keys - platform: matrix_keypad keypad_id: keypad001 row: 0 col: 0 name: "Key 1" on_press: then: - script.execute: id: handle_keypad_press key: "1" - platform: matrix_keypad keypad_id: keypad001 row: 0 col: 1 name: "Key 2" on_press: then: - script.execute: id: handle_keypad_press key: "2" - platform: matrix_keypad keypad_id: keypad001 row: 0 col: 2 name: "Key 3" on_press: then: - script.execute: id: handle_keypad_press key: "3" - platform: matrix_keypad keypad_id: keypad001 row: 1 col: 0 name: "Key 4" on_press: then: - script.execute: id: handle_keypad_press key: "4" - platform: matrix_keypad keypad_id: keypad001 row: 1 col: 1 name: "Key 5" on_press: then: - script.execute: id: handle_keypad_press key: "5" - platform: matrix_keypad keypad_id: keypad001 row: 1 col: 2 name: "Key 6" on_press: then: - script.execute: id: handle_keypad_press key: "6" - platform: matrix_keypad keypad_id: keypad001 row: 2 col: 0 name: "Key 7" on_press: then: - script.execute: id: handle_keypad_press key: "7" - platform: matrix_keypad keypad_id: keypad001 row: 2 col: 1 name: "Key 8" on_press: then: - script.execute: id: handle_keypad_press key: "8" - platform: matrix_keypad keypad_id: keypad001 row: 2 col: 2 name: "Key 9" on_press: then: - script.execute: id: handle_keypad_press key: "9" - platform: matrix_keypad keypad_id: keypad001 row: 3 col: 0 name: "Key 0" on_press: then: - script.execute: id: handle_keypad_press key: "0" - platform: matrix_keypad keypad_id: keypad001 row: 3 col: 1 name: "Key E (EZSet)" on_press: then: - script.execute: id: handle_keypad_press key: "E" - platform: matrix_keypad keypad_id: keypad001 row: 3 col: 2 name: "Key * (Dummy)" on_press: then: - script.execute: id: handle_keypad_press key: "*" - platform: template name: "Door State Valid" id: door_state_valid_sensor lambda: |- uint32_t time_since_update = millis() - id(last_door_update_time); bool is_valid = id(door_state_valid) && (time_since_update < 20000); if (!is_valid && id(door_state_valid)) { // ESP_LOGW("Door", "Door state validation timeout after %dms", time_since_update); id(door_state_valid) = false; } return is_valid; - platform: template name: "Person Detection Active" id: person_detection_active lambda: |- return (millis() - id(person_detected_time)) < 30000; - platform: template name: "Known Person at Door" id: person_at_door lambda: |- return id(known_person_name) != "unknown"; # Add web UI buttons for testing button: - platform: template name: "Move Left Until Limit" on_press: - output.turn_off: motor_right - output.turn_on: motor_left - while: condition: binary_sensor.is_off: limitswitch1 then: - delay: 0.1s - output.turn_off: motor_left - platform: template name: "Move Right Until Limit" on_press: - output.turn_off: motor_left - output.turn_on: motor_right - while: condition: binary_sensor.is_off: limitswitch2 then: - delay: 0.1s - output.turn_off: motor_right #- platform: homekit_base # factory_reset: # name: "Reset Homekit pairings" matrix_keypad: - id: keypad001 columns: - pin: GPIO16 - pin: GPIO12 - pin: GPIO11 rows: - pin: GPIO18 - pin: GPIO09 - pin: GPIO17 - pin: GPIO10 keys: 123456789*E0 key_collector: - id: pincode_reader min_length: 4 max_length: 8 end_keys: "E" end_key_required: true allowed_keys: "0123456789" timeout: 30s on_progress: - lambda: |- id(entered_code) = x; on_result: - lambda: |- std::string user_entered_code = x; id(entered_code) = ""; ESP_LOGI("keypad", "User entered code: '%s'", user_entered_code.c_str()); if (!id(stored_encrypted_codes).empty()) { ESP_LOGI("keypad", "Checking against encrypted codes..."); std::string hex_string = id(stored_encrypted_codes); std::vector buffer; buffer.reserve(hex_string.length() / 2); for (size_t i = 0; i < hex_string.length(); i += 2) { std::string byte_string = hex_string.substr(i, 2); uint8_t byte = (uint8_t) strtol(byte_string.c_str(), nullptr, 16); buffer.push_back(byte); } struct AES_ctx ctx; uint8_t key[32], iv[16]; memcpy(key, id(aes_key_global), 32); memcpy(iv, id(aes_iv_global), 16); AES_init_ctx_iv(&ctx, key, iv); AES_CBC_decrypt_buffer(&ctx, buffer.data(), buffer.size()); size_t len = buffer.size(); if (len > 0) { size_t padding = buffer[len - 1]; if (padding > 0 && padding <= 16) { len -= padding; } } std::string decrypted_list(buffer.begin(), buffer.begin() + len); ESP_LOGI("keypad", "Decrypted list: '%s'", decrypted_list.c_str()); std::string search_list = "," + decrypted_list + ","; std::string code_to_find = "," + user_entered_code + ","; ESP_LOGI("keypad", "Searching for: '%s' in '%s'", code_to_find.c_str(), search_list.c_str()); if (search_list.find(code_to_find) != std::string::npos) { ESP_LOGI("keypad", "User code accepted."); id(success_indicator).execute(); id(unlock_lock_sequence).execute(); return; } else { ESP_LOGI("keypad", "Code not found in list."); } } ESP_LOGI("keypad", "Checking TOTP..."); id(check_totp_code).execute(user_entered_code); on_timeout: - lambda: |- id(entered_code) = ""; script: - id: check_lock_state then: - if: condition: binary_sensor.is_off: limitswitch2 then: # - logger.log: "WARNING: Lock not fully engaged!" - script.execute: error_indicator - id: update_lock_indicator mode: restart then: - if: condition: binary_sensor.is_on: person_at_door then: # Don't change LED states if a person is detected - lambda: |- ESP_LOGD("LED", "Skipping LED update - person detected"); else: # Normal LED update logic when no person detected - if: condition: binary_sensor.is_on: limitswitch1 # Locked state then: # Show red for locked state - light.turn_on: id: ezset_red_light brightness: 100% transition_length: 0.5s - light.turn_off: id: ezset_green_light transition_length: 0.5s - delay: 5s - light.turn_off: id: ezset_red_light transition_length: 2s else: # Show green for unlocked state - light.turn_off: id: ezset_red_light transition_length: 0.5s - light.turn_on: id: ezset_green_light brightness: 100% transition_length: 0.5s - delay: 5s - light.turn_off: id: ezset_green_light transition_length: 2s - id: safety_timeout then: - output.turn_off: motor_left - output.turn_off: motor_right - lambda: |- id(motor_direction) = ""; - script.execute: error_indicator #- logger.log: # format: "Safety timeout triggered - motor stopped" # level: DEBUG - id: unlock_door then: - output.turn_on: motor_right - output.turn_off: motor_left - lambda: |- id(motor_active_time) = millis(); id(motor_direction) = "unlocking"; - wait_until: condition: binary_sensor.is_on: limitswitch2 timeout: 5s - delay: 200ms # Add delay to ensure full movement - output.turn_off: motor_right - output.turn_off: motor_left - lambda: |- id(motor_direction) = ""; - script.execute: update_lock_indicator - if: condition: binary_sensor.is_on: limitswitch2 then: # - logger.log: "Door unlocked" - script.execute: update_lock_indicator else: - script.execute: safety_timeout # - logger.log: "Failed to unlock door" - id: lock_door then: - if: condition: and: - binary_sensor.is_on: door_state_valid_sensor - binary_sensor.is_off: zone_01 then: # Safe to proceed with locking # - logger.log: "Starting lock motor sequence" - output.turn_off: motor_right - output.turn_on: motor_left - lambda: |- id(motor_active_time) = millis(); id(motor_direction) = "locking"; - wait_until: condition: binary_sensor.is_on: limitswitch1 timeout: 5s - delay: 200ms # Add delay to ensure full movement - output.turn_off: motor_left - output.turn_off: motor_right - lambda: |- id(motor_direction) = ""; - script.execute: update_lock_indicator - if: condition: binary_sensor.is_on: limitswitch1 then: # - logger.log: "Door successfully locked" - script.execute: update_lock_indicator else: - script.execute: safety_timeout # - logger.log: "Failed to lock door - timeout reached" else: # Either invalid state or door is open - if: condition: binary_sensor.is_off: door_state_valid_sensor then: # - logger.log: "Cannot lock - Door state unknown!" ###- script.execute: error_indicator else: # - logger.log: "Cannot lock - Door state invalid or door is open!" ###- script.execute: error_indicator - id: auto_lock_delay then: - delay: 30s - if: condition: #and: # - binary_sensor.is_on: limitswitch2 # Door is unlocked # - binary_sensor.is_off: zone_01 # Door is closed binary_sensor.is_on: limitswitch2 then: - script.execute: lock_door # - logger.log: "Auto-lock activated" - id: unlock_lock_sequence then: - script.execute: unlock_door - script.execute: auto_lock_delay - light.turn_off: id: ezset_green_light transition_length: 2s # - logger.log: "Unlock sequence completed with auto-lock scheduled" - id: relock_sequence then: - script.execute: unlock_door - delay: 4s - script.execute: lock_door #- logger.log: "Relock sequence completed" - id: detection_timeout then: # First ensure green LED is on and red is off at start - light.turn_on: id: ezset_green_light brightness: 100% transition_length: 0.5s - light.turn_off: id: ezset_red_light transition_length: 0.5s - delay: 30s # 30 second timeout - if: condition: lambda: |- return !id(intentional_unlock); then: # Only run error script if unlock wasn't intentional - script.execute: error_indicator - lambda: |- id(known_person_name) = ""; id(detected_person_name).publish_state(std::string()); - light.turn_off: id: ezset_green_light transition_length: 2s - script.execute: update_lock_indicator - lambda: |- id(intentional_unlock) = false; // Use C++ style comment inside lambda - id: handle_keypad_press parameters: key: string then: - rtttl.play: 'short_beep:d=4,o=5,b=100:16e6' - script.execute: blue_led_action # Show current lock state and start fade timer - if: condition: binary_sensor.is_on: limitswitch1 # Locked state then: - light.turn_on: id: ezset_red_light brightness: 100% transition_length: 0.5s - light.turn_off: id: ezset_green_light transition_length: 0.5s else: - light.turn_off: id: ezset_red_light transition_length: 0.5s - light.turn_on: id: ezset_green_light brightness: 100% transition_length: 0.5s # Start the LED fade timer script - script.execute: led_fade_timer # Handle keypad input - lambda: |- if (!key.empty()) { char pressed_key = key[0]; if (pressed_key >= '0' && pressed_key <= '9') { id(entered_code) = id(entered_code) + pressed_key; id(pincode_reader).send_key(pressed_key); } else if (pressed_key == 'E') { if (id(entered_code) == "\"\"" || id(entered_code).length() == 0) { id(failure_indicator).execute(); } else { id(pincode_reader).send_key('E'); id(entered_code) = "\"\""; } } } # Handle EZSet button for person detection - if: condition: lambda: |- if (!key.empty()) { char pressed_key = key[0]; return pressed_key == 'E' && id(person_at_door).state && id(known_person_name) != "unknown" && id(detection_timeout).is_running(); } return false; then: - script.stop: detection_timeout - lambda: |- ESP_LOGI("Keypad", "EZSet pressed by known person: %s", id(known_person_name).c_str()); id(intentional_unlock) = true; - if: condition: binary_sensor.is_off: limitswitch2 then: - script.execute: success_indicator - script.execute: unlock_lock_sequence else: - script.execute: success_indicator - script.execute: lock_door - id: led_fade_timer mode: restart then: - delay: 5s - if: condition: binary_sensor.is_on: limitswitch1 then: - light.turn_off: id: ezset_red_light transition_length: 2s else: - light.turn_off: id: ezset_green_light transition_length: 2s - id: blue_led_action then: - light.turn_on: id: numpad_blue_light brightness: 100% transition_length: 0.1s - delay: 10s - light.turn_off: id: numpad_blue_light transition_length: 2s - id: green_led_action then: - light.turn_on: id: ezset_green_light brightness: 100% transition_length: 0.5s - delay: 5s - if: condition: binary_sensor.is_off: person_detection_active then: - light.turn_off: id: ezset_green_light transition_length: 2s - script.execute: update_lock_indicator - id: red_led_action then: - light.turn_on: id: ezset_red_light brightness: 100% transition_length: 0.5s - delay: 5s - if: condition: binary_sensor.is_off: person_detection_active then: - script.execute: update_lock_indicator - id: success_indicator then: - delay: 100ms - rtttl.stop - rtttl.play: 'success_beep:d=4,o=5,b=100:16e7,16e7,16p,16e7,16p,16c7,16e7,16p,16g7' - repeat: count: 3 then: - light.turn_on: id: ezset_green_light brightness: 100% - delay: 200ms - light.turn_off: ezset_green_light - delay: 200ms - id: failure_indicator then: - light.turn_off: ezset_green_light - light.turn_off: ezset_red_light - delay: 100ms - rtttl.stop #- rtttl.play: '"zelda:d=64,o=6,b=300,c=4, g,a,b,f#,g,a,b,f#,b,c7,d7,b,c7,d7,a,b,f#,a,b,f#,c7,d7,e7,c7,d7,e7,b,f#,f7,c7,d7,e7,e,f#,f7,g7,d7,e7,e,a5,b5,c6"' #- rtttl.play: 'zelda:d=16,o=5,b=75,c=4, g,a,b,f#,g,a,b,f#,b,c6,d6,b,c6,d6,a,b,f#,a,b,f#,c6,d6,e6,c6,d6,e6,b,f#,f6,c6,d6,e6,e,f#,f6,g6,d6,e6,e,a4,b4,c' - rtttl.play: 'failure_beep:d=16,o=5,b=100:c,c6,a4,a5,a#4,a#5' - repeat: count: 3 then: - light.turn_on: id: ezset_red_light brightness: 100% transition_length: 0.1s - delay: 200ms - light.turn_off: id: ezset_red_light transition_length: 0.1s - delay: 200ms #- light.turn_off: ezset_red_light - id: error_indicator then: - light.turn_off: ezset_green_light - light.turn_off: ezset_red_light - delay: 100ms - rtttl.stop - rtttl.play: 'error_beep:d=8,o=5,b=100:d,e' - repeat: count: 3 then: - light.turn_on: id: ezset_red_light brightness: 100% transition_length: 0.1s - light.turn_on: id: ezset_green_light brightness: 100% transition_length: 0.1s - delay: 200ms - light.turn_off: id: ezset_red_light transition_length: 0.1s - light.turn_off: id: ezset_green_light transition_length: 0.1s - delay: 200ms - id: check_totp_code parameters: code: std::string then: - lambda: |- std::string current_totp = id(my_totp).get_current_totp(); if (code == current_totp) { ESP_LOGI("keypad", "TOTP code accepted."); id(success_indicator).execute(); id(unlock_lock_sequence).execute(); } else { ESP_LOGW("keypad", "Invalid code."); id(failure_indicator).execute(); } - id: request_door_status then: - lambda: |- //ESP_LOGI("Door", "Requesting door status update"); id(door_state_valid) = false; id(door_state_valid_sensor).publish_state(false); # Add a small delay to ensure UDP packet can be sent - delay: 100ms # Wait for response - wait_until: condition: binary_sensor.is_on: door_state_valid_sensor timeout: 3s - if: condition: binary_sensor.is_on: door_state_valid_sensor then: #- logger.log: # format: "Door status received and validated: %s" # args: ['id(zone_01).state ? "CLOSED" : "OPEN"'] else: # - logger.log: "Warning: No response to door status request" - lambda: |- id(door_state_valid) = false; id(door_state_valid_sensor).publish_state(false); output: - platform: ledc pin: GPIO07 id: rtttl_out - platform: ledc pin: GPIO05 id: ezset_led_green inverted: True - platform: ledc pin: GPIO04 id: ezset_led_red inverted: True - platform: ledc pin: GPIO15 id: numpad_led_blue inverted: False - platform: ledc pin: GPIO13 id: motor_right - platform: ledc pin: GPIO14 id: motor_left - platform: ledc pin: GPIO21 id: motor_enable_pin inverted: False rtttl: output: rtttl_out gain: 60% # on_finished_playback: # - logger.log: 'RTTTL ended!'