Skip to content

Status TV: Config files

Exact copies of every file deployed on the Status TV Pi (statusadmin@10.10.75.9). ~ is /home/statusadmin. See Status TV Setup for where each file fits in.

The source of truth is the devops-status-tv repository (pi/ folder), deployed with Ansible. If you change a file there, update this page too.

~/status-tv/kiosk.sh

Owner: statusadmin, mode 755.

#!/bin/bash
# Status TV kiosk: one Chromium window showing Grafana via the local token
# proxy (nginx on 127.0.0.1:3000). Usage: kiosk.sh <name>  (URL from urls.conf)
# <name> becomes the Wayland app_id; ~/.config/labwc/rc.xml uses it to place
# the window on the right screen. Started from ~/.config/autostart/status-tv-*.desktop;
# restarts Chromium if it exits.
NAME="$1"
URL=$(awk -v n="$NAME" '$1 == n { print $2 }' "$(dirname "$0")/urls.conf")
PROFILE="$HOME/.config/chromium-$NAME"

# Wait for the proxy (and network) to be up
for i in $(seq 1 60); do
    curl -s -o /dev/null -m 3 http://127.0.0.1:3000/api/health && break
    sleep 2
done

while true; do
    # Avoid "Chromium didn't shut down correctly" bubble after power loss
    sed -i 's/"exited_cleanly":false/"exited_cleanly":true/; s/"exit_type":"[^"]*"/"exit_type":"Normal"/' \
        "$PROFILE/Default/Preferences" 2>/dev/null
    chromium \
        --kiosk "$URL" \
        --class="$NAME" \
        --user-data-dir="$PROFILE" \
        --ozone-platform=wayland \
        --noerrdialogs \
        --disable-infobars \
        --no-first-run \
        --disable-session-crashed-bubble \
        --disable-features=Translate \
        --password-store=basic \
        --check-for-update-interval=31536000 \
        --overscroll-history-navigation=0 \
        --disable-pinch
    sleep 5
done

~/status-tv/restart.sh

Owner: statusadmin, mode 755.

#!/bin/bash
# Restart both Status TV kiosk windows (e.g. after editing urls.conf).
export XDG_RUNTIME_DIR=/run/user/$(id -u) WAYLAND_DISPLAY=wayland-0
DIR=$(dirname "$(readlink -f "$0")")
pkill -f "$DIR/kiosk.sh"
pkill -x chromium
sleep 3
for name in $(awk '!/^#/ && NF { print $1 }' "$DIR/urls.conf"); do
    setsid -f "$DIR/kiosk.sh" "$name" >/tmp/$name.log 2>&1 < /dev/null
    sleep 2
done

~/status-tv/urls.conf

Owner: statusadmin, mode 644.

# Status TV dashboards: <kiosk name> <url>  (read by kiosk.sh, no shell expansion)
# Grafana is reached via the local token proxy on 127.0.0.1:3000.
# Each screen plays a Grafana playlist (edit contents/interval in Grafana:
# Dashboards -> Playlists). Dashboards use their saved time range/variables.
status-tv-left  http://127.0.0.1:3000/playlists/play/hetzv9p?kiosk
status-tv-right http://127.0.0.1:3000/playlists/play/hetrgxv?kiosk

# Previous single-dashboard links:
# status-tv-left  http://127.0.0.1:3000/d/ef693154-9727-4c56-a7be-3e151fd0f11d/tms-stability-monitor?orgId=1&from=now-24h&to=now&timezone=browser&var-deploy_type=stable&var-active_pubsub_subs=$__all&refresh=1m&kiosk
# status-tv-right http://127.0.0.1:3000/d/6752404e-fa3e-427b-b2a5-41362fee85d8/eazytms?orgId=1&from=now-12h&to=now&timezone=Europe%2FHelsinki&var-deploy=stable&var-service=$__all&var-server_ips=$__all&refresh=30s&kiosk

~/.config/autostart/status-tv-left.desktop

Owner: statusadmin, mode 644.

