Step-by-Step Guide to Building a Keypad for Admin Access on Your Wall Panel

Home Assistant is a powerful open-source tool for home automation, but as you start to build more complex setups, you may encounter challenges. One of these challenges is creating an intuitive and accessible interface that is also secure and child-proof. This guide is here to help.

Inspired by a desire to manage the intricate details of a Home Assistant (HASS) installation from an Amazon Fire HD 10 wall panel, without exposing critical controls to all users (especially curious little ones), I’ve developed a solution that combines the aesthetics of an alarm panel card with the functionality of a comprehensive control system. By the end of this guide, you’ll know how to create a system that lets you easily switch between different screens and hide sensitive controls from non-admin users with proper password protection.

:shield: Security Caveat: It’s important to note that this solution was not designed to function as a high-security lock, like that of a gun safe! Initial feedback highlighted considerations such as fingerprints on the screen, lock-out functionality based on bad attempts or device tracking, dynamic code cycling, and so forth. However, the primary goal here was to prevent a curious child or guest from accessing your admin screens, not to thwart a determined burglar. Therefore, while this solution serves as a useful interface management tool, it should not be relied upon for serious security purposes. Employ this at your own discretion and always assess the potential risks!

  1. The Sensors

The sensors for your home assistant keypad are vital components that drive the functionality of the system. They consist of two helper sensors and a template sensor.

  1. Keypad Input (input_text.keypad_input): This is a helper sensor, created in the UI under Settings > Devices & Services > Helpers, that serves as the input field for each button press on the keypad. It’s configured with an icon of a dialpad (mdi:dialpad) and has a minimum length of 0 and a maximum length of 100. The display mode is set to Password to conceal the input for privacy (in certain places). It is enabled and visible in the Home Assistant User Interface (HASS UI). You can add this to your own installation by creating a new helper in the HASS UI, selecting the ‘Text’ type, and configuring it with the aforementioned properties.
  2. Keypad Code (input_text.keypad_code): This helper sensor is the ‘correct code’ against which the Keypad Input is compared to determine if the correct PIN has been entered. It shares similar properties with the Keypad Input, including the dialpad icon, and a minimum and maximum length of 0 and 100, respectively. Although it’s enabled, it’s not visible in the HASS UI for security reasons. To add this to your own installation, follow the same steps as the Keypad Input but ensure that it’s not visible in the UI.
  3. Keypad Input Masked (sensor.keypad_input_masked): This is a template sensor that masks the value of the Keypad Input on the screen to maintain privacy. It uses the below template that calculates the length of the entered input and returns the same number of asterisks (*). This effectively hides the true input from view. Copy the below template to the ‘sensor’ configuration in your config.yaml. This will ensure that any entered input is masked with asterisks.
  - sensor:
    - name: Keypad Input Masked
      unique_id: keypad_input_masked
      state: >
        {% set input_length = states('input_text.keypad_input')|length %}
        {{ '*' * input_length }}

There’s one more sensor, Kiosk Additional Info (input_select.kiosk_additional_info), that we’ll delve into in more detail in Section 5. As we explore the use of conditional cards and how they interact with our solution, we’ll examine how this sensor plays a pivotal role in managing which screen is displayed, enabling a dynamic and adaptable interface.

  1. The Config Script

The keypad_input config script is a crucial piece of the home assistant keypad setup. This script is responsible for managing the input received from the keypad. It can be included in the main config.yaml file of your Home Assistant setup or in a separate script file using the !include method.

Here’s how it works:

When a number is pressed on the keypad, this script is called. The script’s sequence involves a single service call to input_text.set_value, which sets the value of input_text.keypad_input. The value is determined by a data template.

The data template first fetches the current value of input_text.keypad_input. If the current value is neither ‘None’ nor ‘unknown’, it appends the pressed number (converted to a string) to the current value. If the current value is ‘None’ or ‘unknown’, it simply sets the value to the pressed number.

The script runs in ‘single’ mode, which means it doesn’t allow concurrent runs. If the script is triggered while it is still running, the second trigger will be ignored. This means that rapid button pushes on the keypad may not take; you’ll need to adjust your entry speed accordingly.

