Files
sky-cam/sky-cam.conf
T
Claude 5810ab0194 Trim weather-tag vocabulary to audio-relevant precipitation only
Drops clear/cloudy/fog/windy and the wind-speed override — those
conditions sound the same on the recordings, so they don't earn a tag.
Also renames "rainshower" → "rainstorm" and rebalances the rain
mapping so 503/504/522 (extreme + heavy showers) get the storm tag.

Final vocabulary: thunderstorm, rain, rainstorm, snow, snowstorm.
Anything else falls through to the un-tagged filename.
2026-04-29 18:55:59 +00:00

442 lines
24 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# =============================================================================
# sky-cam.conf — master configuration for all sky-cam scripts
# =============================================================================
#
# This is the ONLY file you need to edit. After editing, run:
#
# ./install.sh # installs systemd timers (no root needed)
# ./install.sh --system # system-wide install (requires sudo)
#
# Re-run install.sh any time this file changes.
#
# -----------------------------------------------------------------------------
# MANUAL SYSTEMD SETUP (if you prefer not to use install.sh)
# -----------------------------------------------------------------------------
# After editing this file, generate the unit files:
#
# mkdir -p ~/.config/systemd/user
# cp systemd/*.service systemd/*.timer ~/.config/systemd/user/
#
# Then edit ExecStart= in each .service file to match your SCRIPT_DIR, then:
#
# systemctl --user daemon-reload
# systemctl --user enable --now sky-cam-sunrise.timer
# # one sky-cam-seasons-<cam>.timer per camera, e.g.:
# systemctl --user enable --now sky-cam-seasons-east.timer
#
# Check status:
# systemctl --user list-timers 'sky-cam-*'
# journalctl --user -u sky-cam-seasons-sunrise.service -f
#
# System-wide (replace --user with no flag, prepend sudo):
# sudo cp systemd/*.service systemd/*.timer /etc/systemd/system/
# sudo systemctl daemon-reload && sudo systemctl enable --now sky-cam-*.timer
#
# -----------------------------------------------------------------------------
# SCRIPT MAP — what each file does and when it runs
# -----------------------------------------------------------------------------
#
# You run / schedule:
# daily_sunrise_video.sh — daily at SCHEDULE_SUNRISE (via systemd, SUNRISE_CAM only)
# grabs images around sunrise, makes a short video,
# uploads it to Mattermost
#
# 4-seasons.sh <cam> — daily at SCHEDULE_SEASONS_<cam> (via systemd, per camera)
# makes yesterday's daily clip sized to its share
# of the matching Vivaldi movement's music duration;
# on the last day of a movement, auto-triggers
# montage-mvt.sh
#
#
# install.sh — run once at setup, and again if this file changes
# generates and installs systemd units from conf
#
# stitch-cameras.py — run manually when needed
# stitches north + sunrise into a panorama
# usage: python3 stitch-cameras.py north.jpg sunrise.jpg prefix [focal]
#
# Auto-triggered (do not run directly):
# montage-mvt.sh — called by 4-seasons.sh on last day of each movement
# concatenates all daily clips for the movement,
# speed-adjusts to match music, overlays attribution,
# sends completion notification; on last Autumn
# movement also triggers year-end-join.sh
#
# year-end-join.sh — called by montage-mvt.sh at end of Autumn Mvt 3
# concatenates all 12 movement montages into one
# Four Seasons year video, sends notification
#
# Helpers (called by the scripts above, not run directly):
# notify.sh — sends notifications via ntfy / email / Mattermost
# season_info.py — outputs astronomical season/movement/year for a date
# sunrise.py — outputs today's sunrise time (used by daily_sunrise_video)
# sunrise2mm.py — uploads the sunrise video to Mattermost
# sunrise_overlay.py — burns a timestamp overlay onto the sunrise video
#
# -----------------------------------------------------------------------------
# FILE DEPENDENCIES — who calls whom, and what to update if you rename a file
# -----------------------------------------------------------------------------
#
# sky-cam.conf (this file)
# ← sourced by every .sh script; read by sunrise.py, sunrise2mm.py
# → if renamed: update the 'source' line at the top of ALL scripts
# (don't rename it — just edit it in place)
#
# install.sh
# ← run manually; reads this conf
# → if renamed: update the note at the top of this conf file only
# generates → systemd unit files (sky-cam-*.service / .timer)
# and sky-cam-notify-failure@.service
#
# daily_sunrise_video.sh
# ← called by systemd sky-cam-sunrise.timer (SCHEDULE_SUNRISE)
# → if renamed: update install.sh (write_service call ~line 70)
# then re-run install.sh to regenerate the unit
# calls → sunrise.py, sunrise_overlay.py, sunrise2mm.py
#
# 4-seasons.sh
# ← called by systemd sky-cam-seasons-<cam>.timer (SCHEDULE_SEASONS_<cam>)
# → if renamed: update install.sh (write_service call ~line 85)
# then re-run install.sh
# calls → season_info.py, montage-mvt.sh
#
# montage-mvt.sh
# ← called by 4-seasons.sh on the last day of each movement
# → if renamed: update 4-seasons.sh (the exec line near the bottom)
# calls → season_info.py, notify.sh, year-end-join.sh
#
# year-end-join.sh
# ← called by montage-mvt.sh at end of Autumn Mvt 3
# → if renamed: update montage-mvt.sh (the exec line at the bottom)
# calls → notify.sh
#
# notify.sh
# ← called by montage-mvt.sh, year-end-join.sh
# also referenced in install.sh for the OnFailure= failure template
# → if renamed: update montage-mvt.sh, year-end-join.sh, install.sh
#
# season_info.py
# ← called by 4-seasons.sh and montage-mvt.sh
# → if renamed: update 4-seasons.sh and montage-mvt.sh (the eval lines)
#
# sunrise.py
# ← called by daily_sunrise_video.sh
# → if renamed: update daily_sunrise_video.sh
#
# sunrise_overlay.py
# ← called by daily_sunrise_video.sh
# → if renamed: update daily_sunrise_video.sh
#
# sunrise2mm.py
# ← called by daily_sunrise_video.sh
# → if renamed: update daily_sunrise_video.sh
#
# stitch-cameras.py
# ← run manually only; no script calls it
# → safe to rename with no other changes needed
#
# =============================================================================
# ── Install location ──────────────────────────────────────────────────────────
# Auto-detected from where this file lives — override only if you move the
# scripts to a different location after install.
SCRIPT_DIR="${SCRIPT_DIR:-$(dirname "$(realpath "${BASH_SOURCE[0]}")")}"
# ── Storage ───────────────────────────────────────────────────────────────────
# BASE_DIR: root folder where camera images are stored.
# Structure created automatically: BASE_DIR/<cam>/YYYY-MM-DD/HH-MM-SS.jpg
# Defaults to a 'data' subfolder next to the scripts; override to point at
# an external drive or network mount (recommended for production).
BASE_DIR="${BASE_DIR:-$SCRIPT_DIR/data}"
MOVIES_DIR="${MOVIES_DIR:-$BASE_DIR/movies}" # override if videos live on a different drive
MUSIC_DIR="${MUSIC_DIR:-$SCRIPT_DIR/music}"
AUDIO_DIR="${AUDIO_DIR:-$BASE_DIR/audio}" # root for all audio recordings; override in .env
# Structure: AUDIO_DIR/<cam>/YYYY-MM-DD/HH-MM-SS.m4a (ambient)
# AUDIO_DIR/sunrise/<cam>/YYYY-MM-DD/sunrise-audio.m4a (sunrise clips)
# ── Location & timezone ───────────────────────────────────────────────────────
# Used by sunrise.py to calculate sunrise time each day.
#
# To find your coordinates:
# Google Maps — right-click your location → the first item shown is "lat, lon"
# Or: maps.google.com, drop a pin, coordinates appear in the URL and sidebar
#
# ── Location & timezone ───────────────────────────────────────────────────────
# Set your real values in .env — do not put exact coordinates here since
# this file is committed to git. Add these three lines to your .env:
#
# LATITUDE=43.1234
# LONGITUDE=-76.5678
# TIMEZONE=America/Chicago
#
# Placeholder values below are used only if .env has not set them.
LATITUDE="${LATITUDE:-0.0000}"
LONGITUDE="${LONGITUDE:-0.0000}"
TIMEZONE="${TIMEZONE:-America/New_York}"
# ── Weather tagging (OpenWeather) ─────────────────────────────────────────────
# When enabled, ambient-record.sh queries OpenWeather's "Current weather"
# endpoint just before each 30-minute audio chunk and appends a one-word
# weather tag to the filename, e.g.:
# east-08-30-00-thunderstorm.m4a
# east-09-00-00-rain.m4a
# east-09-30-00-cloudy.m4a
# That way each chunk is labelled with what was happening DURING that chunk —
# a foggy dawn won't mislabel the sunny afternoon files.
#
# Tag vocabulary (fixed set — only conditions that change the audio):
# thunderstorm rain rainstorm snow snowstorm
# Clear / cloudy / fog / wind sound the same on the recordings, so those
# conditions fall through to the un-tagged filename.
#
# Setup:
# 1. Get a free API key: https://openweathermap.org/api
# (the free "Current weather" tier — 60 calls/min — is plenty)
# 2. Add to .env: OPENWEATHER_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxx
# 3. Confirm LATITUDE / LONGITUDE are set in .env (already needed by sunrise.py)
# 4. Flip WEATHER_ENABLED=true below
#
# The query uses the same LATITUDE / LONGITUDE configured above — one location
# for the whole installation; per-camera coordinates are not supported.
#
# Failure-safe: if the toggle is off, the key is missing, the network is down,
# or the API errors out, weather-tag.sh prints nothing and the recorder falls
# back to the un-tagged filename. ambient-record.sh never blocks on this.
#
# Manual test: ./weather-tag.sh (prints e.g. "cloudy" or nothing)
WEATHER_ENABLED=false
OPENWEATHER_API_KEY="${OPENWEATHER_API_KEY:-}"
# ── Cameras ───────────────────────────────────────────────────────────────────
# List every camera name here (space-separated inside the parentheses).
# Each name becomes a subfolder under BASE_DIR (images) and BASE_DIR/movies.
# To add a camera: add its name here, add its schedules below, re-run install.sh.
#
CAMERAS=(east north south) # add west here + uncomment its schedule below when ready
SUNRISE_CAM=east # which camera faces east (gets the sunrise video job)
# ── Schedules ─────────────────────────────────────────────────────────────────
# All times are HH:MM:SS (24-hour local time).
# install.sh reads these and generates one systemd timer per camera per job.
#
# SCHEDULE_SUNRISE
# When to START the sunrise video job. The script wakes up, calculates
# today's sunrise time, then WAITS internally until the capture window is
# fully finished before touching any images. Set this early enough to
# cover your earliest possible sunrise minus SUNRISE_PRE_MIN — 03:00 works
# year-round for most locations. The video is ready a few minutes after the
# capture window closes, not at this start time.
#
# SCHEDULE_SEASONS_<cam> (one per camera in CAMERAS)
# When to run the Four Seasons daily clip job. Processes YESTERDAY'S images
# (not today's) — always runs after midnight. On the last day of a season
# movement this automatically triggers the movement montage build.
# Space multiple cameras at least 30 min apart to avoid disk contention.
#
SCHEDULE_SUNRISE=03:00:00
# Four Seasons daily clip — one per camera, staggered 30 min apart.
# Processes yesterday's images; on the last day of a movement auto-triggers
# montage-mvt.sh. Space cameras at least 30 min apart to avoid disk contention.
SCHEDULE_SEASONS_east=01:00:00
SCHEDULE_SEASONS_north=01:30:00
SCHEDULE_SEASONS_south=02:00:00
# To add west: uncomment below, add west to CAMERAS above, add CAM_RTSP_west to .env
#SCHEDULE_SEASONS_west=02:30:00
# ── Sunrise video tuning ──────────────────────────────────────────────────────
# Capture window around sunrise:
# Images from (sunrise - SUNRISE_PRE_MIN) to (sunrise + SUNRISE_POST_MIN)
# are included in the clip. Larger windows give more context; they are all
# speed-adjusted to fit SUNRISE_TARGET_SECS regardless of window size.
SUNRISE_PRE_MIN=70 # minutes before sunrise to start capturing
SUNRISE_POST_MIN=10 # minutes after sunrise to stop capturing
# Target length of the final sunrise video (seconds).
SUNRISE_TARGET_SECS=10
# Burn the local sunrise time vertically on the right side of the video.
# Set false to skip the overlay entirely (cleaner look, faster encoding).
SUNRISE_OVERLAY_ENABLED=true
# Opacity of the sunrise-time text (0.0 = invisible, 1.0 = fully opaque).
SUNRISE_OVERLAY_OPACITY=0.45
# Delete daily sunrise videos older than this many days (rolling window).
# Set 0 to keep forever.
SUNRISE_RETENTION_DAYS=10
# Delete unpinned sunrise posts from Mattermost older than this many days.
# Pinned posts are always kept regardless. Set 0 to disable Mattermost cleanup.
MM_SUNRISE_RETENTION_DAYS=8
# Output directory for --test runs. Kept separate from production sunrise videos.
# Test files older than DEMO_RETENTION_DAYS are deleted automatically.
DEMO_DIR="${DEMO_DIR:-$MOVIES_DIR/demoio}"
DEMO_RETENTION_DAYS=8
# ── Video encoding quality ────────────────────────────────────────────────────
# FFmpeg CRF: lower = higher quality / larger files. Typical range 1830.
# 18 ≈ visually lossless 23 = ffmpeg default 28 = smaller/lower
# H.265 (libx265) CRF is not directly comparable — H.265 CRF 22 ≈ H.264 CRF 26
# in visual quality but ~40-50% smaller file. Final montages use H.265;
# intermediates (deleted after use) stay H.264.
#
# ENCODE_PRESET: slow = better compression, same CRF quality, ~2× encode time.
# Use 'medium' on slower machines, 'slow' or 'slower' on fast ones.
# All encodes run at night so 'slow' is the right default.
#
CRF_SUNRISE=28 # daily_sunrise_video.sh — daily Mattermost upload (H.264)
CRF_SEASONS_RAW=28 # 4-seasons.sh — raw concat before speed-adjust (H.264)
CRF_SEASONS_FINAL=26 # 4-seasons.sh — final daily clip (goes into montage) (H.264)
CRF_MONTAGE=28 # montage-mvt.sh — intermediate concat + sped steps (H.264)
CRF_MONTAGE_FINAL=22 # montage-mvt.sh — final archive montage (H.265)
ENCODE_PRESET=slow # ffmpeg preset for all encodes (slow/medium/fast)
AUDIO_BITRATE=192k # montage-mvt.sh — music track on movement montages
# ── Frame validation (4-seasons.sh) ──────────────────────────────────────────
# Empty (0-byte) frames are always skipped. No config needed for that.
#
# SEASONS_GREY_STDDEV_MIN / SEASONS_GREY_DARK_FLOOR — optional bad-signal filter.
# Catches uniform-colour frames caused by a bad RTSP signal: solid green, grey, etc.
# These are valid JPEGs but contain no real image content.
# Uncomment both lines to enable. A frame is dropped only when BOTH are true:
# 1. pixel standard deviation (0255) < SEASONS_GREY_STDDEV_MIN (frame is uniform)
# 2. mean brightness (0255) > SEASONS_GREY_DARK_FLOOR (frame is not dark)
# Night frames are dark so they always pass even if stddev is low.
# Requires ImageMagick (convert). Start with these values and tune from there:
SEASONS_GREY_STDDEV_MIN=5 # skip if stdev < 5 (very uniform colour)
SEASONS_GREY_DARK_FLOOR=30 # …but only if mean brightness > 30 (not a night frame)
#
# Performance: ImageMagick is only called on files below SEASONS_LARGE_FRAME_BYTES.
# Files above that threshold are always good and skip the check entirely.
# Default 1 MB — adjust if your camera produces larger bad-signal frames.
#SEASONS_LARGE_FRAME_BYTES=1048576
# ── Montage attribution overlay ───────────────────────────────────────────────
# A translucent bar across the top of each movement montage, naming the
# music source ("The Four Seasons — Antonio Vivaldi ...").
# Appears for MONTAGE_ATTR_DUR seconds from the start, with ATTR_FADE-second
# fade in and fade out. FADE_DUR controls the video/audio fade in/out at
# the very start and very end of the montage.
#
MONTAGE_FADE_DUR=2.0 # video + audio fade in/out (seconds)
MONTAGE_ATTR_DUR=6 # attribution overlay total duration (seconds)
MONTAGE_ATTR_FADE=1 # attribution fade-in and fade-out length (seconds)
# ── Music loop multiplier ─────────────────────────────────────────────────────
# Short movements (especially Mvt 2 adagios) give each day only 3-5 seconds of
# screen time. Looping the music N times multiplies the per-day target so days
# are less compressed and more watchable. Per-movement overrides take the form
# MONTAGE_MUSIC_LOOPS_<Season>_<MvtNum>.
#
# Recommended starting point based on movement durations:
# Summer Mvt 2 (107s) → 3× Winter Mvt 2 (132s) → 3×
# Autumn Mvt 2 (152s) → 2× Summer Mvt 3 (159s) → 2×
# Spring Mvt 2 (171s) → 2× others 1× (already ≥10 s/day)
#
MONTAGE_MUSIC_LOOPS=1 # default for all movements
MONTAGE_MUSIC_LOOPS_Summer_2=3
MONTAGE_MUSIC_LOOPS_Winter_2=3
MONTAGE_MUSIC_LOOPS_Autumn_2=2
MONTAGE_MUSIC_LOOPS_Summer_3=2
MONTAGE_MUSIC_LOOPS_Spring_2=2
# ── Direct RTSP capture (replaces MotionEye / any NVR) ────────────────────────
# capture.sh pulls frames directly from each camera's RTSP stream.
# No NVR software required — just ffmpeg and the camera's stream URL.
#
# Per-camera RTSP URL: CAM_RTSP_<cam>=rtsp://user:pass@host:port/path
# Find the URL in your camera's web UI (usually under Network → Video → RTSP)
# or check your camera's manual.
#
# CAM_RTSP_<cam> goes in .env (see bottom of this file), not here.
# Seconds between captured frames (applies to all cameras).
# Lower = smoother timelapse + larger storage footprint:
# 10 s → 8,640 frames/day ~2.5 GB/day (default, good balance)
# 5 s → 17,280 frames/day ~5 GB/day (smoother motion)
# 2 s → 43,200 frames/day ~12 GB/day (very smooth, high storage)
# CAPTURE_STALE_SECS should always be at least 2× CAPTURE_INTERVAL.
CAPTURE_INTERVAL=10
# Per-camera overrides — uncomment and set to override the global value above.
# Useful when cameras serve different purposes (e.g. south records ambient audio
# so fewer frames is fine; north faces a busy scene and benefits from 5s).
#CAPTURE_INTERVAL_east=10
#CAPTURE_INTERVAL_north=10
#CAPTURE_INTERVAL_south=30
# Seconds without a new frame before the watchdog sends a stall notification.
# A recovery notification fires automatically when capture resumes.
# Must always be at least 2× the effective CAPTURE_INTERVAL for that camera.
CAPTURE_STALE_SECS=500
# ── Sunrise audio capture ──────────────────────────────────────────────────────
# Records the camera's RTSP audio stream during the sunrise window so that
# daily_sunrise_video.sh can mix in natural ambient sound (birds, rain, wind)
# at real speed, while the video plays as the sped-up timelapse.
# The audio file is deleted automatically after it is mixed into the video.
#
AUDIO_ENABLED=true # set true if your camera has a working mic
CAPTURE_AUDIO_BITRATE=96k # bitrate for the sunrise audio recording
# Audio is recorded for exactly SUNRISE_TARGET_SECS, centred on actual sunrise:
# floor(TARGET/2) seconds before, ceil(TARGET/2) after (odd second → post-sunrise).
# Clips are saved to AUDIO_DIR/sunrise/<cam>/YYYY-MM-DD/sunrise-audio.m4a.
# After mixing into the video the clip is kept for SUNRISE_AUDIO_RETENTION_DAYS, then deleted.
SUNRISE_AUDIO_RETENTION_DAYS=7
# ── Ambient audio library ──────────────────────────────────────────────────────
# Continuously records natural sounds (birds, rain, wind) from AMBIENT_CAMS to
# build a reusable audio library. Runs as a permanent systemd service alongside
# capture.sh. Camera audio is 8 kHz mono (phone quality) — enough for nature
# sounds and white-noise style playback.
#
AMBIENT_ENABLED=false
AMBIENT_CAMS=(south) # cameras to record from
AMBIENT_CHUNK_SECS=1800 # seconds per file (default 30 min)
AMBIENT_BITRATE=96k # AAC bitrate; 6496k is plenty at 8 kHz
# Retention — global default, then optional per-camera overrides.
# Uncomment and set per-cam lines to keep different cameras for different durations.
AMBIENT_RETENTION_DAYS=30 # global default; 0 = keep forever
#AMBIENT_RETENTION_DAYS_east=14
#AMBIENT_RETENTION_DAYS_north=30
#AMBIENT_RETENTION_DAYS_south=60 # keep south longer for nature sound library
# ── Mattermost — daily sunrise upload ─────────────────────────────────────────
# mattermost_url, access_token, channel_id go in .env (see bottom of this file).
# ── Notifications — montage / year-end complete + any job failure ─────────────
# Notifications fire when a movement montage or the year video finishes,
# and via systemd OnFailure= if any daily job fails.
# Uncomment and configure one or more methods to enable.
# ntfy — push notifications, zero signup for self-hosted or free cloud tier:
# Self-hosted: https://docs.ntfy.sh/install/
# Cloud: pick any topic name at ntfy.sh (no account needed)
# Set NTFY_URL=https://ntfy.sh/your-topic in .env, then flip the toggle below.
NTFY_ENABLED=true
NTFY_URL="${NTFY_URL:-}"
# Email — requires the 'mail' command (package: mailutils or s-nail).
# Configure outbound SMTP via /etc/ssmtp/ssmtp.conf or msmtp.
# Set EMAIL_TO and EMAIL_FROM in .env, then flip the toggle below.
EMAIL_ENABLED=false
EMAIL_TO="${EMAIL_TO:-}"
EMAIL_FROM="${EMAIL_FROM:-skycam@localhost}"
# Mattermost text post — reuses mattermost_url and access_token above.
# Can be the same daily channel or a separate admin/private channel.
# Set MM_NOTIFY_CHANNEL_ID in .env, then flip the toggle below.
MM_NOTIFY_ENABLED=false
MM_NOTIFY_CHANNEL_ID="${MM_NOTIFY_CHANNEL_ID:-}"
# ── Credentials (.env override) ───────────────────────────────────────────────
# Sensitive values (RTSP URLs with passwords, API tokens) can be kept out of
# this file — and out of git — by placing them in a .env file in the same
# directory. install.sh generates .env.example showing exactly which variables
# to define for your camera list.
#
# To use:
# cp .env.example .env
# $EDITOR .env ← fill in real values
# (never commit .env)
#
_conf_dir="$(dirname "$(realpath "${BASH_SOURCE[0]}")")"
[ -f "$_conf_dir/.env" ] && source "$_conf_dir/.env"
unset _conf_dir