HA "Kiosk" on RPi5 running HAOS

I am running HAOS on a RPI5 with 8Gig DRAM – and it seems to have memory and power to spare.
If I plug a monitor into HDMI1, I get console access to the HA CLI which is great.

I would like to plug an LCD monitor (with either mouse or touchsreen) into HDMI2 to view some of my key dashboards. This will be an easy way to check the status of thermostats, temperature & humidity sensors, etc.

This seems like a natural use case for the RPi running HA OS and would allow me to have an always-on view of key dashboards without either having to set up a separate RPi or Android device in kiosk mode or needing to open a browser on my laptop or the app on my mobile phone.

Surprisingly, I haven’t found any links to good ways to do this.

Even if native HAOS is headless, it would seem that this could be a natural use of an add-on to run a light weight window system plus browser to create a kiosk-mode on the RPi running HAOS. Basically, a kiosk in a docker container…

Is there a reason why this has not been done or at least is not popular?

This topic has been pretty thoroughly covered in other posts. For example:

Exactly – and precisely none of those references answers my question. All those links basically say that HA is meant to be a headless server or that you would need to run HA in a container or hypervisor.

Again, to repeat for clarity, I am asking whether anyone has any experience or pointers in how to run a low overhead GUI+browser in kiosk mode on top of an HAOS RPi5 installation. Whether via an add-on or via manual installation of the necessary debian packages or docker containers.

I would imagine it should definitely be possible… just a question about how challenging it would be and how best to go about it given that by default HAOS is headless.

And while some people may philosophically prefer a standalone, headless home automation server, others (like me and other past posters) would rather avoid duplicating machines along with the cost, complexity, maintenance headache, and energy waste of running multiple unnecessary machines – especially now that an RPi5 has more than enough power to do so. Indeed, it seems perfectly natural to have a dashboard displayed on the very machine that is generating the data… even if others have different needs or philosophies…

Well… As several people have mentioned, HAOS is designed to have a minimal footprint, so it will run on smaller and less powerful machines. Among the things sacrificed is a gui, so your add-on would have to make good that deficiency.

HAOS is also a complete operating system, replacing the pre-existing one, so you can’t use docker containers (there are alternatives - see “Advanced installation methods” in the link below).

I’ve no idea if an add-on is even possible, but as you say, it’s an obvious idea, so the fact that nobody has tried it suggests that if it is, it would be very difficult. More trouble that it’s worth, in other words, particularly when it’s so easy to open a browser on your phone.

People may also see the idea as not central to HA, the purpose of which is to integrate smart home devices, so they may be working on other things.

You are obviously passionate about this, so why not have a go yourself? There are tutorials about writing add-ons:

1 Like

You dont load the browser into haos. (refraining from a one does not simply… Into Mordor meme here)

You do it the other way around. Load Proxmox or some other virtualization solution. Load haos in as a guest. Load a SECOND desktop os in as a guest. Use desktop 2 to connect to HA and display to the gear.

HAOS is not intended to provide a gui, interface or anything else like that so you have to completely roll your own.