To add this script to your own installation, you can paste it directly into your config.yaml file under the script: key. If you have a separate scripts file, you can use the !include method in your config.yaml to reference that file.

script:
  keypad_input:
    alias: Keypad Input
    sequence:
      - service: input_text.set_value
        data_template:
          entity_id: input_text.keypad_input
          value: >
            {% set current_value = states('input_text.keypad_input') %}
            {% if current_value != 'None' and current_value != 'unknown' %}
              {{ current_value ~ number | string }}
            {% else %}
              {{ number | string }}
            {% endif %}
    mode: single
  1. The Lovelace Card

The Lovelace card for your Home Assistant keypad is a vertical-stack card, which stacks multiple cards vertically. This creates an organized, cohesive interface for user interaction. Note that this configuration requires u/thomasloven’s lovelace-card-mod and u/RomRider’s button-card, both of which are available through HACS (Home Assistant Community Store).

  1. Entity Card: The first card in the stack displays the state of the ‘keypad_input_masked’ sensor, which shows the masked input. It features a dialpad icon and the name “Passcode Required”. The card has a custom style that removes the background and border, centers the text, and reduces the margin at the bottom for cleanliness.
  2. Grid Card: The second card in the stack is a grid card with three columns. It contains a total of 12 custom button cards that make up the numeric keypad, a clear button, and a submit button.
  3. Numeric Buttons: Each numeric button card (from ‘1’ to ‘9’ and ‘0’) calls a service (script.keypad_input) when tapped, passing the corresponding number as service data. They have a dialpad numeric-circle icon, a gray color scheme, and their visual effect when pressed is disabled.
  4. Clear Button: The clear button card, represented by an ‘X’, calls a different service (input_text.set_value) when tapped, which clears the keypad input.
  5. Submit Button: The last button card is the submit button. Its icon color changes based on whether the entered code matches the correct code. If they match, the color turns green; if not, it stays gray. Note that this button doesn’t perform an action when tapped. Instead, we’ve harnessed the power of automations to unlock access to the protected screens. More on that in Section 4.

To add this to your own installation, navigate to the Lovelace UI editor in Home Assistant, create a new card of the type ‘Manual’, and paste the YAML code provided below. Ensure you have the necessary custom button-card component installed and the correct scripts and entities in place. The custom button-card component can be installed through HACS (Home Assistant Community Store).

