Post

Raspberry Pi Home Assistant Kiosk

Raspberry Pi Home Assistant Kiosk

Here’s how to turn a Raspberry Pi into a touchscreen kiosk running Ubuntu and Home Assistant.

This guide uses modern Wayland (Sway + Chromium). Alternatives like X11-based FullPageOS exist, but Wayland is more future-proof.

The end result is a device that:

  • Boots straight into fullscreen Home Assistant
  • Auto-starts on power-up, no login required
  • Turns the display off after idle, wakes instantly on touch
  • Supports a handy three-finger tap to reload when Wi-Fi or HA hangs

Hardware used:


Flash & Prepare Ubuntu

  1. Flash Ubuntu Server for Raspberry Pi (24.04.3 LTS or newer) using Raspberry Pi Images.
  2. Configure your user-data and network-config before first boot (see Configure Cloud Init).
  3. Boot the Pi and let cloud-init configure everything automatically.
  4. Set locale and timezone for correct time and language:
    1
    2
    
    sudo timedatectl set-timezone Europe/Berlin
    sudo update-locale LANG=en_US.UTF-8
    

Configure Cloud-init

user-data

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#cloud-config

# Hostname
hostname: ubuntu
manage_etc_hosts: true

# Default user with sudo rights
users:
  - name: ubuntu
    groups: sudo,adm
    shell: /bin/bash
    sudo: ["ALL=(ALL) ALL"]
    ssh-authorized-keys:
      - ssh-ed25519 AAAA...your-ssh-key

# Lock out password-based SSH login for security
ssh_pwauth: false

# Expire default password (forces password change if ever used)
chpasswd:
  expire: true
  users:
    - name: ubuntu
      password: ubuntu
      type: text

# Fix Wi-Fi Stability (Broadcom driver)
write_files:
  - path: /etc/modprobe.d/brcmfmac.conf
    content: |
      options cfg80211 ieee80211_regdom=DE
      options brcmfmac power_save=0
      options brcmfmac feature_disable=0x82000

network-config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
network:
  version: 2
  ethernets:
    eth0:
      optional: true
      dhcp4: true
      dhcp6: false
  wifis:
    wlan0:
      optional: false
      dhcp4: true
      dhcp6: false
      access-points:
        "your-ssid":
          password: "your-wifi-password" # pragma: allowlist secret
      regulatory-domain: DE

If cloud-init doesn’t pick up changes, reapply configs manually:

1
2
sudo rm /var/lib/cloud/data/instance-id
sudo cloud-init init --local

Secure Ubuntu (Optional)

To harden your Pi, you can run my Ansible security playbooks. This step is optional but recommended.

1
2
3
4
5
6
git clone --recursive https://github.com/MVladislav/ansible-env-setup.git
cd ansible-env-setup
python3 -m venv .venv
source .venv/bin/activate
python3 -m pip install -r requirements.txt
ansible-galaxy collection install --upgrade -r requirements.yml

Create Inventory

