Files
rich f0bccb2a5e Kiosk stops the taskbar instead of auto-hiding it
wf-panel-pi won't auto-hide while it has notifications up, and at boot it
always does (Wi-Fi connected, updates available), so it stayed on screen.
kiosk.sh now stops the panel and its respawner as they start; the
auto-hide edit is undone on existing installs.
2026-10-22 22:11:25 -04:00

283 lines
14 KiB
Bash
Executable File

#!/usr/bin/env bash
# Kids weather dashboard installer for Raspberry Pi OS (Bookworm/Trixie, desktop, labwc).
# Run it ON THE PI as the user the dashboard should run as (not root):
#
# ./install.sh # auto-detect location from IP, screen off 21:00-06:30
# ./install.sh --lat 40.0 --lon -75.3 # set location explicitly
# ./install.sh --off 2030 --on 0700 # change the night-off schedule
# ./install.sh --no-night-off # keep the screen on 24/7
# ./install.sh --night-brightness 10 # backlight % when tapped awake at night (default 20)
# ./install.sh --evening-brightness 50 # backlight % from 30 min after sunset to sunrise (100 = off)
# ./install.sh --school-start 08-28 --school-end 06-12 # school year (MM-DD; default 09-01 to 06-15)
# ./install.sh --calendar "Nate=URL" # show a kid's Nextcloud calendar (repeat per kid)
# ./install.sh --no-calendars # stop showing calendars
# ./install.sh --uninstall
#
# Safe to re-run: settings you don't pass are kept from the previous install.
set -euo pipefail
REPO=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
DEST="$HOME/kids-dashboard"
AUTOSTART="$HOME/.config/labwc/autostart"
PROFILE="$HOME/.config/kids-dashboard-chromium"
SERVICE=tap-wake.service
say() { printf '\033[1;32m==>\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m!!\033[0m %s\n' "$*" >&2; }
die() { printf '\033[1;31mxx\033[0m %s\n' "$*" >&2; exit 1; }
usage() { sed -n '2,16p' "$0" | sed 's/^# \{0,1\}//'; exit "${1:-0}"; }
# ---- arguments -------------------------------------------------------------------------
LAT= LON= OFF_AT= ON_AT= AWAKE_SECONDS= NIGHT_BRIGHTNESS= EVENING_BRIGHTNESS= NIGHT_OFF= SCHOOL_START= SCHOOL_END= UNINSTALL=0 CALENDARS=() NO_CALENDARS=0
while [ $# -gt 0 ]; do
case "$1" in
--lat) LAT=${2:?}; shift ;;
--lon) LON=${2:?}; shift ;;
--off) OFF_AT=${2:?}; shift ;;
--on) ON_AT=${2:?}; shift ;;
--awake) AWAKE_SECONDS=${2:?}; shift ;;
--night-brightness) NIGHT_BRIGHTNESS=${2:?}; shift ;;
--evening-brightness) EVENING_BRIGHTNESS=${2:?}; shift ;;
--no-night-off) NIGHT_OFF=0 ;;
--night-off) NIGHT_OFF=1 ;;
--school-start) SCHOOL_START=${2:?}; shift ;;
--school-end) SCHOOL_END=${2:?}; shift ;;
--calendar) CALENDARS+=("${2:?}"); shift ;;
--no-calendars) NO_CALENDARS=1 ;;
--uninstall) UNINSTALL=1 ;;
-h|--help) usage ;;
*) warn "unknown option: $1"; usage 1 ;;
esac
shift
done
[ "$(id -u)" -ne 0 ] || die "run this as the desktop user (e.g. 'pi'), not root; it uses sudo where needed."
[ "$REPO" != "$(cd "$DEST" 2>/dev/null && pwd)" ] || die "the repo can't live in $DEST (that's where it installs to); clone it somewhere else."
remove_cron() { (crontab -l 2>/dev/null || true) | { grep -vE 'kids-dashboard/(screen\.sh|fetch-calendars\.py)|# kids-dashboard-update$' || true; } | crontab -; }
restart_kiosk() { pkill -f -- "--user-data-dir=$PROFILE" 2>/dev/null || true; } # lwrespawn relaunches it
# The desktop shows for ~30s at boot before Chromium covers it: make it plain black with no
# icons (kiosk.sh stops the taskbar). Originals are kept as *.kids-dashboard-orig for --uninstall.
PCMANFM_DIR="$HOME/.config/pcmanfm/default"
ORIG=.kids-dashboard-orig
set_key() { # set_key FILE KEY VALUE (single-section ini files)
if grep -q "^$2=" "$1"; then sed -i "s|^$2=.*|$2=$3|" "$1"; else echo "$2=$3" >> "$1"; fi
}
restart_desktop() { pkill -x pcmanfm 2>/dev/null || true; } # lwrespawn relaunches it
black_desktop() {
mkdir -p "$PCMANFM_DIR"
local out f
# pcmanfm keeps one file per screen (desktop-items-<output>.conf); make sure every screen has one
for out in $(XDG_RUNTIME_DIR=/run/user/$(id -u) WAYLAND_DISPLAY=wayland-0 wlr-randr 2>/dev/null | awk '/^[^ ]/{print $1}'); do
f="$PCMANFM_DIR/desktop-items-$out.conf"
[ -f "$f" ] || cp /etc/xdg/pcmanfm/default/desktop-items-0.conf "$f" 2>/dev/null || echo '[*]' > "$f"
done
for f in "$PCMANFM_DIR"/desktop-items-*.conf; do
[ -f "$f" ] || continue
[ -f "$f$ORIG" ] || cp "$f" "$f$ORIG"
set_key "$f" wallpaper_mode color
for k in desktop_bg desktop_fg desktop_shadow; do set_key "$f" "$k" '#000000'; done
for k in show_trash show_mounts show_documents; do set_key "$f" "$k" 0; done
done
}
restore_desktop() {
local f
for f in "$PCMANFM_DIR"/*"$ORIG" "$HOME/.config/wf-panel-pi/wf-panel-pi.ini$ORIG"; do # (older installs edited the panel too)
[ -f "$f" ] && mv "$f" "${f%"$ORIG"}"
done
restart_desktop
}
# ---- uninstall -------------------------------------------------------------------------
if [ "$UNINSTALL" = 1 ]; then
say "Removing kids dashboard"
systemctl --user disable --now "$SERVICE" 2>/dev/null || true
rm -f "$HOME/.config/systemd/user/$SERVICE"; systemctl --user daemon-reload 2>/dev/null || true
remove_cron
if [ -f "$AUTOSTART" ]; then
sed -i '/kids-dashboard\|Kids weather dashboard/d' "$AUTOSTART"
grep -q '[^[:space:]]' "$AUTOSTART" || rm -f "$AUTOSTART"
fi
[ -x "$DEST/screen.sh" ] && "$DEST/screen.sh" on 2>/dev/null || true
restore_desktop
rm -rf "$DEST" # first, so lwrespawn has nothing left to relaunch
restart_kiosk
for _ in $(seq 20); do pgrep -f -- "--user-data-dir=$PROFILE" >/dev/null || break; sleep 0.5; done
rm -rf "$PROFILE"
say "Done. Reboot to get the normal desktop back. (Packages and autologin were left as they are.)"
exit 0
fi
# ---- previous settings become the defaults ------------------------------------------------
if [ -f "$DEST/config.js" ]; then
: "${LAT:=$(sed -n 's/.*lat: *\([-0-9.]*\).*/\1/p' "$DEST/config.js")}"
: "${LON:=$(sed -n 's/.*lon: *\([-0-9.]*\).*/\1/p' "$DEST/config.js")}"
: "${SCHOOL_START:=$(sed -n 's/.*schoolYearStart: *"\([0-9-]*\)".*/\1/p' "$DEST/config.js")}"
: "${SCHOOL_END:=$(sed -n 's/.*schoolYearEnd: *"\([0-9-]*\)".*/\1/p' "$DEST/config.js")}"
fi
if [ -f "$DEST/screen.conf" ]; then
# shellcheck disable=SC1091
prev() { (. "$DEST/screen.conf"; eval "echo \${$1:-}"); }
: "${OFF_AT:=$(prev OFF_AT)}" "${ON_AT:=$(prev ON_AT)}"
: "${AWAKE_SECONDS:=$(prev AWAKE_SECONDS)}" "${NIGHT_OFF:=$(prev NIGHT_OFF)}"
: "${NIGHT_BRIGHTNESS:=$(prev NIGHT_BRIGHTNESS)}" "${EVENING_BRIGHTNESS:=$(prev EVENING_BRIGHTNESS)}"
fi
: "${OFF_AT:=2100}" "${ON_AT:=0630}" "${AWAKE_SECONDS:=60}" "${NIGHT_OFF:=1}" "${NIGHT_BRIGHTNESS:=20}" "${EVENING_BRIGHTNESS:=50}"
[[ $OFF_AT =~ ^([01][0-9]|2[0-3])[0-5][0-9]$ ]] || die "--off must be HHMM (24h), got '$OFF_AT'"
[[ $ON_AT =~ ^([01][0-9]|2[0-3])[0-5][0-9]$ ]] || die "--on must be HHMM (24h), got '$ON_AT'"
[[ $AWAKE_SECONDS =~ ^[0-9]+$ ]] || die "--awake must be a number of seconds"
: "${SCHOOL_START:=09-01}" "${SCHOOL_END:=06-15}"
for d in "$SCHOOL_START" "$SCHOOL_END"; do
[[ $d =~ ^(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$ ]] || die "--school-start/--school-end must be MM-DD, got '$d'"
done
[[ $NIGHT_BRIGHTNESS =~ ^[0-9]+$ ]] && [ "$NIGHT_BRIGHTNESS" -le 100 ] || die "--night-brightness must be 0-100 (percent)"
[[ $EVENING_BRIGHTNESS =~ ^[0-9]+$ ]] && [ "$EVENING_BRIGHTNESS" -le 100 ] || die "--evening-brightness must be 0-100 (percent)"
for c in ${CALENDARS[@]+"${CALENDARS[@]}"}; do
[[ $c =~ ^[^=]+=https?://.+ ]] || die "--calendar must be Name=URL, got '$c'"
done
# ---- sanity checks -----------------------------------------------------------------------
command -v raspi-config >/dev/null || warn "raspi-config not found; this is meant for Raspberry Pi OS. Continuing anyway."
command -v labwc >/dev/null || [ -f /usr/share/wayland-sessions/rpd-labwc.desktop ] \
|| die "labwc not found. Install the Raspberry Pi OS *desktop* image (not Lite) and re-run."
# ---- packages ----------------------------------------------------------------------------
need=()
for pkg in chromium wlopm python3 cron curl; do dpkg -s "$pkg" >/dev/null 2>&1 || need+=("$pkg"); done
if [ ${#need[@]} -gt 0 ]; then
say "Installing packages: ${need[*]}"
sudo apt-get update -qq
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "${need[@]}"
fi
# ---- location ----------------------------------------------------------------------------
TZ_NAME=$(timedatectl show -p Timezone --value 2>/dev/null || cat /etc/timezone 2>/dev/null || echo UTC)
if [ -z "$LAT" ] || [ -z "$LON" ]; then
say "Looking up location from this network's public IP"
loc=$(curl -fsS --max-time 10 https://ipinfo.io/json \
| python3 -c 'import json,sys; j=json.load(sys.stdin); print(j["loc"].replace(","," "), j.get("city",""), j.get("region",""), sep="|")' 2>/dev/null) \
|| die "couldn't detect location; pass it with --lat and --lon"
read -r LAT LON <<<"${loc%%|*}"
say "Found: $(cut -d'|' -f2- <<<"$loc" | tr '|' ' ') ($LAT, $LON). Use --lat/--lon if that's wrong."
fi
[[ $LAT =~ ^-?[0-9]+(\.[0-9]+)?$ && $LON =~ ^-?[0-9]+(\.[0-9]+)?$ ]] || die "bad --lat/--lon: '$LAT' '$LON'"
# ---- desktop session: labwc + autologin --------------------------------------------------
if command -v raspi-config >/dev/null; then
if ! grep -qE '^autologin-session=.*labwc' /etc/lightdm/lightdm.conf 2>/dev/null; then
say "Switching the desktop to labwc (Wayland)"
sudo raspi-config nonint do_wayland W2
fi
if ! grep -qE "^autologin-user=$USER\$" /etc/lightdm/lightdm.conf 2>/dev/null; then
say "Turning on desktop autologin for $USER"
sudo raspi-config nonint do_boot_behaviour B4
fi
fi
if ! id -nG | tr ' ' '\n' | grep -qx input; then
say "Adding $USER to the 'input' group (needed for tap-to-wake)"
sudo usermod -aG input "$USER"
NEEDS_REBOOT=1
fi
# ---- files -------------------------------------------------------------------------------
say "Installing to $DEST"
mkdir -p "$DEST"
install -m 644 "$REPO/dashboard/index.html" "$DEST/index.html"
install -m 755 "$REPO/bin/kiosk.sh" "$REPO/bin/screen.sh" "$REPO/bin/tap-wake.py" "$REPO/bin/fetch-calendars.py" "$REPO/bin/sun.py" "$DEST/"
cat > "$DEST/config.js" <<EOF
// Written by install.sh. Re-run it with --lat/--lon/--school-start/--school-end to change.
window.KIDS_DASHBOARD = { lat: $LAT, lon: $LON, tz: "$TZ_NAME", schoolYearStart: "$SCHOOL_START", schoolYearEnd: "$SCHOOL_END" };
EOF
cat > "$DEST/screen.conf" <<EOF
# Written by install.sh. Re-run it with --off/--on/--awake/--night-brightness/--evening-brightness/
# --no-night-off/--lat/--lon to change (the cron schedule is generated from these values).
NIGHT_OFF=$NIGHT_OFF
OFF_AT=$OFF_AT
ON_AT=$ON_AT
AWAKE_SECONDS=$AWAKE_SECONDS
NIGHT_BRIGHTNESS=$NIGHT_BRIGHTNESS
EVENING_BRIGHTNESS=$EVENING_BRIGHTNESS
LAT=$LAT
LON=$LON
OUTPUT='*'
EOF
# calendars.conf is only rewritten when --calendar/--no-calendars is passed
if [ ${#CALENDARS[@]} -gt 0 ]; then
{ echo "# Written by install.sh. Re-run it with --calendar \"Name=URL\" (once per kid) to change."
printf '%s\n' "${CALENDARS[@]}"; } > "$DEST/calendars.conf"
chmod 600 "$DEST/calendars.conf" # the share links work like passwords
elif [ "$NO_CALENDARS" = 1 ]; then
rm -f "$DEST/calendars.conf" "$DEST/events.js"
fi
# ---- start at login ----------------------------------------------------------------------
mkdir -p "$(dirname "$AUTOSTART")"
touch "$AUTOSTART"
if ! grep -q 'kids-dashboard/kiosk.sh' "$AUTOSTART"; then
cat >> "$AUTOSTART" <<EOF
# kids-dashboard kiosk (labwc also runs /etc/xdg/labwc/autostart; the kiosk covers it).
/usr/bin/lwrespawn $DEST/kiosk.sh &
EOF
fi
# ---- black desktop behind the kiosk ---------------------------------------------------------
say "Desktop: plain black with no icons or taskbar (it only shows while booting)"
black_desktop
f="$HOME/.config/wf-panel-pi/wf-panel-pi.ini" # undo the auto-hide an earlier version set
[ -f "$f$ORIG" ] && mv "$f$ORIG" "$f"
pgrep -u "$USER" -x labwc >/dev/null && restart_desktop
# ---- night-off schedule + tap to wake -----------------------------------------------------
remove_cron
if git -C "$REPO" rev-parse --abbrev-ref '@{u}' >/dev/null 2>&1; then
say "Auto-update: checking $(git -C "$REPO" remote get-url origin) every hour"
{ crontab -l 2>/dev/null || true
echo "17 * * * * $REPO/bin/update.sh # kids-dashboard-update"
} | crontab -
fi
if [ -s "$DEST/calendars.conf" ]; then
say "Calendars: $(grep -v '^#' "$DEST/calendars.conf" | cut -d= -f1 | paste -sd, - | sed 's/,/, /g')"
{ crontab -l 2>/dev/null || true
echo "*/15 * * * * $DEST/fetch-calendars.py >$DEST/calendars.log 2>&1"
echo "@reboot sleep 60; $DEST/fetch-calendars.py >$DEST/calendars.log 2>&1"
} | crontab -
"$DEST/fetch-calendars.py" || warn "couldn't fetch the calendars right now; will retry every 15 minutes"
fi
if [ "$EVENING_BRIGHTNESS" -lt 100 ]; then
say "Screen at $EVENING_BRIGHTNESS% from 30 min after sunset until sunrise"
{ crontab -l 2>/dev/null || true
echo "*/5 * * * * $DEST/screen.sh tick"
} | crontab -
fi
mkdir -p "$HOME/.config/systemd/user"
if [ "$NIGHT_OFF" = 1 ]; then
say "Screen off $OFF_AT-$ON_AT, tap to wake for ${AWAKE_SECONDS}s"
{ crontab -l 2>/dev/null || true
echo "$((10#${OFF_AT:2:2})) $((10#${OFF_AT:0:2})) * * * $DEST/screen.sh off"
echo "$((10#${ON_AT:2:2})) $((10#${ON_AT:0:2})) * * * $DEST/screen.sh on"
echo "@reboot sleep 90; $DEST/screen.sh auto"
} | crontab -
install -m 644 "$REPO/systemd/$SERVICE" "$HOME/.config/systemd/user/$SERVICE"
systemctl --user daemon-reload
systemctl --user enable "$SERVICE" >/dev/null 2>&1
systemctl --user restart "$SERVICE" 2>/dev/null || true
else
say "Night-off disabled; screen stays on"
systemctl --user disable --now "$SERVICE" 2>/dev/null || true
"$DEST/screen.sh" on 2>/dev/null || true
fi
# ---- go ----------------------------------------------------------------------------------
if [ "${NEEDS_REBOOT:-0}" = 0 ] && pgrep -u "$USER" -x labwc >/dev/null && pgrep -u "$USER" -f 'kids-dashboard/kiosk.sh' >/dev/null; then
restart_kiosk
say "Done. The dashboard is reloading with the new version."
else
say "Done. Reboot to start the dashboard: sudo reboot"
fi