[Desktop Entry]
Type=Application
Name=Status TV kiosk (left: TMS stability monitor)
Exec=/home/statusadmin/status-tv/kiosk.sh status-tv-left
X-GNOME-Autostart-enabled=true

~/.config/autostart/status-tv-right.desktop

Owner: statusadmin, mode 644.

[Desktop Entry]
Type=Application
Name=Status TV kiosk (right: eazytms)
Exec=/home/statusadmin/status-tv/kiosk.sh status-tv-right
X-GNOME-Autostart-enabled=true

~/.config/labwc/rc.xml

Owner: statusadmin, mode 644.

<?xml version="1.0"?>
<!-- Merged on top of /etc/xdg/labwc/rc.xml (labwc runs with -m).
     Status TV: pin each kiosk window to its screen (see ~/status-tv/kiosk.sh). -->
<labwc_config>
  <windowRules>
    <windowRule identifier="status-tv-left">
      <action name="MoveToOutput" output="HDMI-A-1" />
    </windowRule>
    <windowRule identifier="status-tv-right">
      <action name="MoveToOutput" output="HDMI-A-2" />
    </windowRule>
  </windowRules>
</labwc_config>

~/.config/kanshi/config

Owner: statusadmin, mode 644.

# One screen (current Samsung on HDMI-A-2)
profile single {
    output HDMI-A-2 enable mode 2560x1440@59.951 position 0,0
}

# Two screens: HDMI-A-1 left, HDMI-A-2 right (swap positions if physically reversed)
profile dual {
    output HDMI-A-1 enable mode 2560x1440 position 0,0
    output HDMI-A-2 enable mode 2560x1440@59.951 position 2560,0
}

/etc/nginx/sites-available/status-tv

Owner: root, mode 644; symlinked from /etc/nginx/sites-enabled/status-tv.

# Status TV: local-only proxy to Grafana that injects the read-only
# service-account token (sa-1-indola-status-tv). Chromium kiosk uses it.
map $http_upgrade $status_tv_connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 127.0.0.1:3000;
    server_name localhost;

    location / {
        proxy_pass https://dash.aholadigital.com;
        proxy_ssl_server_name on;
        proxy_ssl_verify on;
        proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;

        proxy_set_header Host dash.aholadigital.com;
        proxy_set_header Origin https://dash.aholadigital.com;
        proxy_set_header Cookie "";
        include /etc/nginx/grafana-token.conf;

        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $status_tv_connection_upgrade;
        proxy_read_timeout 3600s;
    }
}

/etc/nginx/grafana-token.conf

Owner: root, mode 600. Template only: the real token is in Proton Pass.

# Copy to /etc/nginx/grafana-token.conf on the Pi (root:root, mode 600) and
# replace the placeholder with the Grafana service-account token.
# NEVER commit the real token.
proxy_set_header Authorization "Bearer glsa_REPLACE_ME";

/etc/systemd/timesyncd.conf.d/10-local-ntp.conf

Owner: root, mode 644.

[Time]
NTP=172.31.0.20 172.31.0.17

~/status-tv/tv-power.sh

Owner: statusadmin, mode 755.

#!/bin/bash
# Switch the Status TV screens on/off over HDMI-CEC (Samsung "Anynet+" must be on).
#   tv-power.sh on | off | status | auto
# auto = apply the schedule from TV_ON / TV_OFF / TV_DAYS (set by the systemd unit):
#   on between TV_ON and TV_OFF on TV_DAYS (date +%u: 1=Mon … 7=Sun, range "1-5"), else off.
# One /dev/cecN per Pi HDMI port: cec0 = HDMI-A-1 (left TV), cec1 = HDMI-A-2 (right TV).
set -u
ACTION="${1:-status}"
log() { echo "$(date '+%F %T') $*"; }

# Register as a playback device (not persistent across reboots) and print the
# physical address the TV gave this port, e.g. 1.0.0.0
setup() {
    cec-ctl -d "$1" --playback --osd-name StatusTV >/dev/null 2>&1
    cec-ctl -d "$1" 2>/dev/null | awk '/Physical Address/ { print $4; exit }'
}

