diff --git a/MODULAR.md b/MODULAR.md index 5c3c920..c1aa1c6 100644 --- a/MODULAR.md +++ b/MODULAR.md @@ -95,7 +95,7 @@ is retained as a frozen evolution record. | `homelab` | `caddy`, `crowdsec`, `authelia`, `homeassistant` | | `utilities` | `actualbudget`, `ddclient`, `filebrowser`, `fmd`, `magicmirror`, `mealie`, `meshcentral`, `ntfy`, `portainer`, `traccar`, `uptimekuma`, `watchtower`, `wg-easy` | | `media` | `arm`, `audiobookshelf`, `emby`, `immich`, `jellyfin`, `lyrion` | -| `cameras` | `frigate`, `frigate-notify` | +| `cameras` | `frigate`, `frigate-audio`, `frigate-notify`, `sky-cam` | | `gaming` | `js99er`, `minecraft`, `wolf`, `wolf-pair` | | `extras` | `linux-to-sync`, `silent-send`, `sync-cc` | | `backup` | `backup` | diff --git a/services/frigate-audio.sh b/services/frigate-audio.sh new file mode 100644 index 0000000..9406975 --- /dev/null +++ b/services/frigate-audio.sh @@ -0,0 +1,557 @@ +#!/bin/bash +# services/frigate-audio.sh — Frigate NVR + Mosquitto MQTT + frigate-notify +# full-stack with audio support and push notifications via ntfy. +# Part of the modular post-install system (sourced by setup.sh). +# +# Based on outis1one/frigate_w_audio. This is the full stack: +# Frigate 0.17 NVR, face recognition, LPR, motion detection +# Mosquitto MQTT broker (events bus between Frigate and notify) +# frigate-notify Event consumer — sends ntfy push notifications +# +# Audio is OFF by default in the Frigate config (audio.enabled: false). +# To enable it you need at least one camera with a working microphone — +# see the HOW TO ADD A CAMERA WITH A MIC section in the generated config.yml. +# +# Hardware acceleration and Coral TPU are opt-in during setup; the +# default falls back to CPU detection so the stack runs everywhere. +# +# Differs from services/frigate.sh (simpler, standalone Frigate only): +# • includes Mosquitto + frigate-notify +# • audio-ready camera config template +# • Frigate 0.17 schema with face recognition + LPR pre-configured + +register_service frigate-audio cameras "Frigate NVR + MQTT + push notifications (audio-ready stack)" 8971 + +install_frigate-audio() { + require_docker || return 1 + + local DIR="$DOCKER_DIR/frigate-audio" + + if [ "$DRY_RUN" = true ]; then + echo "[DRY-RUN] frigate-audio would:" + echo " Create $DIR with Frigate + Mosquitto + frigate-notify stack" + echo " Prompt for camera RTSP credentials, IPs, MQTT password, ntfy server" + echo " Generate docker-compose.yml, frigate config, mosquitto config, .env" + echo " Bootstrap the Mosquitto password file" + return 0 + fi + + echo "" + echo "╔═══════════════════════════════════════════════════════════════╗" + echo "║ Frigate + Mosquitto + frigate-notify (audio-ready stack) ║" + echo "║ Face recognition · LPR · ntfy push alerts ║" + echo "╚═══════════════════════════════════════════════════════════════╝" + echo "" + + # ── Media storage path ───────────────────────────────────────────────────── + log_info "Frigate media storage (recordings, snapshots)" + echo " Recordings can fill tens of GB quickly — a dedicated drive is recommended." + echo "" + local FRIGATE_MEDIA_DIR="" + local DEFAULT_MEDIA="$DOCKER_DIR/frigate-audio/media" + if declare -f select_storage_path &>/dev/null; then + select_storage_path "Frigate recordings" FRIGATE_MEDIA_DIR + [ -z "$FRIGATE_MEDIA_DIR" ] && FRIGATE_MEDIA_DIR="$DEFAULT_MEDIA" + else + prompt_text "Frigate media path [$DEFAULT_MEDIA]:" "$DEFAULT_MEDIA" FRIGATE_MEDIA_DIR + fi + log_info "Media path: $FRIGATE_MEDIA_DIR" + + # ── Camera credentials ───────────────────────────────────────────────────── + echo "" + log_info "Camera 1 — Front Door (required)" + local CAM1_USER="" CAM1_PASS="" CAM1_IP="" + prompt_text " RTSP username [admin]:" "admin" CAM1_USER + prompt_text " RTSP password:" "" CAM1_PASS + prompt_text " Camera IP [192.168.1.100]:" "192.168.1.100" CAM1_IP + + echo "" + log_info "Camera 2 — Back Door (optional, press Enter to skip IP)" + local CAM2_USER="" CAM2_PASS="" CAM2_IP="" + prompt_text " RTSP username [admin]:" "admin" CAM2_USER + prompt_text " RTSP password [changeme]:" "changeme" CAM2_PASS + prompt_text " Camera IP (Enter to disable):" "" CAM2_IP + + echo "" + log_info "Camera 3 — Third camera (optional)" + local CAM3_USER="" CAM3_PASS="" CAM3_IP="" + prompt_text " RTSP username [admin]:" "admin" CAM3_USER + prompt_text " RTSP password [changeme]:" "changeme" CAM3_PASS + prompt_text " Camera IP (Enter to disable):" "" CAM3_IP + + # ── MQTT password ────────────────────────────────────────────────────────── + echo "" + log_info "MQTT credentials (Frigate ↔ Mosquitto ↔ frigate-notify)" + local MQTT_PASS="" + prompt_text " MQTT username [frigate]:" "frigate" MQTT_USER + if [ -z "${MQTT_USER:-}" ]; then MQTT_USER="frigate"; fi + MQTT_PASS=$(generate_password 24) + log_info " Generated MQTT password: $MQTT_PASS" + + # ── ntfy server ───────────────────────────────────────────────────────── + echo "" + log_info "ntfy push notifications" + echo " frigate-notify sends alerts via ntfy. Set to your ntfy server URL." + local NTFY_SERVER="" NTFY_TOPIC="frigate" + prompt_text " ntfy server URL [https://ntfy.yourdomain.com]:" "https://ntfy.yourdomain.com" NTFY_SERVER + prompt_text " ntfy topic [frigate]:" "frigate" NTFY_TOPIC + + # ── Frigate public URL ───────────────────────────────────────────────────── + echo "" + local BASE_DOMAIN="" + [ -f "$DOCKER_DIR/.config" ] && BASE_DOMAIN=$(grep '^BASE_DOMAIN=' "$DOCKER_DIR/.config" 2>/dev/null | cut -d= -f2-) + local FRIGATE_PUBLIC_URL="" + if [ -n "$BASE_DOMAIN" ]; then + local _PFX="" + prompt_text " Subdomain prefix for Frigate [cam].${BASE_DOMAIN}:" "cam" _PFX + FRIGATE_PUBLIC_URL="https://${_PFX:-cam}.${BASE_DOMAIN}" + else + prompt_text " Frigate public URL [https://cam.yourdomain.com]:" "https://cam.yourdomain.com" FRIGATE_PUBLIC_URL + fi + + # ── Detector choice ──────────────────────────────────────────────────────── + echo "" + log_info "Object detector" + echo " 1) CPU (works everywhere, higher CPU usage)" + echo " 2) USB Coral TPU (faster detection, lower CPU — requires USB Coral stick)" + echo " 3) PCIe Coral TPU" + local DET_CHOICE="" + prompt_text "Detector [1]:" "1" DET_CHOICE + local DETECTOR_BLOCK HWA_COMMENT + case "${DET_CHOICE:-1}" in + 2) DETECTOR_BLOCK="detectors:\n coral:\n type: edgetpu\n device: usb" + HWA_COMMENT=" devices:\n - /dev/bus/usb:/dev/bus/usb # USB Coral" ;; + 3) DETECTOR_BLOCK="detectors:\n coral:\n type: edgetpu\n device: pci" + HWA_COMMENT=" devices:\n - /dev/apex_0:/dev/apex_0 # PCIe Coral" ;; + *) DETECTOR_BLOCK="detectors:\n cpu:\n type: cpu\n num_threads: 3" + HWA_COMMENT="" ;; + esac + + # ── Hardware acceleration for re-encoding ────────────────────────────────── + echo "" + local HWA="" + prompt_yn " Enable hardware video decode (Intel/AMD /dev/dri/renderD128)? (y/n) [n]:" "n" HWA + local DRI_LINE="" + [[ ${HWA:-n} =~ ^[Yy]$ ]] && DRI_LINE=" - /dev/dri/renderD128 # Intel/AMD hwaccel" + + # ── Create directory structure ───────────────────────────────────────────── + mkdir -p "$DIR"/{frigate_config,mosquitto/config,mosquitto/data,mosquitto/log,"frigate-notify"} + mkdir -p "$FRIGATE_MEDIA_DIR" + ensure_docker_dir_ownership "$DIR" + cd "$DIR" || return 1 + + # ── .env ────────────────────────────────────────────────────────────────── + log_info "Writing .env..." + cat > "$DIR/.env" << ENVEOF +# Frigate audio stack — generated by setup.sh +# DO NOT commit this file — it contains credentials. + +# ---- Camera 1: Front Door ---- +FRIGATE_RTSP_USER=${CAM1_USER:-admin} +FRIGATE_RTSP_PASSWORD=${CAM1_PASS:-changeme} +FRIGATE_FRONT_DOOR_IP=${CAM1_IP:-192.168.1.100} + +# ---- Camera 2: Back Door ---- +FRIGATE_RTSP_USER1=${CAM2_USER:-admin} +FRIGATE_RTSP_PASSWORD1=${CAM2_PASS:-changeme} +FRIGATE_BACK_DOOR_IP=${CAM2_IP:-192.168.1.101} + +# ---- Camera 3 (optional) ---- +FRIGATE_RTSP_USER2=${CAM3_USER:-admin} +FRIGATE_RTSP_PASSWORD2=${CAM3_PASS:-changeme} +FRIGATE_SQUIRREL_IP=${CAM3_IP:-192.168.1.102} + +# ---- MQTT ---- +FRIGATE_MQTT_USER=${MQTT_USER:-frigate} +FRIGATE_MQTT_PASSWORD=${MQTT_PASS} + +# ---- frigate-notify ---- +FN_FRIGATE__MQTT__PASSWORD=${MQTT_PASS} +FN_FRIGATE__SERVER=http://frigate:5000 +FN_FRIGATE__PUBLIC_URL=${FRIGATE_PUBLIC_URL} +FN_ALERTS__NTFY__SERVER=${NTFY_SERVER} +ENVEOF + chmod 600 "$DIR/.env" + log_success ".env written" + + # ── docker-compose.yml ───────────────────────────────────────────────────── + log_info "Writing docker-compose.yml..." + + local DEVICES_BLOCK="" + [ -n "$HWA_COMMENT" ] && DEVICES_BLOCK=" devices:\n${HWA_COMMENT}" + [ -n "$DRI_LINE" ] && DEVICES_BLOCK="${DEVICES_BLOCK}\n ${DRI_LINE}" + if [ -n "$DET_CHOICE" ] && [ "$DET_CHOICE" = "2" ]; then + DEVICES_BLOCK=" devices:\n${HWA_COMMENT}" + [ -n "$DRI_LINE" ] && DEVICES_BLOCK="${DEVICES_BLOCK}\n ${DRI_LINE}" + fi + + cat > "$DIR/docker-compose.yml" << 'COMPOSEEOF' +# Frigate NVR + Mosquitto MQTT + frigate-notify +# Generated by ubuntu-post-install setup.sh +name: frigate-audio + +services: + + frigate: + container_name: frigate-audio + image: ghcr.io/blakeblackshear/frigate:0.17.1 + restart: unless-stopped + stop_grace_period: 30s + privileged: true + shm_size: "512mb" + env_file: .env + depends_on: + - mosquitto +COMPOSEEOF + + # Inject devices block if hardware acceleration chosen + if [ -n "$HWA_COMMENT" ] || [ -n "$DRI_LINE" ]; then + echo " devices:" >> "$DIR/docker-compose.yml" + [ -n "$HWA_COMMENT" ] && printf " %s\n" "$HWA_COMMENT" | sed 's|^ *||' >> "$DIR/docker-compose.yml" + [ -n "$DRI_LINE" ] && echo " $DRI_LINE" >> "$DIR/docker-compose.yml" + fi + + cat >> "$DIR/docker-compose.yml" << COMPOSEEOF + volumes: + - /etc/localtime:/etc/localtime:ro + - ./frigate_config:/config + - ${FRIGATE_MEDIA_DIR}:/media/frigate + - type: tmpfs + target: /tmp/cache + tmpfs: + size: 1000000000 + ports: + - "8971:8971" + - "5001:5000" + - "8554:8554" + - "8555:8555/tcp" + - "8555:8555/udp" + healthcheck: + test: ["CMD", "curl", "-f", "http://127.0.0.1:5000/api/version"] + interval: 10s + timeout: 5s + retries: 12 + start_period: 60s + + mosquitto: + container_name: frigate-audio-mqtt + hostname: mosquitto + image: eclipse-mosquitto:2 + restart: unless-stopped + ports: + - "1883:1883" + volumes: + - ./mosquitto/config:/mosquitto/config + - ./mosquitto/data:/mosquitto/data + - ./mosquitto/log:/mosquitto/log + + frigate-notify: + container_name: frigate-audio-notify + hostname: frigate-notify + image: ghcr.io/0x2142/frigate-notify:latest + restart: unless-stopped + env_file: .env + depends_on: + mosquitto: + condition: service_started + frigate: + condition: service_healthy + volumes: + - ./frigate-notify/config.yml:/app/config.yml:ro +COMPOSEEOF + log_success "docker-compose.yml written" + + # ── Mosquitto config ─────────────────────────────────────────────────────── + log_info "Writing Mosquitto config..." + cat > "$DIR/mosquitto/config/mosquitto.conf" << 'MQTTEOF' +listener 1883 0.0.0.0 +protocol mqtt + +persistence true +persistence_location /mosquitto/data/ + +log_dest stdout +log_dest file /mosquitto/log/mosquitto.log + +allow_anonymous false +password_file /mosquitto/config/passwd +MQTTEOF + + # Bootstrap the Mosquitto password file + log_info "Bootstrapping Mosquitto password file..." + if docker run --rm -i eclipse-mosquitto:2 \ + mosquitto_passwd -b -c /dev/stdout "$MQTT_USER" "$MQTT_PASS" \ + > "$DIR/mosquitto/config/passwd" 2>/dev/null; then + log_success "Mosquitto passwd file created" + else + log_warning "Could not bootstrap Mosquitto passwd — do it manually:" + log_warning " docker run --rm eclipse-mosquitto:2 mosquitto_passwd -b -c /passwd ${MQTT_USER} '${MQTT_PASS}'" + log_warning " Then copy the output to ${DIR}/mosquitto/config/passwd" + fi + + # ── Frigate config.yml ───────────────────────────────────────────────────── + log_info "Writing Frigate config..." + local CAM2_ENABLED="false"; [ -n "$CAM2_IP" ] && CAM2_ENABLED="true" + local CAM3_ENABLED="false"; [ -n "$CAM3_IP" ] && CAM3_ENABLED="true" + + local DETECTOR_YAML + case "${DET_CHOICE:-1}" in + 2) DETECTOR_YAML="detectors:\n coral:\n type: edgetpu\n device: usb" ;; + 3) DETECTOR_YAML="detectors:\n coral:\n type: edgetpu\n device: pci" ;; + *) DETECTOR_YAML="detectors:\n cpu:\n type: cpu\n num_threads: 3" ;; + esac + + cat > "$DIR/frigate_config/config.yml" << FRIGCFGEOF +version: 0.17-0 + +mqtt: + enabled: true + host: mosquitto + port: 1883 + user: "{FRIGATE_MQTT_USER}" + password: "{FRIGATE_MQTT_PASSWORD}" + topic_prefix: frigate + client_id: frigate + stats_interval: 60 + +tls: + enabled: false + +# Audio detection — set true when you have a camera with a working mic. +# See the HOW TO ADD A CAMERA WITH A MIC section at the bottom of this file. +audio: + enabled: false + +$(printf "$DETECTOR_YAML") + +birdseye: + mode: continuous + +semantic_search: + enabled: false + model_size: small + +face_recognition: + enabled: true + model_size: small + +lpr: + enabled: true + model_size: small + +objects: + track: + - person + +record: + enabled: true + continuous: + days: 0 + motion: + days: 10 + +go2rtc: + streams: + front_door: + - rtsp://{FRIGATE_RTSP_USER}:{FRIGATE_RTSP_PASSWORD}@{FRIGATE_FRONT_DOOR_IP}:554/Streaming/Channels/101 + back_door: + - rtsp://{FRIGATE_RTSP_USER1}:{FRIGATE_RTSP_PASSWORD1}@{FRIGATE_BACK_DOOR_IP}:554/Streaming/Channels/101 + squirrel: + - rtsp://{FRIGATE_RTSP_USER2}:{FRIGATE_RTSP_PASSWORD2}@{FRIGATE_SQUIRREL_IP}:554/Streaming/Channels/101 + +cameras: + front_door: + enabled: true + ffmpeg: + inputs: + - path: rtsp://127.0.0.1:8554/front_door + input_args: preset-rtsp-restream + roles: + - detect + - record + detect: + enabled: true + width: 2688 + height: 1520 + fps: 5 + + back_door: + enabled: ${CAM2_ENABLED} + ffmpeg: + inputs: + - path: rtsp://127.0.0.1:8554/back_door + input_args: preset-rtsp-restream + roles: + - detect + - record + detect: + enabled: true + width: 2688 + height: 1520 + fps: 5 + + squirrel: + enabled: ${CAM3_ENABLED} + ffmpeg: + inputs: + - path: rtsp://127.0.0.1:8554/squirrel + input_args: preset-rtsp-restream + roles: + - detect + - record + detect: + enabled: true + width: 2688 + height: 1520 + fps: 5 + +############################################################################## +# HOW TO ADD A CAMERA WITH A MIC +# +# 1. Set audio.enabled: true at the top of this file. +# +# 2. In go2rtc.streams, add the audio transcode line: +# your_cam: +# - rtsp://{FRIGATE_RTSP_USER3}:{FRIGATE_RTSP_PASSWORD3}@{IP}:554/path#backchannel=0 +# - "ffmpeg:your_cam#audio=aac#audio=opus" +# +# 3. In cameras, add the 'audio' role and audio-aware record preset: +# your_cam: +# enabled: true +# ffmpeg: +# output_args: +# record: preset-record-generic-audio-aac +# inputs: +# - path: rtsp://127.0.0.1:8554/your_cam +# input_args: preset-rtsp-restream +# roles: +# - detect +# - record +# - audio +# +# 4. Add credentials to .env: +# FRIGATE_RTSP_USER3=admin +# FRIGATE_RTSP_PASSWORD3=yourpass +# +# 5. RTSP paths by vendor: +# Hikvision / Hikvision OEM: /Streaming/Channels/101 (main), /102 (sub) +# Dahua / Dahua OEM: /cam/realmonitor?channel=1&subtype=0 (main) +############################################################################## +FRIGCFGEOF + log_success "Frigate config.yml written" + + # ── frigate-notify config.yml ───────────────────────────────────────────── + log_info "Writing frigate-notify config..." + cat > "$DIR/frigate-notify/config.yml" << FNEOF +## frigate-notify config +## Docs: https://frigate-notify.0x2142.com +## Secrets come from .env via FN_* environment variables. + +frigate: + server: # FN_FRIGATE__SERVER + ignoressl: true + public_url: # FN_FRIGATE__PUBLIC_URL + + startup_check: + attempts: 5 + interval: 30 + + mqtt: + enabled: true + server: mosquitto + port: 1883 + clientid: frigate-notify + username: ${MQTT_USER:-frigate} + password: # FN_FRIGATE__MQTT__PASSWORD + topic_prefix: frigate + +alerts: + general: + title: 'Frigate - {{ if .SubLabel }}{{ .SubLabel }}{{ else }}{{ .Label }}{{ end }} at {{ .Camera }}' + nosnap: allow + recheck_delay: 10 + + ntfy: + enabled: true + server: # FN_ALERTS__NTFY__SERVER + topic: "${NTFY_TOPIC:-frigate}" + ignoressl: false + headers: + - X-Priority: '{{ if .SubLabel }}3{{ else }}4{{ end }}' + - X-Tags: '{{ if .SubLabel }}wave{{ else }}rotating_light{{ end }}' + template: | + {{ if .SubLabel -}}{{ .SubLabel }}{{ else }}{{ .Label }}{{ end }} at {{ .Camera }} + {{- if gt (len .CurrentZones) 0 }} + Zone: {{ range \$i, \$z := .CurrentZones }}{{ if \$i }}, {{ end }}{{ \$z }}{{ end }}{{ end }} + Score: {{ printf "%.0f" (mul .TopScore 100) }}% + Time: {{ .StartTime.Format "Mon 3:04 PM" }} + +monitor: + enabled: false + + discord: + enabled: false + gotify: + enabled: false + smtp: + enabled: false + telegram: + enabled: false + pushover: + enabled: false + webhook: + enabled: false +FNEOF + log_success "frigate-notify config.yml written" + + # ── Caddy snippet ────────────────────────────────────────────────────────── + if [ -n "$FRIGATE_PUBLIC_URL" ] && [ "$FRIGATE_PUBLIC_URL" != "https://cam.yourdomain.com" ]; then + local _DOM="${FRIGATE_PUBLIC_URL#https://}" + configure_caddy_for_service "Frigate" "8971" "frigate-audio" || true + fi + + ensure_docker_dir_ownership "$DIR" + + # ── Summary ──────────────────────────────────────────────────────────────── + echo "" + echo "═══════════════════════════════════════════════════════" + echo " Frigate Audio Stack — Setup Complete" + echo "═══════════════════════════════════════════════════════" + echo "" + echo " Directory: $DIR" + echo " Media: $FRIGATE_MEDIA_DIR" + echo " Public URL: $FRIGATE_PUBLIC_URL" + echo " MQTT user: ${MQTT_USER:-frigate}" + echo " ntfy server: $NTFY_SERVER topic: ${NTFY_TOPIC:-frigate}" + echo "" + echo " Before starting:" + echo " 1. Edit frigate_config/config.yml — adjust RTSP paths for your cameras" + echo " (paths vary by vendor; check your camera's manual)" + echo " 2. Edit frigate_config/config.yml — remove/adjust motion masks" + echo " (the masks are blanks — add yours via the Frigate UI after first run)" + echo " 3. Verify .env credentials are correct" + echo "" + echo " Start:" + echo " cd $DIR && docker compose up -d" + echo "" + echo " Face recognition training (after Frigate is running):" + echo " — Go to Frigate UI → Faces → add face photos for household members" + echo " → In frigate-notify/config.yml, add names to alerts.sublabels.block" + echo " to silence push notifications for recognized family members." + echo "" + + local START_NOW="" + prompt_yn "Start the stack now? (y/n) [n]:" "n" START_NOW + if [[ ${START_NOW:-n} =~ ^[Yy]$ ]]; then + log_info "Starting frigate-audio stack..." + if ( cd "$DIR" && docker compose up -d ); then + log_success "Stack started — Frigate UI: http://localhost:8971" + else + log_warning "Start failed — check: cd $DIR && docker compose logs" + fi + else + echo "" + log_info "When ready: cd $DIR && docker compose up -d" + fi + echo "" +} diff --git a/services/sky-cam.sh b/services/sky-cam.sh new file mode 100644 index 0000000..ef34630 --- /dev/null +++ b/services/sky-cam.sh @@ -0,0 +1,184 @@ +#!/bin/bash +# services/sky-cam.sh — Automated sky / timelapse camera scripts. +# Part of the modular post-install system (sourced by setup.sh). +# +# NON-DOCKER module. sky-cam produces: +# • Daily sunrise clip — speed-adjusted video, uploaded to Mattermost +# • Four Seasons timelapse — daily clips sized to Vivaldi movements' music +# • Full-day timelapse — fixed-fps timelapse of every captured image +# • Moon-track timelapse — moon tracked & cropped each visible night +# • Monthly moon-phase close-ups — NASA Dial-a-Moon images posted to MM +# +# Source: https://github.com/outis1one/sky-cam (cloned via bootstrap.sh) +# Installs systemd user timers via sky-cam's install.sh. + +register_service sky-cam cameras "Automated sky / timelapse camera scripts (sky-cam)" + +install_sky-cam() { + local SKYCAM_DIR="$ACTUAL_HOME/sky-cam" + + if [ "$DRY_RUN" = true ]; then + echo "[DRY-RUN] sky-cam would:" + echo " - Install: ffmpeg bc fonts-dejavu curl python3-pip" + echo " - pip install: suntime pytz requests skyfield Pillow numpy scipy" + echo " - Clone sky-cam to $SKYCAM_DIR via bootstrap.sh" + echo " - Edit sky-cam.conf with your location and camera names" + echo " - Copy .env.example → .env and set Mattermost credentials" + echo " - Run ./install.sh to register systemd user timers" + return 0 + fi + + echo "" + echo "╔═══════════════════════════════════════════════════════╗" + echo "║ sky-cam — Automated Sky & Timelapse Camera System ║" + echo "╚═══════════════════════════════════════════════════════╝" + echo "" + + # ── System packages ────────────────────────────────────────────────────── + log_info "Installing system dependencies..." + run_cmd apt-get update -qq + run_cmd apt-get install -y --no-install-recommends \ + ffmpeg bc fonts-dejavu curl python3-pip git + log_success "System packages installed" + + # ── Python packages ────────────────────────────────────────────────────── + log_info "Installing Python packages..." + local PIP="pip3 install --user --quiet" + sudo -u "$ACTUAL_USER" $PIP suntime pytz requests skyfield Pillow numpy scipy \ + || log_warning "Some pip packages may have failed — check output above" + log_success "Python packages installed" + + # ── Clone sky-cam ──────────────────────────────────────────────────────── + if [ -d "$SKYCAM_DIR/.git" ]; then + log_info "sky-cam already cloned at $SKYCAM_DIR — pulling latest..." + sudo -u "$ACTUAL_USER" git -C "$SKYCAM_DIR" pull --ff-only \ + || log_warning "git pull failed — continuing with existing version" + else + log_info "Cloning sky-cam from GitHub..." + sudo -u "$ACTUAL_USER" bash -c " + curl -fsSL https://raw.githubusercontent.com/outis1one/sky-cam/main/bootstrap.sh \ + | bash -s -- '$SKYCAM_DIR' + " || { log_error "Failed to clone sky-cam — check internet connection"; return 1; } + log_success "sky-cam cloned to $SKYCAM_DIR" + fi + + # ── Essential configuration ────────────────────────────────────────────── + echo "" + log_info "Location and camera configuration" + echo " sky-cam needs your GPS coordinates and timezone to calculate" + echo " sunrise times accurately. Use decimal degrees (e.g. 40.7128, -74.0060)." + echo "" + + local LATITUDE="" LONGITUDE="" TIMEZONE="" BASE_DIR="" CAMERAS_LIST="" SUNRISE_CAM="" + prompt_text " Latitude (decimal degrees) [0.0000]:" "0.0000" LATITUDE + prompt_text " Longitude (decimal degrees) [0.0000]:" "0.0000" LONGITUDE + prompt_text " Timezone [America/New_York]:" "America/New_York" TIMEZONE + + echo "" + echo " Camera names are short identifiers, e.g.: east north south west" + echo " These names must match the directories where your camera images are saved." + echo "" + prompt_text " Camera names (space-separated) [east]:" "east" CAMERAS_LIST + prompt_text " Sunrise camera (faces east) [east]:" "east" SUNRISE_CAM + + echo "" + echo " BASE_DIR is where your camera images live." + echo " Each camera should have a sub-folder: BASE_DIR//" + local DEFAULT_BASE="$ACTUAL_HOME/sky-cam/data" + prompt_text " Image base directory [$DEFAULT_BASE]:" "$DEFAULT_BASE" BASE_DIR + [ -z "$BASE_DIR" ] && BASE_DIR="$DEFAULT_BASE" + + # Prompt for Mattermost credentials + echo "" + log_info "Mattermost webhook (for automated uploads)" + echo " sky-cam posts sunrise clips and moon photos to a Mattermost channel." + echo " Create an incoming webhook in Mattermost: Settings → Integrations → Webhooks" + echo " (Leave blank to skip — add to $SKYCAM_DIR/.env later)" + echo "" + local MM_WEBHOOK="" MM_CHANNEL="" + if [ "$UNATTENDED" != true ]; then + read -p " Mattermost webhook URL [Enter to skip]: " MM_WEBHOOK + if [ -n "$MM_WEBHOOK" ]; then + prompt_text " Mattermost channel name [sky-cam]:" "sky-cam" MM_CHANNEL + fi + fi + + # ── Write .env ─────────────────────────────────────────────────────────── + log_info "Writing sky-cam.conf overrides to $SKYCAM_DIR/.env..." + { + echo "# sky-cam site configuration — generated by ubuntu-post-install" + echo "# Edit sky-cam.conf for full settings." + echo "" + echo "LATITUDE=${LATITUDE:-0.0000}" + echo "LONGITUDE=${LONGITUDE:-0.0000}" + echo "TIMEZONE=${TIMEZONE:-America/New_York}" + echo "BASE_DIR=${BASE_DIR}" + if [ -n "$MM_WEBHOOK" ]; then + echo "MM_WEBHOOK_URL=${MM_WEBHOOK}" + echo "MM_CHANNEL=${MM_CHANNEL:-sky-cam}" + else + echo "# MM_WEBHOOK_URL=https://mattermost.yourdomain.com/hooks/your-webhook-id" + echo "# MM_CHANNEL=sky-cam" + fi + } > "$SKYCAM_DIR/.env" + chmod 600 "$SKYCAM_DIR/.env" + + # ── Patch sky-cam.conf with cameras and basic settings ─────────────────── + local CONF="$SKYCAM_DIR/sky-cam.conf" + if [ -f "$CONF" ]; then + log_info "Patching sky-cam.conf with location and camera names..." + # Build CAMERAS=(...) line + local CAM_ARRAY="(${CAMERAS_LIST})" + sed -i "s|^CAMERAS=.*|CAMERAS=${CAM_ARRAY}|" "$CONF" + sed -i "s|^SUNRISE_CAM=.*|SUNRISE_CAM=${SUNRISE_CAM:-east}|" "$CONF" + log_success "sky-cam.conf updated" + else + log_warning "sky-cam.conf not found — check $SKYCAM_DIR" + fi + + # ── Create image directories ───────────────────────────────────────────── + mkdir -p "$BASE_DIR" + for _cam in $CAMERAS_LIST; do + mkdir -p "$BASE_DIR/$_cam" + done + chown -R "$ACTUAL_USER:$ACTUAL_USER" "$SKYCAM_DIR" "$BASE_DIR" 2>/dev/null || true + log_success "Image directories created under $BASE_DIR" + + # ── Install systemd timers ──────────────────────────────────────────────── + if [ -f "$SKYCAM_DIR/install.sh" ]; then + log_info "Installing systemd user timers via install.sh..." + ( cd "$SKYCAM_DIR" && sudo -u "$ACTUAL_USER" bash install.sh ) \ + && log_success "Systemd timers installed" \ + || log_warning "install.sh failed — run manually: cd $SKYCAM_DIR && ./install.sh" + else + log_warning "install.sh not found in $SKYCAM_DIR — run it manually after review" + fi + + # ── Summary ─────────────────────────────────────────────────────────────── + echo "" + echo "═══════════════════════════════════════════════════════" + echo " sky-cam installed" + echo "═══════════════════════════════════════════════════════" + echo "" + echo " Location: $SKYCAM_DIR" + echo " Data: $BASE_DIR" + echo " Cameras: $CAMERAS_LIST" + echo " Timezone: ${TIMEZONE:-America/New_York}" + echo "" + echo " Next steps:" + echo " 1. Review $SKYCAM_DIR/sky-cam.conf" + echo " — SCRIPT_DIR, MUSIC_DIR, schedules, encoding settings" + echo " 2. Put your Vivaldi Four Seasons audio files in:" + echo " $SKYCAM_DIR/music/" + echo " (filenames and expected MUSIC_DIR path are in sky-cam.conf)" + echo " 3. Verify systemd timers:" + echo " systemctl --user list-timers 'sky-cam-*'" + echo " 4. Credentials → $SKYCAM_DIR/.env" + echo "" + echo " To test the sunrise script manually:" + echo " cd $SKYCAM_DIR && ./daily_sunrise_video.sh" + echo "" + echo " Logs:" + echo " journalctl --user -u sky-cam-sunrise.service -f" + echo "" +} diff --git a/setup.sh b/setup.sh index d359670..b57c78e 100755 --- a/setup.sh +++ b/setup.sh @@ -82,6 +82,7 @@ is_installed() { silent-send) [ -d "$ACTUAL_HOME/silent-send/.git" ] ;; linux-to-sync) [ -d "$ACTUAL_HOME/linux-to-sync/.git" ] ;; sync-cc) [ -f "$ACTUAL_HOME/sync-cc/sync_cc.py" ] ;; + sky-cam) [ -d "$ACTUAL_HOME/sky-cam/.git" ] ;; *) [ -e "$DOCKER_DIR/$1" ] ;; esac }