Status TV: Setup from scratch
How to rebuild the Status TV (or set up a second one) on a fresh Raspberry Pi. The content of every file mentioned below is on Status TV Config files. Those are exact copies of what runs on the production Pi.
Faster way: the devops-status-tv repository has an Ansible playbook that does steps 2 and 4–6 for you. Do step 1 (flash the Pi) and step 3 (service account) by hand, point
inventory.iniat the new Pi, fill.envfrom Proton Pass, and run./deploy.sh --diff. The manual steps below explain what the playbook does.
Tested with Raspberry Pi 4 Model B (4 GB), Raspberry Pi OS (Debian 13 "trixie", 64-bit, desktop), labwc 0.20, Chromium 154, nginx 1.26 and Grafana 12.4.
1. Prepare the Pi
- Flash Raspberry Pi OS (64-bit) with desktop using Raspberry Pi Imager.
In the Imager settings, set:
- hostname
status-tv - user
statusadminand a password (store it in Proton Pass) - SSH enabled (password authentication)
- timezone
Europe/Helsinki, keyboardfi
- hostname
- Boot it on the wired network. Make sure the desktop logs in
automatically. That's the default on Pi OS; otherwise run
sudo raspi-config→ System → Boot / Auto login → Desktop Autologin. - Screen blanking must be off. That's the default on current Pi OS. Check
with
raspi-config nonint get_blanking:1means it's disabled. If it isn't, runsudo raspi-config nonint do_blanking 1.
2. Fix the clock (do this first)
The Pi has no battery-backed clock, and public NTP (UDP 123) is blocked on
this network, so out of the box the clock is days off. A wrong clock breaks
both apt (signature errors like "not live until …") and Grafana: relative
times such as now-12h come from the browser's clock, so the TV would show
old data.
The internal DNS servers also serve NTP. Create
/etc/systemd/timesyncd.conf.d/10-local-ntp.conf (content on the config page),
then:
sudo systemctl restart systemd-timesyncd
timedatectl # → "System clock synchronized: yes"
3. Grafana service account
In Grafana (needs the Admin role):
- Administration → Users and access → Service accounts → Add service account
- Name:
indola-status-tv(or similar), Role: Viewer
- Name:
- Add service account token, with expiry Never (or a long expiry and a reminder to renew it). Store the token in Proton Pass right away, because Grafana shows it only once.
- If a dashboard is in a folder with restricted permissions, give the service account View permission on that folder.
Test the token:
curl -s -H "Authorization: Bearer <token>" https://dash.aholadigital.com/api/user
# → "login":"sa-1-indola-status-tv" ...
4. nginx token proxy
sudo apt-get update && sudo apt-get install -y nginx
- Create
/etc/nginx/grafana-token.confwith the token from Proton Pass (template on the config page). It must be readable by root only:sudo chown root:root /etc/nginx/grafana-token.conf sudo chmod 600 /etc/nginx/grafana-token.conf - Create
/etc/nginx/sites-available/status-tv(content on the config page), then enable it and disable the default site:sudo ln -sf /etc/nginx/sites-available/status-tv /etc/nginx/sites-enabled/status-tv sudo rm -f /etc/nginx/sites-enabled/default # don't expose port 80 sudo nginx -t && sudo systemctl enable --now nginx && sudo systemctl reload nginx curl -s http://127.0.0.1:3000/api/user # → sa-1-indola-status-tv
What the proxy config does:
| Directive | Why |
|---|---|
listen 127.0.0.1:3000 |
Only the Pi itself can use the token |
include grafana-token.conf |
Adds Authorization: Bearer … to every request |
Host / Origin = dash.aholadigital.com |
Grafana checks Origin on POST requests (queries) and Live websockets |
Cookie "" |
Ignores any browser session cookie, so the token is always used |
Upgrade / Connection, proxy_read_timeout 3600s |
Grafana Live websockets |
proxy_ssl_verify on |
Verifies Grafana's TLS certificate |
5. Screens (kanshi)
Create ~/.config/kanshi/config (content on the config page). It has two
profiles: single (only HDMI-A-2 connected) and dual (both screens). kanshi
picks the matching one automatically at login and when a screen is plugged in
or out. On a Pi 4, HDMI-A-1 is the micro-HDMI port next to the USB-C power
port.
Check the result with WAYLAND_DISPLAY=wayland-0 wlr-randr.
6. Kiosk
Create these files as user statusadmin (content on the config page):
| File | Purpose |
|---|---|
~/status-tv/kiosk.sh (chmod 755) |
Runs one Chromium kiosk and restarts it if it exits |
~/status-tv/restart.sh (chmod 755) |
Restarts all kiosks listed in urls.conf |
~/status-tv/urls.conf |
Which URL each screen shows |
~/.config/autostart/status-tv-left.desktop |
Starts the left kiosk at login |
~/.config/autostart/status-tv-right.desktop |
Starts the right kiosk at login |
~/.config/labwc/rc.xml |
Pins each kiosk window to its HDMI output |
How the pieces fit together:
urls.confhas one line per screen:<kiosk name> <url>. The URLs are kept in a separate file because Grafana URLs contain$__all, which a shell silently replaces with an empty string.kiosk.sh <name>waits until the proxy responds, then runs Chromium in--kioskmode with its own profile (~/.config/chromium-<name>) and--class=<name>, which becomes the window's Wayland app_id. It loops forever, so Chromium comes back if it crashes.rc.xmlhas labwc window rules: app_idstatus-tv-leftgoes toHDMI-A-1andstatus-tv-righttoHDMI-A-2. labwc on Pi OS runs with-m, so this file is merged with/etc/xdg/labwc/rc.xmlinstead of replacing it.
If you create new playlists, put their UIDs in urls.conf. You can list them
on the Pi with curl -s http://127.0.0.1:3000/api/playlists.
Load the window rules and start everything without rebooting:
LABWC_PID=$(pgrep -x labwc) WAYLAND_DISPLAY=wayland-0 labwc --reconfigure
~/status-tv/restart.sh
7. Test with a reboot
sudo reboot
About a minute later both screens should show their playlists. Check from your laptop:
ssh statusadmin@10.10.75.9 '
timedatectl | grep synchronized
systemctl is-active nginx
pgrep -fa "kiosk.s[h]"
WAYLAND_DISPLAY=wayland-0 wlr-randr | grep -E "^\S|current|Position"'
8. Recommended
- A case with a heatsink or fan. With two dashboards the Pi runs at about 70 °C, and it starts slowing itself down at about 80 °C.
- SSH keys instead of the password: see SSH access with your own key.