type: vertical-stack
cards:
  - type: entity
    entity: sensor.keypad_input_masked
    icon: mdi:dialpad
    name: Passcode Required
    style: |
      ha-card { 
        --ha-card-background: none;
        border: none;
        text-align: center;
        margin-bottom: -20px
      }
  - type: grid
    columns: 3
    square: false
    cards:
      - type: custom:button-card
        show_name: false
        show_icon: true
        aspect_ratio: 1/.75
        tap_action:
          action: call-service
          service: script.keypad_input
          service_data:
            number: '1'
        icon: mdi:numeric-1-circle
        color: '#bdbdbd'
        size: 100%
        styles:
          card:
            - background: none
            - border: none
            - '--mdc-ripple-press-opacity': 0
      - type: custom:button-card
        show_name: false
        show_icon: true
        aspect_ratio: 1/.75
        tap_action:
          action: call-service
          service: script.keypad_input
          service_data:
            number: '2'
        icon: mdi:numeric-2-circle
        color: '#bdbdbd'
        size: 100%
        styles:
          card:
            - background: none
            - border: none
            - '--mdc-ripple-press-opacity': 0
      - type: custom:button-card
        show_name: false
        show_icon: true
        aspect_ratio: 1/.75
        tap_action:
          action: call-service
          service: script.keypad_input
          service_data:
            number: '3'
        icon: mdi:numeric-3-circle
        color: '#bdbdbd'
        size: 100%
        styles:
          card:
            - background: none
            - border: none
            - '--mdc-ripple-press-opacity': 0
      - type: custom:button-card
        show_name: false
        show_icon: true
        aspect_ratio: 1/.75
        tap_action:
          action: call-service
          service: script.keypad_input
          service_data:
            number: '4'
        icon: mdi:numeric-4-circle
        color: '#bdbdbd'
        size: 100%
        styles:
          card:
            - background: none
            - border: none
            - '--mdc-ripple-press-opacity': 0
      - type: custom:button-card
        show_name: false
        show_icon: true
        aspect_ratio: 1/.75
        tap_action:
          action: call-service
          service: script.keypad_input
          service_data:
            number: '5'
        icon: mdi:numeric-5-circle
        color: '#bdbdbd'
        size: 100%
        styles:
          card:
            - background: none
            - border: none
            - '--mdc-ripple-press-opacity': 0
      - type: custom:button-card
        show_name: false
        show_icon: true
        aspect_ratio: 1/.75
        tap_action:
          action: call-service
          service: script.keypad_input
          service_data:
            number: '6'
        icon: mdi:numeric-6-circle
        color: '#bdbdbd'
        size: 100%
        styles:
          card:
            - background: none
            - border: none
            - '--mdc-ripple-press-opacity': 0
      - type: custom:button-card
        show_name: false
        show_icon: true
        aspect_ratio: 1/.75
        tap_action:
          action: call-service
          service: script.keypad_input
          service_data:
            number: '7'
        icon: mdi:numeric-7-circle
        color: '#bdbdbd'
        size: 100%
        styles:
          card:
            - background: none
            - border: none
            - '--mdc-ripple-press-opacity': 0
      - type: custom:button-card
        show_name: false
        show_icon: true
        aspect_ratio: 1/.75
        tap_action:
          action: call-service
          service: script.keypad_input
          service_data:
            number: '8'
        icon: mdi:numeric-8-circle
        color: '#bdbdbd'
        size: 100%
        styles:
          card:
            - background: none
            - border: none
            - '--mdc-ripple-press-opacity': 0
      - type: custom:button-card
        show_name: false
        show_icon: true
        aspect_ratio: 1/.75
        tap_action:
          action: call-service
          service: script.keypad_input
          service_data:
            number: '9'
        icon: mdi:numeric-9-circle
        color: '#bdbdbd'
        size: 100%
        styles:
          card:
            - background: none
            - border: none
            - '--mdc-ripple-press-opacity': 0
      - type: custom:button-card
        show_name: false
        show_icon: true
        aspect_ratio: 1/.75
        tap_action:
          action: call-service
          service: input_text.set_value
          service_data:
            entity_id: input_text.keypad_input
            value: ''
        icon: mdi:alpha-x-circle-outline
        color: '#f87272'
        size: 100%
        styles:
          card:
            - background: none
            - border: none
            - '--mdc-ripple-press-opacity': 0
      - type: custom:button-card
        show_name: false
        show_icon: true
        aspect_ratio: 1/.75
        tap_action:
          action: call-service
          service: script.keypad_input
          service_data:
            number: '0'
        icon: mdi:numeric-0-circle
        color: '#bdbdbd'
        size: 100%
        styles:
          card:
            - background: none
            - border: none
            - '--mdc-ripple-press-opacity': 0
      - type: custom:button-card
        show_name: false
        show_icon: true
        aspect_ratio: 1/.75
        tap_action:
          action: none
        icon: mdi:check-circle-outline
        size: 100%
        styles:
          card:
            - background: none
            - border: none
            - '--mdc-ripple-press-opacity': 0
          icon:
            - color: |
                [[[
                  if (states['input_text.keypad_input'].state == states['input_text.keypad_code'].state) {
                    return "#8dc869";  //green
                  } else {
                    return "#bdbdbd";  //gray
                  }
                ]]]
  1. The Automation

The “Kiosk Admin” automation performs key tasks in managing the keypad and admin screens, ensuring that they are not left active for more than three minutes, and unlocking the admin screen when the entered keypad input matches the stored keypad code.

This automation triggers based on three conditions:

  1. Hide Keypad: When the kiosk_additional_info state changes to ‘keypad’ and stays for 3 minutes.
  2. Hide Admin: When the kiosk_additional_info state changes to ‘admin’ and stays for 3 minutes.
  3. Authorized: When the keypad input (input_text.keypad_input) matches the stored keypad code (input_text.keypad_code).

On triggering, the automation performs the following actions based on the trigger condition:

  1. For ‘hide keypad’ and ‘hide admin’ triggers: The kiosk_additional_info state is set to ‘main’, effectively hiding the keypad and admin screens. The keypad input is also cleared to ensure no leftover data remains.
  2. For ‘authorized’ trigger: The kiosk_additional_info state is set to ‘admin’, unlocking the admin screen. The keypad input is then cleared for security and readiness for the next input.

