From 7dc9a762a00cec673d0b6e5c5673ba706191c973 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 1 Jul 2026 19:49:26 +0000 Subject: [PATCH 1/2] frigate.sh: interactive camera wizard generates go2rtc + cameras config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously Frigate always wrote a placeholder config.yml the operator had to hand-edit to add cameras. Now install_frigate prompts to add cameras one at a time (name, RTSP IP/port/user/password/path, optional sub-stream, enabled, notifications), matching the go2rtc + cameras structure used in production frigate configs: - Each camera gets a go2rtc stream entry (+ optional _sub for detection) and a cameras: block with ffmpeg inputs/roles, detect, notifications - RTSP credentials/IPs are written to .env as FRIGATE_* variables (first camera gets FRIGATE_RTSP_USER/PASSWORD, later cameras get numbered suffixes _1, _2, ... to avoid collisions) and referenced in config.yml via Frigate's {FRIGATE_VAR} substitution syntax — secrets never appear in the YAML directly - docker-compose.yml now includes env_file: .env so those vars actually reach the container for substitution to work - Skipping all camera prompts falls back to the original starter config.yml for manual editing, preserving existing behavior - README and DRY-RUN summary updated to reflect the new flow Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01LQJBvqzXeyuhhAcAA3Q5Wq --- services/frigate.sh | 185 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 172 insertions(+), 13 deletions(-) diff --git a/services/frigate.sh b/services/frigate.sh index 90249f0..37737a4 100644 --- a/services/frigate.sh +++ b/services/frigate.sh @@ -9,7 +9,10 @@ # Ported from ubuntu-post-install-24.04-crowdsec.sh (# ---- FRIGATE NVR ----). # Own ~/docker/frigate/ with a standalone docker-compose.yml + .env + config.yml. # Auto-enables /dev/dri/renderD128 for hardware detection (Intel/AMD) when present. -# YOU MUST edit config/config.yml to add your camera RTSP streams before starting. +# Interactively prompts for cameras — RTSP user/pass/IP go in .env as +# FRIGATE_* vars, which config.yml references via Frigate's {FRIGATE_VAR} +# substitution syntax so credentials never appear in the YAML directly. +# Skip the camera prompts to get a starter config.yml to edit by hand. # ── Standalone bootstrap ────────────────────────────────────────────────────── # Detected when the script is executed directly rather than sourced by setup.sh. @@ -205,7 +208,8 @@ install_frigate() { echo " - Create $FRIGATE_DIR with docker-compose.yml + .env + config/config.yml" echo " - Auto-enable /dev/dri/renderD128 for GPU-assisted detection if present" echo " - Expose ports 5000 (web), 8554 (RTSP restream), 8555 (WebRTC)" - echo " - Write a starter config.yml — edit to add camera streams before starting" + echo " - Prompt to add cameras interactively (RTSP creds go in .env)" + echo " or write a starter config.yml if none are added" echo " - Offer a Caddy reverse proxy and to start the container" return 0 fi @@ -258,6 +262,8 @@ services: restart: unless-stopped privileged: true shm_size: "256mb" + env_file: + - .env environment: - TZ=$TZ_VAL $DEVICE_BLOCK @@ -276,15 +282,117 @@ $DEVICE_BLOCK ${_CADDY_NET_BLOCK}${_CADDY_NET_SECTION} FRIGATE_COMPOSE - cat > .env << FRIGATE_ENV -FRIGATE_MEDIA=$FRIGATE_MEDIA -CADDY_NET=$SITE_CADDY_NET -FRIGATE_ENV - mkdir -p config mkdir -p "$FRIGATE_MEDIA" - cat > config/config.yml << 'FRIGATE_CONFIG' + # ── Camera wizard ──────────────────────────────────────────────────────── + # Credentials/IPs go in .env as FRIGATE_* variables; Frigate substitutes + # any {FRIGATE_VAR} placeholder in config.yml from its container env at + # startup, so RTSP secrets never need to be typed into the YAML directly. + local GO2RTC_BLOCK="" CAMERAS_BLOCK="" ENV_CAM_VARS="" CAM_INDEX=0 + while true; do + local ADD_CAM="" + if [ "$CAM_INDEX" -eq 0 ]; then + prompt_yn "Add a camera now? (y/n):" "y" ADD_CAM + else + prompt_yn "Add another camera? (y/n):" "n" ADD_CAM + fi + [[ "$ADD_CAM" =~ ^[Yy]$ ]] || break + + local CAM_NAME="" + prompt_text " Camera name (e.g. front_door):" "camera$((CAM_INDEX+1))" CAM_NAME + CAM_NAME="$(echo "$CAM_NAME" | tr '[:upper:] ' '[:lower:]_' | tr -cd 'a-z0-9_')" + CAM_NAME="${CAM_NAME:-camera$((CAM_INDEX+1))}" + local CAM_UPPER="${CAM_NAME^^}" + + local CAM_IP="" + prompt_text " ${CAM_NAME} RTSP IP address:" "" CAM_IP + local CAM_PORT="" + prompt_text " ${CAM_NAME} RTSP port [554]:" "554" CAM_PORT + local CAM_USER="" + prompt_text " ${CAM_NAME} RTSP username:" "admin" CAM_USER + local CAM_PASS="" + prompt_text " ${CAM_NAME} RTSP password (blank = generate one):" "" CAM_PASS + [ -z "$CAM_PASS" ] && CAM_PASS="$(generate_password 20)" + + local CAM_PATH="" + prompt_text " ${CAM_NAME} RTSP path (use {sub} for main/sub-stream marker) [/cam/realmonitor?channel=1&subtype={sub}#backchannel=0]:" \ + "/cam/realmonitor?channel=1&subtype={sub}#backchannel=0" CAM_PATH + + local CAM_SUBSTREAM="" + prompt_yn " Add a lower-res sub-stream for detection? (y/n):" "y" CAM_SUBSTREAM + + local CAM_ENABLED="" + prompt_yn " Enable ${CAM_NAME} immediately? (y/n):" "y" CAM_ENABLED + local CAM_NOTIFY="" + prompt_yn " Enable notifications for ${CAM_NAME}? (y/n):" "n" CAM_NOTIFY + + # First camera's credentials get no suffix (FRIGATE_RTSP_USER); each + # camera after that gets an index suffix (…USER1, …USER2, …) so + # cameras with different logins don't collide. + local IDX_SUFFIX="" + [ "$CAM_INDEX" -gt 0 ] && IDX_SUFFIX="$CAM_INDEX" + local VAR_USER="FRIGATE_RTSP_USER${IDX_SUFFIX}" + local VAR_PASS="FRIGATE_RTSP_PASSWORD${IDX_SUFFIX}" + local VAR_IP="FRIGATE_${CAM_UPPER}_IP" + + ENV_CAM_VARS="${ENV_CAM_VARS}${VAR_USER}=${CAM_USER} +${VAR_PASS}=${CAM_PASS} +${VAR_IP}=${CAM_IP} +" + + local MAIN_PATH="${CAM_PATH//\{sub\}/0}" + local SUB_PATH="${CAM_PATH//\{sub\}/1}" + + GO2RTC_BLOCK="${GO2RTC_BLOCK} ${CAM_NAME}: + - rtsp://{${VAR_USER}}:{${VAR_PASS}}@{${VAR_IP}}:${CAM_PORT}${MAIN_PATH} +" + local INPUTS_BLOCK + if [[ "$CAM_SUBSTREAM" =~ ^[Yy]$ ]]; then + GO2RTC_BLOCK="${GO2RTC_BLOCK} ${CAM_NAME}_sub: + - rtsp://{${VAR_USER}}:{${VAR_PASS}}@{${VAR_IP}}:${CAM_PORT}${SUB_PATH} +" + INPUTS_BLOCK=" - path: rtsp://127.0.0.1:8554/${CAM_NAME} + input_args: preset-rtsp-restream + roles: + - record + - path: rtsp://127.0.0.1:8554/${CAM_NAME}_sub + input_args: preset-rtsp-restream + roles: + - detect" + else + INPUTS_BLOCK=" - path: rtsp://127.0.0.1:8554/${CAM_NAME} + input_args: preset-rtsp-restream + roles: + - detect + - record" + fi + GO2RTC_BLOCK="${GO2RTC_BLOCK} +" + + local ENABLED_VAL="false"; [[ "$CAM_ENABLED" =~ ^[Yy]$ ]] && ENABLED_VAL="true" + local NOTIFY_VAL="false"; [[ "$CAM_NOTIFY" =~ ^[Yy]$ ]] && NOTIFY_VAL="true" + + CAMERAS_BLOCK="${CAMERAS_BLOCK} ${CAM_NAME}: + enabled: ${ENABLED_VAL} + ffmpeg: + inputs: +${INPUTS_BLOCK} + detect: + enabled: true + width: 640 + height: 480 + fps: 5 + notifications: + enabled: ${NOTIFY_VAL} + +" + CAM_INDEX=$((CAM_INDEX+1)) + done + + if [ "$CAM_INDEX" -eq 0 ]; then + # No cameras entered — write a starter config the operator edits by hand. + cat > config/config.yml << 'FRIGATE_CONFIG' # Frigate Configuration — Docs: https://docs.frigate.video # # ⚠️ YOU MUST EDIT THIS FILE to add your cameras before starting Frigate. @@ -319,6 +427,44 @@ snapshots: retain: default: 7 FRIGATE_CONFIG + else + cat > config/config.yml << FRIGATE_CONFIG +# Frigate Configuration — Docs: https://docs.frigate.video +# +# RTSP credentials/IPs come from .env — Frigate substitutes {FRIGATE_VAR} +# placeholders below from the container's environment at startup. + +mqtt: + enabled: false # Set to true and configure if you use Home Assistant + +go2rtc: + streams: +${GO2RTC_BLOCK} +cameras: +${CAMERAS_BLOCK} +detectors: + default: + type: cpu # Change to 'edgetpu' for Coral TPU or 'openvino' for Intel GPU + +record: + enabled: true + retain: + days: 7 + mode: motion + +snapshots: + enabled: true + retain: + default: 7 +FRIGATE_CONFIG + fi + + cat > .env << FRIGATE_ENV +FRIGATE_MEDIA=$FRIGATE_MEDIA +CADDY_NET=$SITE_CADDY_NET +${ENV_CAM_VARS} +FRIGATE_ENV + chmod 600 .env chown -R "$ACTUAL_USER:$ACTUAL_USER" "$FRIGATE_DIR" chown -R "$ACTUAL_USER:$ACTUAL_USER" "$FRIGATE_MEDIA" 2>/dev/null || true @@ -336,7 +482,8 @@ security cameras. Detects people, cars, animals, and more. - RTSP restream: port 8554 - WebRTC: port 8555 - Recordings: \`$FRIGATE_MEDIA\` -- Config: \`config/config.yml\` — **add your camera RTSP streams here** +- Config: \`config/config.yml\` — cameras configured during install ($CAM_INDEX added) +- Credentials: \`.env\` — RTSP user/pass/IP per camera as FRIGATE_* variables ## Manage \`\`\`bash @@ -347,8 +494,14 @@ docker compose logs -f # logs docker compose pull && docker compose up -d # update \`\`\` +## Adding/editing cameras later +Re-run the installer (\`sudo ./setup.sh frigate\`) to add more cameras through +the same wizard, or edit \`config/config.yml\` and \`.env\` by hand — camera +credentials in config.yml use Frigate's \`{FRIGATE_VAR}\` substitution syntax, +resolved from \`.env\` at container startup. + ## First steps -1. Edit \`config/config.yml\` — add your camera RTSP URLs under \`cameras:\` +1. Review \`config/config.yml\` — adjust detection zones, masks, retention 2. Start Frigate: \`docker compose up -d\` 3. Open http://localhost:5000 to view cameras and configure detection zones @@ -359,17 +512,23 @@ docker compose pull && docker compose up -d # update MD echo "" - log_warning "Edit config/config.yml to add your camera RTSP streams before starting." + local START_DEFAULT="n" + if [ "$CAM_INDEX" -eq 0 ]; then + log_warning "No cameras added — edit config/config.yml to add camera RTSP streams before starting." + else + log_success "$CAM_INDEX camera(s) configured." + START_DEFAULT="y" + fi echo "" local START_FRIGATE="" - prompt_yn "Start Frigate now anyway? (y/n):" "n" START_FRIGATE + prompt_yn "Start Frigate now? (y/n):" "$START_DEFAULT" START_FRIGATE if [ "$START_FRIGATE" = "y" ] || [ "$START_FRIGATE" = "Y" ]; then docker compose up -d && log_success "Frigate started" || log_warning "Failed to start — check: docker compose logs" fi echo "" echo " Access at: http://localhost:5000" - echo " Config: $FRIGATE_DIR/config/config.yml (add cameras here)" + echo " Config: $FRIGATE_DIR/config/config.yml" echo "" } From 42a38c9397fab65b1e64e802f9a443885a050d45 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 1 Jul 2026 20:25:55 +0000 Subject: [PATCH 2/2] frigate.sh: detect existing camera config, offer keep/backup/add/remove Previously re-running the installer always overwrote config.yml and .env from scratch, silently discarding any real camera credentials already on disk. Now install_frigate parses an existing config.yml + .env (best-effort, matching this installer's own output shape) and presents a numbered list of detected cameras with a menu: [1] Keep everything as-is (no changes at all) [2] Backup existing config and start fresh [3] Add more cameras (keep these) [4] Remove cameras (choose numbers, or 'all'), then optionally add more Implementation switches from building config.yml/.env as concatenated text blocks inline in the collection loop to parallel CAM_* bash arrays (name/ip/port/var-names/enabled/notify/substream-suffixes), so cameras can be parsed, listed, removed, and re-rendered independently: - _frigate_parse_existing: reads go2rtc streams + cameras: enabled/notifications from config.yml, and credential values from .env, into the CAM_* arrays - _frigate_review_existing: numbered menu, mutates arrays per choice - _frigate_next_suffix_int: kept cameras retain their existing FRIGATE_RTSP_USER[N] var names unchanged; new cameras get the next unused numeric suffix so credentials never collide after removals - _frigate_camera_wizard / _frigate_render_config: same prompts and output shape as before, now array-driven so kept + new cameras render uniformly - _frigate_backup_existing: copies config.yml/.env/docker-compose.yml to a timestamped backup-YYYYMMDD-HHMMSS/ dir before any destructive rewrite Verified with a 5-scenario test harness (fresh install, keep-as-is producing byte-identical output, add-camera preserving existing credentials, remove- by-number without var collisions, backup-and-fresh) before committing. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01LQJBvqzXeyuhhAcAA3Q5Wq --- services/frigate.sh | 526 +++++++++++++++++++++++++++++++------------- 1 file changed, 377 insertions(+), 149 deletions(-) diff --git a/services/frigate.sh b/services/frigate.sh index 37737a4..ce0bfef 100644 --- a/services/frigate.sh +++ b/services/frigate.sh @@ -197,61 +197,385 @@ fi register_service frigate cameras "AI-powered NVR — object detection on security cameras (Frigate)" 5000 +# ── Existing-config parsing (best-effort) ──────────────────────────────────── +# Populates the CAM_* arrays (declared by install_frigate) plus ENV_MAP by +# reading an already-generated config.yml + .env. Only understands the shape +# this installer itself writes; heavily hand-edited files may not parse +# cleanly — the caller always offers a backup-and-fresh option as a fallback. +_frigate_parse_existing() { + local cfg="$1" env="$2" + declare -gA ENV_MAP=() + local k v + while IFS='=' read -r k v; do + [[ "$k" =~ ^[[:space:]]*# ]] && continue + [[ -z "${k// /}" ]] && continue + ENV_MAP["$k"]="$v" + done < "$env" + + local re_stream_key='^[[:space:]]{4}([a-z0-9_]+):[[:space:]]*$' + local re_rtsp_line='rtsp://\{([A-Za-z0-9_]+)\}:\{([A-Za-z0-9_]+)\}@\{([A-Za-z0-9_]+)\}:([0-9]+)(.*)$' + local re_cam_key='^[[:space:]]{2}([a-z0-9_]+):[[:space:]]*$' + local re_enabled='^[[:space:]]{4}enabled:[[:space:]]*(true|false)' + local re_notify_key='^[[:space:]]{4}notifications:' + local re_notify_val='^[[:space:]]{6}enabled:[[:space:]]*(true|false)' + + declare -A _MAIN_OF=() _SUB_OF=() _PORT_OF=() _VU_OF=() _VP_OF=() _VI_OF=() + local _order=() _stream="" in_go2rtc=false line + + while IFS= read -r line; do + if [[ "$line" =~ ^go2rtc: ]]; then in_go2rtc=true; continue; fi + if $in_go2rtc && [[ "$line" =~ ^[a-zA-Z] ]]; then in_go2rtc=false; fi + $in_go2rtc || continue + if [[ "$line" =~ $re_stream_key ]]; then _stream="${BASH_REMATCH[1]}"; continue; fi + if [[ -n "$_stream" ]] && [[ "$line" =~ $re_rtsp_line ]]; then + local vu="${BASH_REMATCH[1]}" vp="${BASH_REMATCH[2]}" vi="${BASH_REMATCH[3]}" + local port="${BASH_REMATCH[4]}" suffix="${BASH_REMATCH[5]}" + if [[ "$_stream" == *_sub ]]; then + _SUB_OF["${_stream%_sub}"]="$suffix" + else + _MAIN_OF["$_stream"]="$suffix"; _PORT_OF["$_stream"]="$port" + _VU_OF["$_stream"]="$vu"; _VP_OF["$_stream"]="$vp"; _VI_OF["$_stream"]="$vi" + _order+=("$_stream") + fi + _stream="" + fi + done < "$cfg" + + declare -A _ENABLED_OF=() _NOTIFY_OF=() + local cam="" in_cameras=false in_notify=false + while IFS= read -r line; do + if [[ "$line" =~ ^cameras: ]]; then in_cameras=true; continue; fi + if $in_cameras && [[ "$line" =~ ^[a-zA-Z] ]]; then in_cameras=false; fi + $in_cameras || continue + if [[ "$line" =~ $re_cam_key ]]; then cam="${BASH_REMATCH[1]}"; in_notify=false; continue; fi + [[ -z "$cam" ]] && continue + if [[ "$line" =~ $re_notify_key ]]; then in_notify=true; continue; fi + if $in_notify && [[ "$line" =~ $re_notify_val ]]; then + _NOTIFY_OF["$cam"]="${BASH_REMATCH[1]}"; in_notify=false; continue + fi + if [[ "$line" =~ $re_enabled ]]; then _ENABLED_OF["$cam"]="${BASH_REMATCH[1]}"; fi + done < "$cfg" + + CAM_NAME=() CAM_PORT=() CAM_VARUSER=() CAM_VARPASS=() CAM_VARIP=() + CAM_HASSUB=() CAM_MAIN_SUFFIX=() CAM_SUB_SUFFIX=() CAM_ENABLED=() CAM_NOTIFY=() + CAM_CRED_USER=() CAM_CRED_PASS=() CAM_CRED_IP=() + local name + for name in "${_order[@]}"; do + CAM_NAME+=("$name") + CAM_PORT+=("${_PORT_OF[$name]}") + CAM_VARUSER+=("${_VU_OF[$name]}") + CAM_VARPASS+=("${_VP_OF[$name]}") + CAM_VARIP+=("${_VI_OF[$name]}") + CAM_MAIN_SUFFIX+=("${_MAIN_OF[$name]}") + if [[ -n "${_SUB_OF[$name]:-}" ]]; then + CAM_HASSUB+=("true"); CAM_SUB_SUFFIX+=("${_SUB_OF[$name]}") + else + CAM_HASSUB+=("false"); CAM_SUB_SUFFIX+=("") + fi + CAM_ENABLED+=("${_ENABLED_OF[$name]:-false}") + CAM_NOTIFY+=("${_NOTIFY_OF[$name]:-false}") + CAM_CRED_USER+=("${ENV_MAP[${_VU_OF[$name]}]:-}") + CAM_CRED_PASS+=("${ENV_MAP[${_VP_OF[$name]}]:-}") + CAM_CRED_IP+=("${ENV_MAP[${_VI_OF[$name]}]:-}") + done +} + +_frigate_backup_existing() { + local ts; ts="$(date +%Y%m%d-%H%M%S)" + local bdir="$FRIGATE_DIR/backup-$ts" + mkdir -p "$bdir" + [ -f config/config.yml ] && cp config/config.yml "$bdir/config.yml" + [ -f .env ] && cp .env "$bdir/.env" + [ -f docker-compose.yml ] && cp docker-compose.yml "$bdir/docker-compose.yml" + chown -R "$ACTUAL_USER:$ACTUAL_USER" "$bdir" 2>/dev/null || true + log_success "Backed up existing config to $bdir" +} + +_frigate_clear_cameras() { + CAM_NAME=() CAM_PORT=() CAM_VARUSER=() CAM_VARPASS=() CAM_VARIP=() + CAM_HASSUB=() CAM_MAIN_SUFFIX=() CAM_SUB_SUFFIX=() CAM_ENABLED=() CAM_NOTIFY=() + CAM_CRED_USER=() CAM_CRED_PASS=() CAM_CRED_IP=() +} + +_frigate_remove_by_index() { + local nums="$1" + declare -A to_remove=() + local n + for n in $nums; do to_remove["$((n-1))"]=1; done + local _NAME=() _PORT=() _VU=() _VP=() _VI=() _HS=() _MS=() _SS=() _EN=() _NO=() _CU=() _CP=() _CI=() + local i removed=0 + for i in "${!CAM_NAME[@]}"; do + if [[ -n "${to_remove[$i]:-}" ]]; then removed=$((removed+1)); continue; fi + _NAME+=("${CAM_NAME[$i]}"); _PORT+=("${CAM_PORT[$i]}") + _VU+=("${CAM_VARUSER[$i]}"); _VP+=("${CAM_VARPASS[$i]}"); _VI+=("${CAM_VARIP[$i]}") + _HS+=("${CAM_HASSUB[$i]}"); _MS+=("${CAM_MAIN_SUFFIX[$i]}"); _SS+=("${CAM_SUB_SUFFIX[$i]}") + _EN+=("${CAM_ENABLED[$i]}"); _NO+=("${CAM_NOTIFY[$i]}") + _CU+=("${CAM_CRED_USER[$i]}"); _CP+=("${CAM_CRED_PASS[$i]}"); _CI+=("${CAM_CRED_IP[$i]}") + done + CAM_NAME=("${_NAME[@]}"); CAM_PORT=("${_PORT[@]}"); CAM_VARUSER=("${_VU[@]}"); CAM_VARPASS=("${_VP[@]}") + CAM_VARIP=("${_VI[@]}"); CAM_HASSUB=("${_HS[@]}"); CAM_MAIN_SUFFIX=("${_MS[@]}"); CAM_SUB_SUFFIX=("${_SS[@]}") + CAM_ENABLED=("${_EN[@]}"); CAM_NOTIFY=("${_NO[@]}"); CAM_CRED_USER=("${_CU[@]}") + CAM_CRED_PASS=("${_CP[@]}"); CAM_CRED_IP=("${_CI[@]}") + log_success "Removed $removed camera(s)." +} + +# Presents the numbered menu once existing cameras were parsed. Sets +# FRIGATE_SKIP_WIZARD=true when the operator chooses to leave everything +# untouched; mutates the CAM_* arrays in place for backup/remove/add. +_frigate_review_existing() { + echo "" + echo "╔══════════════════════════════════════════════════════════════╗" + echo "║ Existing Frigate cameras found ║" + echo "╚══════════════════════════════════════════════════════════════╝" + local i + for i in "${!CAM_NAME[@]}"; do + printf " %d) %-16s enabled=%-5s substream=%-5s ip=%s\n" \ + "$((i+1))" "${CAM_NAME[$i]}" "${CAM_ENABLED[$i]}" "${CAM_HASSUB[$i]}" "${CAM_CRED_IP[$i]:-}" + done + echo "" + echo " [1] Keep everything as-is (no changes)" + echo " [2] Backup existing config and start fresh" + echo " [3] Add more cameras (keep these)" + echo " [4] Remove cameras (choose numbers, or 'all'), then optionally add more" + echo "" + local CHOICE="" + prompt_text " Choice [1]:" "1" CHOICE + case "$CHOICE" in + 2) + _frigate_backup_existing + _frigate_clear_cameras + FRIGATE_SKIP_WIZARD=false + ;; + 4) + local REMOVE="" + prompt_text " Remove which numbers (space-separated) or 'all':" "" REMOVE + if [ "$REMOVE" = "all" ]; then + _frigate_clear_cameras + elif [ -n "$REMOVE" ]; then + _frigate_remove_by_index "$REMOVE" + fi + FRIGATE_SKIP_WIZARD=false + ;; + 3) + FRIGATE_SKIP_WIZARD=false + ;; + *) + FRIGATE_SKIP_WIZARD=true + ;; + esac +} + +# Next unused credential-var suffix, treating "no suffix" as 0. Kept cameras +# retain their existing var names untouched; new cameras never reuse one. +_frigate_next_suffix_int() { + local max=-1 re='^FRIGATE_RTSP_USER([0-9]*)$' v n i + for i in "${!CAM_VARUSER[@]}"; do + v="${CAM_VARUSER[$i]}" + [[ "$v" =~ $re ]] || continue + n="${BASH_REMATCH[1]}"; [ -z "$n" ] && n=0 + [ "$n" -gt "$max" ] && max="$n" + done + echo "$((max+1))" +} + +# Prompts to add cameras one at a time, appending to the CAM_* arrays. +_frigate_camera_wizard() { + while true; do + local ADD_CAM="" + if [ "${#CAM_NAME[@]}" -eq 0 ]; then + prompt_yn "Add a camera now? (y/n):" "y" ADD_CAM + else + prompt_yn "Add another camera? (y/n):" "n" ADD_CAM + fi + [[ "$ADD_CAM" =~ ^[Yy]$ ]] || break + + local NC_NAME="" + prompt_text " Camera name (e.g. front_door):" "camera$((${#CAM_NAME[@]}+1))" NC_NAME + NC_NAME="$(echo "$NC_NAME" | tr '[:upper:] ' '[:lower:]_' | tr -cd 'a-z0-9_')" + NC_NAME="${NC_NAME:-camera$((${#CAM_NAME[@]}+1))}" + local NC_UPPER="${NC_NAME^^}" + + local NC_IP="" + prompt_text " ${NC_NAME} RTSP IP address:" "" NC_IP + local NC_PORT="" + prompt_text " ${NC_NAME} RTSP port [554]:" "554" NC_PORT + local NC_USER="" + prompt_text " ${NC_NAME} RTSP username:" "admin" NC_USER + local NC_PASS="" + prompt_text " ${NC_NAME} RTSP password (blank = generate one):" "" NC_PASS + [ -z "$NC_PASS" ] && NC_PASS="$(generate_password 20)" + + local NC_PATH="" + prompt_text " ${NC_NAME} RTSP path (use {sub} for main/sub-stream marker) [/cam/realmonitor?channel=1&subtype={sub}#backchannel=0]:" \ + "/cam/realmonitor?channel=1&subtype={sub}#backchannel=0" NC_PATH + + local NC_SUBSTREAM="" + prompt_yn " Add a lower-res sub-stream for detection? (y/n):" "y" NC_SUBSTREAM + + local NC_ENABLED="" + prompt_yn " Enable ${NC_NAME} immediately? (y/n):" "y" NC_ENABLED + local NC_NOTIFY="" + prompt_yn " Enable notifications for ${NC_NAME}? (y/n):" "n" NC_NOTIFY + + local _suffix_int; _suffix_int="$(_frigate_next_suffix_int)" + local IDX_SUFFIX=""; [ "$_suffix_int" -gt 0 ] && IDX_SUFFIX="$_suffix_int" + + CAM_NAME+=("$NC_NAME") + CAM_PORT+=("$NC_PORT") + CAM_VARUSER+=("FRIGATE_RTSP_USER${IDX_SUFFIX}") + CAM_VARPASS+=("FRIGATE_RTSP_PASSWORD${IDX_SUFFIX}") + CAM_VARIP+=("FRIGATE_${NC_UPPER}_IP") + if [[ "$NC_SUBSTREAM" =~ ^[Yy]$ ]]; then CAM_HASSUB+=("true"); else CAM_HASSUB+=("false"); fi + CAM_MAIN_SUFFIX+=("${NC_PATH//\{sub\}/0}") + CAM_SUB_SUFFIX+=("${NC_PATH//\{sub\}/1}") + if [[ "$NC_ENABLED" =~ ^[Yy]$ ]]; then CAM_ENABLED+=("true"); else CAM_ENABLED+=("false"); fi + if [[ "$NC_NOTIFY" =~ ^[Yy]$ ]]; then CAM_NOTIFY+=("true"); else CAM_NOTIFY+=("false"); fi + CAM_CRED_USER+=("$NC_USER") + CAM_CRED_PASS+=("$NC_PASS") + CAM_CRED_IP+=("$NC_IP") + done +} + +# Builds GO2RTC_BLOCK / CAMERAS_BLOCK / ENV_CAM_VARS from the CAM_* arrays. +_frigate_render_config() { + GO2RTC_BLOCK=""; CAMERAS_BLOCK=""; ENV_CAM_VARS="" + local i + for i in "${!CAM_NAME[@]}"; do + local nm="${CAM_NAME[$i]}" port="${CAM_PORT[$i]}" + local vu="${CAM_VARUSER[$i]}" vp="${CAM_VARPASS[$i]}" vi="${CAM_VARIP[$i]}" + local hassub="${CAM_HASSUB[$i]}" mainsuf="${CAM_MAIN_SUFFIX[$i]}" subsuf="${CAM_SUB_SUFFIX[$i]}" + local enabled="${CAM_ENABLED[$i]}" notify="${CAM_NOTIFY[$i]}" + local cu="${CAM_CRED_USER[$i]}" cp="${CAM_CRED_PASS[$i]}" ci="${CAM_CRED_IP[$i]}" + + ENV_CAM_VARS="${ENV_CAM_VARS}${vu}=${cu} +${vp}=${cp} +${vi}=${ci} +" + GO2RTC_BLOCK="${GO2RTC_BLOCK} ${nm}: + - rtsp://{${vu}}:{${vp}}@{${vi}}:${port}${mainsuf} +" + local inputs_block + if [ "$hassub" = true ]; then + GO2RTC_BLOCK="${GO2RTC_BLOCK} ${nm}_sub: + - rtsp://{${vu}}:{${vp}}@{${vi}}:${port}${subsuf} +" + inputs_block=" - path: rtsp://127.0.0.1:8554/${nm} + input_args: preset-rtsp-restream + roles: + - record + - path: rtsp://127.0.0.1:8554/${nm}_sub + input_args: preset-rtsp-restream + roles: + - detect" + else + inputs_block=" - path: rtsp://127.0.0.1:8554/${nm} + input_args: preset-rtsp-restream + roles: + - detect + - record" + fi + GO2RTC_BLOCK="${GO2RTC_BLOCK} +" + CAMERAS_BLOCK="${CAMERAS_BLOCK} ${nm}: + enabled: ${enabled} + ffmpeg: + inputs: +${inputs_block} + detect: + enabled: true + width: 640 + height: 480 + fps: 5 + notifications: + enabled: ${notify} + +" + done +} + install_frigate() { require_docker || return 1 local FRIGATE_DIR="$DOCKER_DIR/frigate" - local DEFAULT_MEDIA="$ACTUAL_HOME/frigate" if [ "$DRY_RUN" = true ]; then echo "[DRY-RUN] Frigate would:" echo " - Create $FRIGATE_DIR with docker-compose.yml + .env + config/config.yml" echo " - Auto-enable /dev/dri/renderD128 for GPU-assisted detection if present" echo " - Expose ports 5000 (web), 8554 (RTSP restream), 8555 (WebRTC)" + echo " - If already configured: show existing cameras and offer to keep," + echo " back up + start fresh, add more, or remove some" echo " - Prompt to add cameras interactively (RTSP creds go in .env)" echo " or write a starter config.yml if none are added" echo " - Offer a Caddy reverse proxy and to start the container" return 0 fi - local FRIGATE_MEDIA="" - prompt_text "Path for recordings/snapshots [$DEFAULT_MEDIA]:" "$DEFAULT_MEDIA" FRIGATE_MEDIA - FRIGATE_MEDIA="${FRIGATE_MEDIA/#\~/$ACTUAL_HOME}"; FRIGATE_MEDIA="${FRIGATE_MEDIA%/}" - mkdir -p "$FRIGATE_DIR" ensure_docker_dir_ownership "$FRIGATE_DIR" cd "$FRIGATE_DIR" || return 1 - local TZ_VAL; TZ_VAL="${SITE_TZ:-$(cat /etc/timezone 2>/dev/null || echo UTC)}" + local CAM_NAME=() CAM_PORT=() CAM_VARUSER=() CAM_VARPASS=() CAM_VARIP=() + local CAM_HASSUB=() CAM_MAIN_SUFFIX=() CAM_SUB_SUFFIX=() CAM_ENABLED=() CAM_NOTIFY=() + local CAM_CRED_USER=() CAM_CRED_PASS=() CAM_CRED_IP=() + local FRIGATE_SKIP_WIZARD=false + declare -A ENV_MAP=() - # Hardware detection: include /dev/dri only when a render node exists - local DEVICE_BLOCK="" - if [ -e /dev/dri/renderD128 ]; then - DEVICE_BLOCK=" devices: - - /dev/dri/renderD128:/dev/dri/renderD128" - log_success "Render node found — enabling hardware-accelerated detection" - else - log_warning "No /dev/dri/renderD128 — Frigate will use CPU detection." + if [ -f config/config.yml ] && [ -f .env ]; then + _frigate_parse_existing config/config.yml .env + if [ "${#CAM_NAME[@]}" -gt 0 ]; then + _frigate_review_existing + else + log_warning "Existing config.yml found but no cameras could be parsed from it." + local PROCEED_ANYWAY="" + prompt_yn " Back up existing config and continue with the wizard? (y/n):" "y" PROCEED_ANYWAY + if [[ "$PROCEED_ANYWAY" =~ ^[Yy]$ ]]; then + _frigate_backup_existing + else + log_info "Leaving existing config untouched." + return 0 + fi + fi fi - local _CADDY_NET_BLOCK="" - if [ -d "$DOCKER_DIR/caddy" ]; then - _CADDY_NET_BLOCK=" networks: + if [ "$FRIGATE_SKIP_WIZARD" = true ]; then + log_success "Keeping existing Frigate configuration unchanged ($DOCKER_DIR/frigate)." + else + local DEFAULT_MEDIA="${ENV_MAP[FRIGATE_MEDIA]:-$ACTUAL_HOME/frigate}" + local FRIGATE_MEDIA="" + prompt_text "Path for recordings/snapshots [$DEFAULT_MEDIA]:" "$DEFAULT_MEDIA" FRIGATE_MEDIA + FRIGATE_MEDIA="${FRIGATE_MEDIA/#\~/$ACTUAL_HOME}"; FRIGATE_MEDIA="${FRIGATE_MEDIA%/}" + + local TZ_VAL; TZ_VAL="${SITE_TZ:-$(cat /etc/timezone 2>/dev/null || echo UTC)}" + + # Hardware detection: include /dev/dri only when a render node exists + local DEVICE_BLOCK="" + if [ -e /dev/dri/renderD128 ]; then + DEVICE_BLOCK=" devices: + - /dev/dri/renderD128:/dev/dri/renderD128" + log_success "Render node found — enabling hardware-accelerated detection" + else + log_warning "No /dev/dri/renderD128 — Frigate will use CPU detection." + fi + + local _CADDY_NET_BLOCK="" + if [ -d "$DOCKER_DIR/caddy" ]; then + _CADDY_NET_BLOCK=" networks: - caddy_net " - fi + fi - local _CADDY_NET_SECTION="" - if [ -d "$DOCKER_DIR/caddy" ]; then - _CADDY_NET_SECTION=" + local _CADDY_NET_SECTION="" + if [ -d "$DOCKER_DIR/caddy" ]; then + _CADDY_NET_SECTION=" networks: caddy_net: external: true name: ${SITE_CADDY_NET:-caddy_net} " - fi + fi - cat > docker-compose.yml << FRIGATE_COMPOSE + cat > docker-compose.yml << FRIGATE_COMPOSE name: frigate services: @@ -282,117 +606,18 @@ $DEVICE_BLOCK ${_CADDY_NET_BLOCK}${_CADDY_NET_SECTION} FRIGATE_COMPOSE - mkdir -p config - mkdir -p "$FRIGATE_MEDIA" + mkdir -p config + mkdir -p "$FRIGATE_MEDIA" - # ── Camera wizard ──────────────────────────────────────────────────────── - # Credentials/IPs go in .env as FRIGATE_* variables; Frigate substitutes - # any {FRIGATE_VAR} placeholder in config.yml from its container env at - # startup, so RTSP secrets never need to be typed into the YAML directly. - local GO2RTC_BLOCK="" CAMERAS_BLOCK="" ENV_CAM_VARS="" CAM_INDEX=0 - while true; do - local ADD_CAM="" - if [ "$CAM_INDEX" -eq 0 ]; then - prompt_yn "Add a camera now? (y/n):" "y" ADD_CAM - else - prompt_yn "Add another camera? (y/n):" "n" ADD_CAM - fi - [[ "$ADD_CAM" =~ ^[Yy]$ ]] || break + # Credentials/IPs go in .env as FRIGATE_* variables; Frigate substitutes + # any {FRIGATE_VAR} placeholder in config.yml from its container env at + # startup, so RTSP secrets never need to be typed into the YAML directly. + _frigate_camera_wizard + _frigate_render_config - local CAM_NAME="" - prompt_text " Camera name (e.g. front_door):" "camera$((CAM_INDEX+1))" CAM_NAME - CAM_NAME="$(echo "$CAM_NAME" | tr '[:upper:] ' '[:lower:]_' | tr -cd 'a-z0-9_')" - CAM_NAME="${CAM_NAME:-camera$((CAM_INDEX+1))}" - local CAM_UPPER="${CAM_NAME^^}" - - local CAM_IP="" - prompt_text " ${CAM_NAME} RTSP IP address:" "" CAM_IP - local CAM_PORT="" - prompt_text " ${CAM_NAME} RTSP port [554]:" "554" CAM_PORT - local CAM_USER="" - prompt_text " ${CAM_NAME} RTSP username:" "admin" CAM_USER - local CAM_PASS="" - prompt_text " ${CAM_NAME} RTSP password (blank = generate one):" "" CAM_PASS - [ -z "$CAM_PASS" ] && CAM_PASS="$(generate_password 20)" - - local CAM_PATH="" - prompt_text " ${CAM_NAME} RTSP path (use {sub} for main/sub-stream marker) [/cam/realmonitor?channel=1&subtype={sub}#backchannel=0]:" \ - "/cam/realmonitor?channel=1&subtype={sub}#backchannel=0" CAM_PATH - - local CAM_SUBSTREAM="" - prompt_yn " Add a lower-res sub-stream for detection? (y/n):" "y" CAM_SUBSTREAM - - local CAM_ENABLED="" - prompt_yn " Enable ${CAM_NAME} immediately? (y/n):" "y" CAM_ENABLED - local CAM_NOTIFY="" - prompt_yn " Enable notifications for ${CAM_NAME}? (y/n):" "n" CAM_NOTIFY - - # First camera's credentials get no suffix (FRIGATE_RTSP_USER); each - # camera after that gets an index suffix (…USER1, …USER2, …) so - # cameras with different logins don't collide. - local IDX_SUFFIX="" - [ "$CAM_INDEX" -gt 0 ] && IDX_SUFFIX="$CAM_INDEX" - local VAR_USER="FRIGATE_RTSP_USER${IDX_SUFFIX}" - local VAR_PASS="FRIGATE_RTSP_PASSWORD${IDX_SUFFIX}" - local VAR_IP="FRIGATE_${CAM_UPPER}_IP" - - ENV_CAM_VARS="${ENV_CAM_VARS}${VAR_USER}=${CAM_USER} -${VAR_PASS}=${CAM_PASS} -${VAR_IP}=${CAM_IP} -" - - local MAIN_PATH="${CAM_PATH//\{sub\}/0}" - local SUB_PATH="${CAM_PATH//\{sub\}/1}" - - GO2RTC_BLOCK="${GO2RTC_BLOCK} ${CAM_NAME}: - - rtsp://{${VAR_USER}}:{${VAR_PASS}}@{${VAR_IP}}:${CAM_PORT}${MAIN_PATH} -" - local INPUTS_BLOCK - if [[ "$CAM_SUBSTREAM" =~ ^[Yy]$ ]]; then - GO2RTC_BLOCK="${GO2RTC_BLOCK} ${CAM_NAME}_sub: - - rtsp://{${VAR_USER}}:{${VAR_PASS}}@{${VAR_IP}}:${CAM_PORT}${SUB_PATH} -" - INPUTS_BLOCK=" - path: rtsp://127.0.0.1:8554/${CAM_NAME} - input_args: preset-rtsp-restream - roles: - - record - - path: rtsp://127.0.0.1:8554/${CAM_NAME}_sub - input_args: preset-rtsp-restream - roles: - - detect" - else - INPUTS_BLOCK=" - path: rtsp://127.0.0.1:8554/${CAM_NAME} - input_args: preset-rtsp-restream - roles: - - detect - - record" - fi - GO2RTC_BLOCK="${GO2RTC_BLOCK} -" - - local ENABLED_VAL="false"; [[ "$CAM_ENABLED" =~ ^[Yy]$ ]] && ENABLED_VAL="true" - local NOTIFY_VAL="false"; [[ "$CAM_NOTIFY" =~ ^[Yy]$ ]] && NOTIFY_VAL="true" - - CAMERAS_BLOCK="${CAMERAS_BLOCK} ${CAM_NAME}: - enabled: ${ENABLED_VAL} - ffmpeg: - inputs: -${INPUTS_BLOCK} - detect: - enabled: true - width: 640 - height: 480 - fps: 5 - notifications: - enabled: ${NOTIFY_VAL} - -" - CAM_INDEX=$((CAM_INDEX+1)) - done - - if [ "$CAM_INDEX" -eq 0 ]; then - # No cameras entered — write a starter config the operator edits by hand. - cat > config/config.yml << 'FRIGATE_CONFIG' + if [ "${#CAM_NAME[@]}" -eq 0 ]; then + # No cameras entered — write a starter config the operator edits by hand. + cat > config/config.yml << 'FRIGATE_CONFIG' # Frigate Configuration — Docs: https://docs.frigate.video # # ⚠️ YOU MUST EDIT THIS FILE to add your cameras before starting Frigate. @@ -427,8 +652,8 @@ snapshots: retain: default: 7 FRIGATE_CONFIG - else - cat > config/config.yml << FRIGATE_CONFIG + else + cat > config/config.yml << FRIGATE_CONFIG # Frigate Configuration — Docs: https://docs.frigate.video # # RTSP credentials/IPs come from .env — Frigate substitutes {FRIGATE_VAR} @@ -457,22 +682,22 @@ snapshots: retain: default: 7 FRIGATE_CONFIG - fi + fi - cat > .env << FRIGATE_ENV + cat > .env << FRIGATE_ENV FRIGATE_MEDIA=$FRIGATE_MEDIA CADDY_NET=$SITE_CADDY_NET ${ENV_CAM_VARS} FRIGATE_ENV - chmod 600 .env + chmod 600 .env - chown -R "$ACTUAL_USER:$ACTUAL_USER" "$FRIGATE_DIR" - chown -R "$ACTUAL_USER:$ACTUAL_USER" "$FRIGATE_MEDIA" 2>/dev/null || true - log_success "Frigate configured at $FRIGATE_DIR" + chown -R "$ACTUAL_USER:$ACTUAL_USER" "$FRIGATE_DIR" + chown -R "$ACTUAL_USER:$ACTUAL_USER" "$FRIGATE_MEDIA" 2>/dev/null || true + log_success "Frigate configured at $FRIGATE_DIR" - configure_caddy_for_service "Frigate" "frigate:5000" "frigate" + configure_caddy_for_service "Frigate" "frigate:5000" "frigate" - write_readme "$FRIGATE_DIR" << MD + write_readme "$FRIGATE_DIR" << MD # Frigate NVR AI-powered network video recorder with real-time object detection for @@ -482,7 +707,7 @@ security cameras. Detects people, cars, animals, and more. - RTSP restream: port 8554 - WebRTC: port 8555 - Recordings: \`$FRIGATE_MEDIA\` -- Config: \`config/config.yml\` — cameras configured during install ($CAM_INDEX added) +- Config: \`config/config.yml\` — cameras configured during install (${#CAM_NAME[@]} total) - Credentials: \`.env\` — RTSP user/pass/IP per camera as FRIGATE_* variables ## Manage @@ -495,10 +720,12 @@ docker compose pull && docker compose up -d # update \`\`\` ## Adding/editing cameras later -Re-run the installer (\`sudo ./setup.sh frigate\`) to add more cameras through -the same wizard, or edit \`config/config.yml\` and \`.env\` by hand — camera -credentials in config.yml use Frigate's \`{FRIGATE_VAR}\` substitution syntax, -resolved from \`.env\` at container startup. +Re-run the installer (\`sudo ./setup.sh frigate\`) — it detects your existing +config.yml/.env and lets you keep everything as-is, back up and start fresh, +add more cameras, or remove specific ones by number. Or edit +\`config/config.yml\` and \`.env\` by hand — camera credentials in config.yml +use Frigate's \`{FRIGATE_VAR}\` substitution syntax, resolved from \`.env\` at +container startup. ## First steps 1. Review \`config/config.yml\` — adjust detection zones, masks, retention @@ -510,13 +737,14 @@ resolved from \`.env\` at container startup. - Google Coral TPU: set \`detectors.default.type: edgetpu\` + add USB device - Docs: https://docs.frigate.video/configuration/hardware_acceleration MD + fi echo "" local START_DEFAULT="n" - if [ "$CAM_INDEX" -eq 0 ]; then + if [ "${#CAM_NAME[@]}" -eq 0 ]; then log_warning "No cameras added — edit config/config.yml to add camera RTSP streams before starting." else - log_success "$CAM_INDEX camera(s) configured." + log_success "${#CAM_NAME[@]} camera(s) configured." START_DEFAULT="y" fi echo ""