Skip to content

External Keypads ESPHome

raman325 edited this page Oct 27, 2026 · 1 revision

External Keypads: ESPHome

Part of External Keypads, which explains the use_credential action these recipes call and what a valid code produces.

1. Keypad entity → validate → open

On the device, publish the code and clear it (clearing is what lets the same code be entered twice in a row):

key_collector:
  - id: pin_collector
    source_id: keypad
    min_length: 4
    max_length: 8
    end_keys: "#"
    end_key_required: true
    on_result:
      - text_sensor.template.publish:
          id: keypad_code
          state: !lambda 'return x;'
      - delay: 1s
      - text_sensor.template.publish: {id: keypad_code, state: ""}

In Home Assistant:

automation:
  - alias: "Garage: check keypad codes"
    triggers:
      - trigger: state
        entity_id: sensor.keypad_code
        not_to: [unknown, unavailable, ""]
    actions:
      - action: lock_code_manager.use_credential
        data:
          config_entry_title: House Locks
          code: "{{ trigger.to_state.state }}"
          source: sensor.keypad_code
          target: lock.garage
        response_variable: check
      - if: "{{ check.valid }}"
        then:
          - action: cover.open_cover
            target: {entity_id: cover.garage_door}

not_to: [unknown, unavailable, ""] is not optional — without it, a Wi-Fi reconnect that republishes the device's last value runs this with nobody at the keypad.

2. Let the keypad ask for itself

ESPHome 2025.10+ can capture an action's response, so the device decides locally and the code never becomes an entity state at all.

    on_result:
      - homeassistant.action:
          action: lock_code_manager.use_credential
          data_template:
            config_entry_title: House Locks
            code: "{{ entered }}"
            source: binary_sensor.garage_keypad_status
            target: lock.garage
          variables:
            entered: !lambda 'return x;'
          capture_response: true
          on_success:
            - if:
                condition:
                  lambda: 'return response["valid"].as<bool>();'
                then:
                  - switch.turn_on: garage_relay
                  - rtttl.play: "ok:d=4,o=5,b=100:c6"
                else:
                  - rtttl.play: "no:d=4,o=5,b=100:c4,c4"

source still has to name an entity — any entity belonging to the keypad will do.

3. Different feedback per rejection reason

      - choose:
          - conditions: "{{ check.valid }}"
            sequence:
              - action: tts.speak
                data: {message: "Welcome, {{ check.user }}."}
          - conditions: "{{ check.reason == 'condition_not_met' }}"
            sequence:
              - action: tts.speak
                data: {message: "That code isn't valid right now."}
          - conditions: "{{ check.reason == 'user_disabled' }}"
            sequence:
              - action: tts.speak
                data: {message: "That code has been turned off."}
        default:
          - action: tts.speak
            data: {message: "Code not recognized."}

4. A device that fires an event instead of publishing state

    on_result:
      - homeassistant.event:
          event: esphome.keypad_code_entered   # must start with "esphome."
          data_template:
            code: "{{ entered }}"
          variables:
            entered: !lambda 'return x;'

Trigger on esphome.keypad_code_entered and pass trigger.event.data.code to the action.

Clone this wiki locally