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.
283 lines
14 KiB
Bash
Executable File
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
|