state() {
    timeout 8 cec-ctl -d "$1" --to 0 --give-device-power-status 2>/dev/null \
        | awk '/pwr-state:/ { print $2; exit }'
}

power_on() {  # wake the TV and switch it to the Pi's input
    cec-ctl -d "$1" --to 0 --image-view-on >/dev/null 2>&1
    cec-ctl -d "$1" --to 15 --active-source phys-addr="$2" >/dev/null 2>&1
}

power_off() {  # Samsung ignores standby unless the Pi is the active source
    cec-ctl -d "$1" --to 15 --active-source phys-addr="$2" >/dev/null 2>&1
    sleep 2
    cec-ctl -d "$1" --to 0 --standby >/dev/null 2>&1
}

if [ "$ACTION" = auto ]; then
    day=$(date +%u) now=$(date +%H:%M)
    first=${TV_DAYS%-*} last=${TV_DAYS#*-}
    if [ "$day" -ge "$first" ] && [ "$day" -le "$last" ] \
       && [[ ! "$now" < "$TV_ON" ]] && [[ "$now" < "$TV_OFF" ]]; then
        ACTION=on
    else
        ACTION=off
    fi
    log "schedule: days $TV_DAYS $TV_ON-$TV_OFF, now day $day $now -> $ACTION"
fi

rc=0
for dev in /dev/cec*; do
    phys=$(setup "$dev")
    if [ -z "$phys" ] || [ "$phys" = "f.f.f.f" ]; then
        log "$dev: no TV connected"
        continue
    fi
    case "$ACTION" in
        on)     want=on;      power_on "$dev" "$phys" ;;
        off)    want=standby; power_off "$dev" "$phys" ;;
        status) log "$dev ($phys): $(state "$dev")"; continue ;;
        *)      echo "usage: $0 on|off|status|auto" >&2; exit 2 ;;
    esac
    # Verify, retry once
    for try in 1 2; do
        sleep 8
        now_state=$(state "$dev")
        [ "$now_state" = "$want" ] && break
        [ "$try" = 1 ] && { [ "$want" = on ] && power_on "$dev" "$phys" || power_off "$dev" "$phys"; }
    done
    log "$dev ($phys): $ACTION -> ${now_state:-no reply}"
    [ "$now_state" = "$want" ] || rc=1
done
exit $rc

/etc/systemd/system/status-tv-tv-power.service

Owner: root, mode 644. Ansible template: values come from inventory.ini.

# Managed by devops-status-tv (Ansible) – schedule in inventory.ini
[Unit]
Description=Status TV: switch TVs on/off by schedule (HDMI-CEC)
After=multi-user.target

[Service]
Type=oneshot
User={{ ansible_user }}
Environment=TV_ON={{ tv_on_time }} TV_OFF={{ tv_off_time }} TV_DAYS={{ tv_days }}
ExecStart={{ home }}/status-tv/tv-power.sh auto

/etc/systemd/system/status-tv-tv-power.timer

Owner: root, mode 644. Ansible template: values come from inventory.ini.

# Managed by devops-status-tv (Ansible) – schedule in inventory.ini
{% set names = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] %}
{% set first, last = tv_days.split('-') %}
[Unit]
Description=Status TV: TV power schedule

[Timer]
OnCalendar={{ names[first | int - 1] }}..{{ names[last | int - 1] }} *-*-* {{ tv_on_time }}:00
OnCalendar={{ names[first | int - 1] }}..{{ names[last | int - 1] }} *-*-* {{ tv_off_time }}:00
# Also apply the schedule shortly after boot (desktop + CEC up)
OnBootSec=2min

[Install]
WantedBy=timers.target

/etc/ssh/sshd_config.d/10-status-tv.conf

Owner: root, mode 644. Only deployed when ssh_keys_only=true (not yet).

# Managed by devops-status-tv (Ansible): SSH login with keys only.
# Keys: keys/*.pub in the repo. The password still works for sudo and on the
# Pi's own keyboard/screen (local console), so there's a way back in.
# Read before 50-cloud-init.conf (which says "yes"); sshd uses the first value.
PasswordAuthentication no
KbdInteractiveAuthentication no