Replace:

  • <SSH_PRIV_KEY_FILENAME>: Path to your private ssh key e.g. ~/.ssh/example
  • <SSH_PUB_KEY_STRING>: Add the public key content e.g. cat ~/.ssh/example.pub
  • <PI_IP_ADDRESS>: IP address from your Pi
  • <PI_USER_PASSWORD>: Can be created with sudo apt install whois && mkpasswd --method=sha-512
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
tee ./inventory/inventory-pi.yml <<'EOF'
all:
  hosts:
    server:
      cis_ubuntu2404_rule_1_1_1_9: false
      cis_ubuntu2404_rule_1_3_1_2: false # no grub available
      cis_ubuntu2404_rule_3_1_1: false # no grub available
      cis_ubuntu2404_rule_6_2_1_3: false # no grub available
      cis_ubuntu2404_rule_6_2_1_4: false # no grub available
      ## ANSIBLE DEFAULTS
      ## _______________________
      ansible_ssh_private_key_file: ~/.ssh/<SSH_PRIV_KEY_FILENAME>
      pl_a_cis_setup_ssh_pub_key: "<SSH_PUB_KEY_STRING>"
      ansible_user: ubuntu
      ansible_host: <PI_IP_ADDRESS>
      ansible_port: 22
      ansible_host_hostname: "kiosk"
      pl_a_service_name: "server_"
      # NOTE: change to your required dns and ntp server, or change in playbooks
      pl_a_host_default_ntp: time.cloudflare.com
      pl_a_host_fallback_ntp: ntp.ubuntu.com
      ## ANSIBLE CLIENT ACCESS SETUP
      ## _______________________
      pl_a_user_setup: true
      pl_a_user_config:
        is_update: true
        password: "<PI_USER_PASSWORD>"
        group: sudo,adm,dip,lxd,plugdev
        system: false
        append: true
        add_ssh_key: true
      ## UPDATER
      ## _______________________
      updater_time_timezone: Europe/Berlin
      updater_ntp_server: ""
      updater_ntp_fallback_server: ""
      ## NETPLAN
      ## _______________________
      pl_a_netplan_setup: false
      netplan_remove_existing: false
      ## SSH
      ## NOTE: ssh not needed, is general setup by CIS, here only default conf to setup per user defaults
      ## _______________________
      pl_a_ssh_setup: true
      security_ssh_only_client_setup: true
      ## UFW
      ## _______________________
      ## NOTE: ufw is general setup by CIS, use here to set logging level and ssh if not setup by cis
      pl_a_ufw_setup_enable: true
      pl_a_ufw_logging_level: "off" # off | low | medium
      ## SERVICES (disable to not setup)
      ## _______________________
      pl_a_snmp_setup: false
      pl_a_nullmailer_setup: false
      pl_a_postfix_setup: false
      pl_a_hardening_scan_setup: false
      ## CIS
      ## _______________________
      pl_a_cis_setup: true
      pl_a_cis_setup_aide: false
      pl_a_cis_ipv6_required: true
      ## HARDENING SECURITY
      ## _______________________
      pl_a_auditd_setup: true
      pl_a_fail2ban_setup: true
  ## ---------------------------------------------------------------------------
  vars:
    ansible_python_interpreter: /usr/bin/python3
  children:
    servers:
      hosts:
        server:

EOF

Create PlayBook SSH-Default

Replace:

  • <SSH_PUB_KEY_STRING>: Add the public key content e.g. cat ~/.ssh/example.pub
  • <PI_IP_ADDRESS>: IP address from your Pi
1
2
3
4
5
6
7
tee ./playbooks/vars/default.yml <<'EOF'
keystodeploy:
  # id_ed25519
  "ubuntu-<PI_IP_ADDRESS>":
    - sshkey: <SSH_PUB_KEY_STRING>

EOF

Run PlayBook

1
ansible-playbook ./playbooks/playbook-sec-short.yml --ask-become-pass -k -i ./inventory/inventory-pi.yml

Install Browser & Tools

We’ll use Chromium in kiosk mode (Wayland + Sway). Firefox can also be used, but Chromium tends to run smoother on the Pi.

1
2
sudo apt update
sudo apt install -y sway swayidle wayland-utils

Install Chromium

1
sudo snap install chromium

Create a Kiosk User

1
2
3
sudo adduser kiosk
sudo usermod -aG video,input,render,audio kiosk
sudo loginctl enable-linger kiosk

Auto-Login & Auto-Start Kiosk

Enable autologin for the kiosk user:

1
2
3
4
5
6
7
sudo mkdir -p /etc/systemd/system/getty@tty1.service.d/
sudo tee /etc/systemd/system/getty@tty1.service.d/override.conf <<'EOF'
[Service]
ExecStart=
ExecStart=-/sbin/agetty --autologin kiosk --noclear %I $TERM