People aren’t answering the question because what you want to do isn’t exactly advisable nor usual. You’re better off using the horsepower for something else IMHO. (and yes I have a NUC10 on bare metal sitting right below a 65" TV with hdmi connected. And no I’m not even going to try. It’s way easier to cast the desktop to the browser running on the same TV and not switch inputs)

OK after a lot of learning and frustrations due to various limitations of HAOS containers, alpine distro and supported browsers, an X’s insistance on having access to /dev/tty0, I finally got it to work and have the following add-on:

Dockerfile

ARG BUILD_FROM
FROM $BUILD_FROM

# Install Luakit and necessary dependencies
RUN apk update && apk add --no-cache \
    luakit \
    xorg-server \
    xf86-video-fbdev \
    xf86-input-evdev \
    openbox \
    ttf-dejavu \
    util-linux \
    xset \
    bash \
    && rm -rf /var/cache/apk/*

# Set the display environment variable
ENV DISPLAY=:0
ENV DBUS_SESSION_BUS_ADDRESS=unix:path=/tmp/dbus-session

# Copy over 'xorg.conf' and lua 'userconf.lua' file
COPY xorg.conf /etc/X11/xorg.conf
COPY userconf.lua /root/.config/luakit/userconf.lua

COPY run.sh /
RUN chmod a+x /run.sh

CMD ["/run.sh"]

run.sh

#!/usr/bin/with-contenv bashio

################################################################################
#Get config variables
START_URL=$(bashio::config 'start_url' || echo "http://localhost:8123")
LOGIN_DELAY=$(bashio::config 'login_delay' || echo "2")
HA_USERNAME=$(bashio::config 'ha_username' || echo "")
HA_PASSWORD=$(bashio::config 'ha_password' || echo "")
BROWSER_REFRESH=$(bashio::config 'browser_refresh' || echo "30") #Default to 30 seconds
export START_URL LOGIN_DELAY HA_USERNAME HA_PASSWORD BROWSER_REFRESH #Referenced in 'userconfig.lua'

HDMI_PORT=$(bashio::config 'hdmi_port' || echo "0")
#NOTE: For now, both HDMI ports are mirrored and there is only /dev/fb0
#      Not sure how to get them unmirrored so that console can be on /dev/fb0 and X on /dev/fb1
#      As a result, setting HDMI=0 vs. 1 has no effect
SCREEN_TIMEOUT=$(bashio::config 'screen_timeout' || echo "600") #Default to 600 seconds


#Validate environment variables set by config.yaml
if [ -z "$HA_USERNAME" ] || [ -z "$HA_PASSWORD" ]; then
    echo "Error: HA_USERNAME and HA_PASSWORD must be set" >&2
    exit 1
fi

################################################################################
#Note need to delete /dev/tty0 since X won't start if it is there
#because X doesn't have permissions to access it in the container
#First, remount /dev as read-write since X absolutely, must have /dev/tty access
#Note: need to use the version in util-linux, not busybox
if [ -e "/dev/tty0" ]; then
    echo "Attempting to (temporarily) delete /dev/tty0..." >&2
    mount -o remount,rw /dev
    if [ $? -ne 0 ]; then
        echo "Failed to remount /dev as read-write..." >&2
        exit 1
    fi
    rm /dev/tty0
    if [ $? -ne 0 ]; then
        mount -o remount,ro /dev
        echo "Failed to delete /dev/tty0..." >&2
        exit 1
    fi
    TTY0_DELETED=1
fi

#Start Xorg in the background
Xorg $DISPLAY -layout Layout${HDMI_PORT} & < /dev/null

XSTARTUP=30
for ((i=0; i<=$XSTARTUP; i++)); do
  if xset q >/dev/null 2>&1; then
    break
  fi
  sleep 1
done

#Restore /dev/tty0 and 'ro' mode for /dev if deleted
if [ -n "TTY0_DELETED" ]; then
    if ! ( mknod -m 620 /dev/tty0 c 4 0 &&  mount -o remount,ro /dev ); then
        echo "Failed to restore /dev/tty0 and remount /dev/ read only..." >&2
    fi
fi

if ! xset q >/dev/null 2>&1; then
  echo "Error: X server failed to start within $XSTARTUP seconds." >&2
  exit 1
fi
echo "X started successfully..." >&2

#Stop console blinking cursor (this projects through the X-screen)
echo -e "\033[?25l" > /dev/console

#Start Openbox in the background
openbox &
O_PID=$!
sleep 0.5  #Ensure Openbox starts
if ! kill -0 "$O_PID" 2>/dev/null; then #Checks if process alive
    echo "Failed to start Openbox window manager" >&2
    exit 1
fi
echo "Openbox started successfully..." >&2

#Start D-Bus session (otherwise luakit hangs for 5 minutes befor starting)
dbus-daemon --session --address=unix:path=/tmp/dbus-session &
sleep 0.5  #Allow DBUS to initialize
export DBUS_SESSION_BUS_ADDRESS=unix:path=/tmp/dbus-session
echo "DBUS started..." >&2

#Configure screen timeout
if [ "$SCREEN_TIMEOUT" -eq 0 ]; then #Disable screen saver and DPMS for no timeout
    xset s 0
    xset dpms 0 0 0
    xset -dpms
    echo "Screen timeout disabled..." >&2
else
    xset s "$SCREEN_TIMEOUT"
    xset dpms "$SCREEN_TIMEOUT" "$SCREEN_TIMEOUT" "$SCREEN_TIMEOUT"  #DPMS standby, suspend, off
    xset +dpms
    echo "Screen timeout after $SCREEN_TIMEOUT seconds..." >&2
fi

#Run Luakit in the foreground
echo "Launching Luakit browser..." >&2
exec luakit "$START_URL"

config.yaml

name: "LocalDashboard"
description: >
  Start X server and launch dashboard from browser on local HAOS server (Jeff Kosowsky)
version: "0.9.0"
slug: "localdashboard"

arch:
  - aarch64
  - amd64
  - armhf
  - armv7
  - i386

startup: application
host_network: true
host_dbus: true
init: false

devices:
  - /dev/dri
  - /dev/fb0
  - /dev/fb1
  - /dev/input/event0
  - /dev/input/event1

privileged:
  - SYS_ADMIN

security_opt:
  - apparmor:unconfined

options:
  start_url: "http://localhost:8123"
  login_delay: 2
  hdmi_port: 0
  screen_timeout: 0
  browser_refresh: 600

schema:
  ha_username: str
  ha_password: password
  start_url: str
  login_delay: int(0,)
  hdmi_port: int(0,1)
  screen_timeout: int(0,)
  browser_refresh: int(0,)

environment:
  DISPLAY: ":0"

xorg.conf

Section "ServerLayout"
    Identifier     "Layout0"
    Screen         0 "Screen0" 0 0
    InputDevice    "Keyboard0" "CoreKeyboard"
    InputDevice    "Mouse0" "CorePointer"
EndSection

Section "ServerLayout"
Identifier     "Layout1"
Screen         0 "Screen1" 0 0
InputDevice    "Keyboard0" "CoreKeyboard"
InputDevice    "Mouse0" "CorePointer"
EndSection

Section "Device"
    Identifier    "FBDEV0"
    Driver        "fbdev"
    Option        "fbdev"  "/dev/fb0"
EndSection

Section "Device"
Identifier    "FBDEV1"
Driver        "fbdev"
Option        "fbdev"  "/dev/fb1"
EndSection

Section "Screen"
    Identifier    "Screen0"
    Device        "FBDEV"
EndSection

Section "Screen"
    Identifier    "Screen1"
    Device        "FBDEV"
EndSection

Section "InputDevice"
    Identifier    "Keyboard0"
    Driver        "evdev"
    Option        "Device" "/dev/input/event0"
    Option        "XkbLayout" "us"
    Option        "GrabDevice" "on"  #Exclusively grab device
EndSection

Section "InputDevice"
    Identifier     "Mouse0"
    Driver         "evdev"
    Option         "Device" "/dev/input/event1"
    Option         "Protocol" "auto"
    Option         "ZAxisMapping" "4 5"    # Optional, for mouse wheel
    Option         "Buttons" "5"           # Optional, configure number of buttons
EndSection

userconf.lua

local webview = require("webview")

-- Define username, password, delay, and refresh from environment variables
local username = os.getenv("HA_USERNAME")
local password = os.getenv("HA_PASSWORD")
local login_delay = tonumber(os.getenv("LOGIN_DELAY")) or 2  -- Default to 2 seconds
local start_url = os.getenv("START_URL") or "http://localhost:8123"
local browser_refresh = tonumber(os.getenv("BROWSER_REFRESH")) or 600  -- Default to 600 seconds

-- Check for required environment variables
if not username or not password then
    print("Error: HA_USERNAME and HA_PASSWORD environment variables must be set")
    os.exit(1)
end

-- Convert delays to milliseconds
local delay_ms = login_delay * 1000
if not delay_ms or delay_ms < 0 then
    print("Error: LOGIN_DELAY must be a non-negative number (in seconds)")
    os.exit(1)
end
local refresh_ms = browser_refresh * 1000
if not refresh_ms or refresh_ms < 0 then
    print("Error: BROWSER_REFRESH must be a non-negative number (in seconds)")
    os.exit(1)
end

local window = require("window")
window.add_signal("init", function(w)
    w.win.fullscreen = true
end)

webview.add_signal("init", function(view)
    -- Auto-login and refresh on every page load
    view:add_signal("load-status", function(v, status)
        if status == "finished" then
            -- Auto-login on auth page
            if v.uri:match("^" .. start_url .. "/auth/authorize%?response_type=code") then
                v:eval_js([[
                    setTimeout(function() {
                        var userField = document.querySelector('input[name="username"]');
                        var passField = document.querySelector('input[name="password"]');
                        var submitBtn = document.querySelector('mwc-button');
                        if (userField && passField && submitBtn) {
                            userField.value = "";
                            userField.dispatchEvent(new Event('input', { bubbles: true }));
                            userField.value = "]] .. username .. [[";
                            userField.dispatchEvent(new Event('input', { bubbles: true }));
                            passField.value = "";
                            passField.dispatchEvent(new Event('input', { bubbles: true }));
                            passField.value = "]] .. password .. [[";
                            passField.dispatchEvent(new Event('input', { bubbles: true }));
                            submitBtn.click();
                        }
                    }, ]] .. delay_ms .. [[);
                ]], { source = "auto_login.js" })
            end

            -- Periodic refresh of current page if refresh_ms > 0
            if refresh_ms > 0 then
                v:eval_js([[
                    // Clear any existing interval to avoid duplicates
                    if (window.refreshInterval) clearInterval(window.refreshInterval);
                    window.refreshInterval = setInterval(function() {
                        location.reload();
                    }, ]] .. refresh_ms .. [[);
                ]], { source = "auto_refresh.js" })
            end
        end
    end)
end)

To install and run add-on:

  1. Copy the above 5 files to `/addons/local_dashboard
  2. In Home Assistant, go to: Settings->Add-ons. Click on 'ADD-ON STOREand find the new add-on in theLocal add-onssection, labeledLocalDashboard`
  3. Click on it, click INSTALL
  4. Go to Configuration to enter mandatory HA username and password
  5. Click on START

Note: I have only validated this on a RPi5. Other RPi’s and devices may require modification to work…
Note: Currently there is no difference between HDMI0 and HDMI1 since HAOS mirrors them…

Put it in GitHub

Thank you for finding this solution!
(Even though everyone told you multiple times not to lol)
I’m happy to confirm this works perfectly on an old x86 tablet I’ve been using as my server for the past couple years. I’m going to see if I can get the touchscreen and stylus to work by loading /dev/input/event3 - 6

So far the most useful enhancements have been:

  1. Installing the HACS kiosk mode addon to remove the sidebar
  2. Adding a dashboard variable to navigate to something other thanlovelace/0/
  3. The tablet has a surprisingly high DPI for something I found in the trash so I’ve needed to add a luakit local settings import to zoom in the webkit browser:
local settings = require "settings"
settings.webview.zoom_level = os.getenv("ZOOM_LEVEL") 

Adding variables for start dashboard and for zoom level are great ideas.
I implemented that in my code (along with multiple improvements).
I hope to republish revised code soon on github

Among the key changes that took forever to figure out how to do was to remove the luakit status/command line a the bottom of each page (it’s like vi/vim basically) and to keep it always in insert mode so that you can type text (e.g., in the studio code server add-in) without having to enter insert mode or without accidentally leaving insert mode every time you type an escape.