This automation runs in ‘single’ mode, which means if the automation is triggered again while it’s still running from a previous trigger, the new trigger will be ignored.

To add this automation to your own Home Assistant setup, you can paste this YAML code in your automations.yaml file, or use the automation editor in the Home Assistant UI. Make sure the entities (input_text.keypad_input, input_text.keypad_code, input_select.kiosk_additional_info) exist in your setup.

alias: Kiosk Admin
description: ""
trigger:
  - platform: state
    entity_id:
      - input_select.kiosk_additional_info
    to: keypad
    for:
      hours: 0
      minutes: 3
      seconds: 0
    id: hide keypad
  - platform: state
    entity_id:
      - input_select.kiosk_additional_info
    to: admin
    for:
      hours: 0
      minutes: 3
      seconds: 0
    id: hide admin
  - platform: template
    value_template: >-
      {{ states('input_text.keypad_input') == states('input_text.keypad_code')
      }}
    id: authorized
condition: []
action:
  - choose:
      - conditions:
          - condition: trigger
            id: hide keypad
          - condition: trigger
            id: hide admin
        sequence:
          - service: input_select.select_option
            data:
              option: main
            target:
              entity_id: input_select.kiosk_additional_info
          - service: input_text.set_value
            data:
              value: ""
            target:
              entity_id: input_text.keypad_input
      - conditions:
          - condition: trigger
            id: authorized
        sequence:
          - service: input_select.select_option
            data:
              option: admin
            target:
              entity_id: input_select.kiosk_additional_info
          - service: input_text.set_value
            data:
              value: ""
            target:
              entity_id: input_text.keypad_input
mode: single
  1. Conditional Cards

Conditional cards in Home Assistant are a powerful tool for creating dynamic, responsive dashboards that can change their displayed content based on the state of certain entities. In this case, the input_select.kiosk_additional_info entity is being used to determine which screen to display. Of note, I personally reserve half of my main display for conditional content such as a summary of key sensors, a local weather report, and a media controller.

The input_select.kiosk_additional_info entity is a Helper entity, specifically an Input Select Helper. Input Select Helpers allow you to define a list of options, and you can set or get the selected option via services or in the frontend via a dropdown.

The example conditional card below checks the state of input_select.kiosk_additional_info, and if the state is ‘admin’, it displays a vertical stack card with a series of controls and information relevant to the ‘admin’ view.

type: vertical-stack
cards:
  - type: conditional
    conditions:
      - entity: input_select.kiosk_additional_info
        state: admin
    card:
      type: vertical-stack
      cards: [...]

The use of an Input Select Helper here, as opposed to an Input Text Helper, adds a user-friendly dropdown interface in the Home Assistant UI, making it easier to switch between screens without needing to remember or manually type the exact state string. This is especially useful when debugging or if you have a large number of possible screens, but can also slow you down if you’re creating and testing new screens on the fly.

To set up similar functionality in your own Home Assistant instance, you would create the necessary Helper entities (such as input_select.kiosk_additional_info) with the appropriate options for your use case, then create conditional cards for each option that display the content you want for that state. Then, by changing the state of the Helper entity, either manually in the UI or automatically through an automation or script, you can control which screen is displayed.

Conclusion

Building a secure, flexible, and user-friendly interface for Home Assistant is a multifaceted task, but with the right approach, it’s entirely achievable. By leveraging Helper entities, scripts, automation, and conditional cards, you can create a control system that adapts to your needs, hides sensitive controls from unauthorized users, and even adds an extra layer of security with a password-protected admin screen.

Remember, the best part about Home Assistant is its flexibility! If this setup doesn’t perfectly suit your needs, you can adjust, expand, or simplify as you see fit. Hopefully, this guide serves as a good starting point for your journey into more advanced Home Assistant interfaces.

Note: This guide was originally posted to Reddit

4 Likes

Extraordinary work and documentation. Thank you.

You could radically reduce the code in custom button Lovelace card using button card templates. See my TV remote card example here … using templates you could just pass in one variable which is the number.