EOF

Systemd User Service for Sway

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
sudo -u kiosk mkdir -p /home/kiosk/.config/systemd/user
sudo -u kiosk tee /home/kiosk/.config/systemd/user/sway.service <<'EOF'
[Unit]
Description=Sway - Wayland compositor
Documentation=man:sway(5) man:sway(1)
PartOf=graphical.target
After=graphical.target

[Service]
Environment=XDG_RUNTIME_DIR=%t
Restart=always
RestartSec=2
ExecStart=/usr/bin/sway --unsupported-gpu

[Install]
WantedBy=default.target

EOF

# Enable + start service
sudo -u kiosk XDG_RUNTIME_DIR=/run/user/$(id -u kiosk) systemctl --user daemon-reload
sudo -u kiosk XDG_RUNTIME_DIR=/run/user/$(id -u kiosk) systemctl --user --now enable sway.service

Configure Sway Session

Create Sway config:

Replace placeholders:

  • <DISPLAY-OUTPUT>: your HDMI output - check possible option with:
    1
    2
    3
    4
    
    sudo -u kiosk XDG_RUNTIME_DIR=/run/user/$(id -u kiosk) \
    SWAYSOCK=$(sudo -u kiosk find /run/user/$(id -u kiosk) \
    -type s -name 'sway-ipc.*.sock' 2>/dev/null | head -n1) \
    swaymsg -t get_outputs | jq '.[] | .name'
    
  • input "8746:1:ILITEK_ILITEK-TP": your touch input - check possible option with:
    1
    2
    3
    4
    
    sudo -u kiosk XDG_RUNTIME_DIR=/run/user/$(id -u kiosk) \
    SWAYSOCK=$(sudo -u kiosk find /run/user/$(id -u kiosk) \
    -type s -name 'sway-ipc.*.sock' 2>/dev/null | head -n1) \
    swaymsg -t get_inputs | jq '.[] | select(.type == "touch") | .identifier'
    
  • 1024x600: your screen resolution - check possible option with:
    1
    2
    3
    4
    
     sudo -u kiosk XDG_RUNTIME_DIR=/run/user/$(id -u kiosk) \
    SWAYSOCK=$(sudo -u kiosk find /run/user/$(id -u kiosk) \
    -type s -name 'sway-ipc.*.sock' 2>/dev/null | head -n1) \
    swaymsg -t get_outputs
    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
sudo -u kiosk mkdir -p /home/kiosk/.config/sway
sudo -u kiosk tee /home/kiosk/.config/sway/config <<'EOF'
xwayland disable

set $mod Mod4
set $term foot
set $menu dmenu_path | wmenu | xargs swaymsg exec --
output * bg /usr/share/backgrounds/sway/Sway_Wallpaper_Blue_1920x1080.png fill

### Idle handling with dpms + touch fix ###
exec swayidle -w \
  timeout 60 'swaymsg "output * dpms off"' \
  resume     'swaymsg "output * dpms on"'

### Screen & input mapping ###
output <DISPLAY-OUTPUT> {
    resolution 1280x800
    transform 90
}
input "8746:1:ILITEK_ILITEK-TP" {
    map_to_output <DISPLAY-OUTPUT>
    tap enabled
    natural_scroll enabled
    middle_emulation enabled
}

### Cursor hiding ###
seat * hide_cursor 1000

