#!/bin/bash # Webcam presence detection daemon. # Checks for motion at an adaptive interval (20s normally, backed off to 120s # under heavy system load) and shares caffeine's systemd-inhibit idle lock # while the user is detected, so hypridle never fires during an active session. # # Camera selection: set PRESENCE_DETECT_CAMERA env var or write # CAMERA= to ~/.config/presence-detect.conf # # Exit codes from python helper: 0=motion, 1=no motion, 2=camera error (busy/unavailable) # On camera error the daemon silently skips the tick and leaves the inhibit # state exactly as it was on the previous run. # Resolve the script's real directory so the Python helper path stays valid # even when invoked via a symlink or from a different cwd. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PYTHON_DETECT="$SCRIPT_DIR/python/presence_detect.py" # Shared with caffeine.sh: both the manual toggle and this daemon drive the # same systemd-inhibit lock, so caffeine-status.sh reflects either source. PID_FILE="/tmp/caffeine-inhibit.pid" # Marks that *this daemon* (not the manual caffeine toggle) currently holds # the lock, so a "no motion" tick never releases a manually-started session. OWNED_FLAG="/tmp/presence-inhibit-owned" PRESENCE_CFG="${XDG_CONFIG_HOME:-$HOME/.config}/presence-detect.conf" INTERVAL_IDLE=20 # seconds between checks under normal load INTERVAL_BUSY=120 # seconds between checks when CPU or RAM is >= LOAD_THRESHOLD LOAD_THRESHOLD=0.70 NPROC="$(nproc)" # Resolve camera ID: env var takes highest priority, then config file, then default 0. _camera_id() { if [[ -n "$PRESENCE_DETECT_CAMERA" ]]; then echo "$PRESENCE_DETECT_CAMERA" elif [[ -f "$PRESENCE_CFG" ]]; then # -oP 'CAMERA=\K[0-9]+': Perl-style look-behind strips "CAMERA=" prefix. grep -oP 'CAMERA=\K[0-9]+' "$PRESENCE_CFG" 2>/dev/null || echo 0 else echo 0 fi } # True if 1-minute load average (normalized by core count) or RAM usage is # at/above LOAD_THRESHOLD. Load average is a cheap, sampling-free proxy for # "CPU busy" — avoids adding an extra measurement delay to every tick. _system_busy() { local load1 cpu_busy mem_frac mem_busy load1="$(awk '{print $1}' /proc/loadavg)" cpu_busy="$(awk -v l="$load1" -v n="$NPROC" -v t="$LOAD_THRESHOLD" \ 'BEGIN{print (l/n >= t) ? 1 : 0}')" [[ "$cpu_busy" == "1" ]] && return 0 mem_frac="$(free | awk '/^Mem:/ {print $3/$2}')" mem_busy="$(awk -v m="$mem_frac" -v t="$LOAD_THRESHOLD" 'BEGIN{print (m >= t) ? 1 : 0}')" [[ "$mem_busy" == "1" ]] } _next_interval() { if _system_busy; then echo "$INTERVAL_BUSY"; else echo "$INTERVAL_IDLE"; fi } # Returns true if the inhibitor sentinel process is still alive. _inhibit_running() { [[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null } _start_inhibit() { # Guard: don't start a second inhibitor if one is already active — whether # it's ours from a previous tick or a manually-started caffeine session. _inhibit_running && return # --what=idle: target the logind idle-inhibit lock that hypridle polls. # "sleep infinity" is the sentinel; its PID is saved so we can kill it later. systemd-inhibit --what=idle --who="presence-detect" \ --why="User presence detected" --mode=block \ sleep infinity & echo $! > "$PID_FILE" touch "$OWNED_FLAG" # logger writes to the system journal — visible via `journalctl -t presence-detect`. logger -t presence-detect "Motion detected — idle inhibited" } _stop_inhibit() { _inhibit_running || return # Never release a lock we didn't start — that would be a manual caffeine # session, which must persist regardless of presence. [[ -f "$OWNED_FLAG" ]] || return # Killing the sleep process releases the systemd-inhibit lock automatically. kill "$(cat "$PID_FILE")" 2>/dev/null rm -f "$PID_FILE" "$OWNED_FLAG" logger -t presence-detect "No motion — idle inhibit released" } _cleanup() { # On daemon stop (systemd unit stop, user logout, etc.), release the lock # cleanly, but only if we're the one holding it. _stop_inhibit exit 0 } # Intercept termination signals to ensure the inhibitor PID is never orphaned. trap _cleanup SIGTERM SIGINT SIGHUP while true; do CAMERA="$(_camera_id)" # Run the OpenCV motion detector; stderr suppressed to keep the journal clean. python3 "$PYTHON_DETECT" "$CAMERA" 2>/dev/null rc=$? case $rc in 0) _start_inhibit ;; 1) _stop_inhibit ;; # rc=2 = camera busy/unavailable — silently skip, state unchanged esac sleep "$(_next_interval)" done