include /etc/sway/config.d/*

EOF

sudo -u kiosk XDG_RUNTIME_DIR=/run/user/$(id -u kiosk) systemctl --user restart sway.service

Systemd User Service for Browser

Replace placeholders:

  • <URL-TO-DASHBOARD>: e.g. https://home.local:8123/lovelace/kiosk
  • wayland-1: find correct DISPLAY index - check possible option with:
    1
    
    sudo -u kiosk ls "/run/user/$(sudo -u kiosk id -u)" | grep wayland
    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
sudo -u kiosk mkdir -p /home/kiosk/.config/systemd/user

sudo -u kiosk tee /home/kiosk/.config/systemd/user/browser-kiosk.service <<'EOF'
[Unit]
Description=Browser in kiosk mode
After=graphical.target sway.service
PartOf=sway.service

[Service]
Environment=WAYLAND_DISPLAY=wayland-1
Environment=XDG_RUNTIME_DIR=%t
Restart=always
RestartSec=1
StartLimitIntervalSec=60
StartLimitBurst=5
ExecStart=/snap/bin/chromium --kiosk \
  --app=<URL-TO-DASHBOARD> \
  --enable-features=VaapiVideoDecoder,CanvasOopRasterization,UseOzonePlatform,WaylandWindowDecorations \
  --ozone-platform=wayland \
  --use-gl=egl \
  --enable-gpu-rasterization \
  --ignore-gpu-blocklist \
  --overscroll-history-navigation=0 \
  --disable-translate \
  --disable-session-crashed-bubble \
  --noerrdialogs --no-first-run --disable-infobars \
  --load-extension=/home/kiosk/reload-on-touch

[Install]
WantedBy=default.target

EOF

sudo -u kiosk tee /home/kiosk/.config/systemd/user/browser-kiosk-restart.service <<'EOF'
[Unit]
Description=Restart the browser kiosk service

[Service]
Type=oneshot
ExecStart=/usr/bin/systemctl --user restart browser-kiosk.service

EOF

sudo -u kiosk tee /home/kiosk/.config/systemd/user/browser-kiosk-restart.timer <<'EOF'
[Unit]
Description=Restart browser kiosk daily at 03:00

[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true

[Install]
WantedBy=timers.target

EOF

Add Reload-on-Touch Extension

Chromium extension for three-finger reload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
sudo -u kiosk mkdir -p /home/kiosk/reload-on-touch
sudo -u kiosk tee /home/kiosk/reload-on-touch/manifest.json <<'EOF'
{
  "manifest_version": 3,
  "name": "Reload on Touch",
  "version": "1.0",
  "permissions": ["activeTab"],
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["touch.js"]
    }
  ]
}

EOF

sudo -u kiosk tee /home/kiosk/reload-on-touch/touch.js <<'EOF'
// Reload page on three-finger tap
document.addEventListener("touchstart", (e) => {
  if (e.touches.length === 3) {
    location.reload();
  }
});

EOF

Enable and start

1
2
3
sudo -u kiosk XDG_RUNTIME_DIR=/run/user/$(id -u kiosk) systemctl --user daemon-reload
sudo -u kiosk XDG_RUNTIME_DIR=/run/user/$(id -u kiosk) systemctl --user --now enable browser-kiosk.service
sudo -u kiosk XDG_RUNTIME_DIR=/run/user/$(id -u kiosk) systemctl --user --now enable browser-kiosk-restart.timer

Disable Cloud-init (Optional)

You can also just leave it enabled; after first boot it won’t change much unless re-provisioned.

1
sudo touch /etc/cloud/cloud-init.disabled

🔧 Troubleshooting & Tips

  • Wi-Fi drops: verify dmesg | grep brcmfmac for errors, adjust options in /etc/modprobe.d/brcmfmac.conf.
  • Slow Snap apps: prefer apt-installed Chromium/Firefox when possible.
  • Check service logs on problems:
    • sudo -u kiosk XDG_RUNTIME_DIR=/run/user/$(id -u kiosk) journalctl --user -u sway.service -f
    • sudo -u kiosk XDG_RUNTIME_DIR=/run/user/$(id -u kiosk) journalctl --user -u browser-kiosk.service -f
  • Kiosk stuck or blank?
    • Switch to another TTY with Ctrl+Alt+F2 to get a shell for troubleshooting.
    • You can disable kiosk mode by editing/removing the systemd user services for debugging.
This post is licensed under CC BY 4.0 by the author.