# ============================================================================= # 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-.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 — daily at SCHEDULE_SEASONS_ (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 # # moon-track.sh — nightly at SCHEDULE_MOON_TRACK (SUNRISE_CAM only) # detects the moon in each east night frame, crops # a tracked sequence, stitches into an mp4 # # moon-phase-monthly.sh — daily at SCHEDULE_MOON_PHASE (SUNRISE_CAM only) # no-ops except on phase post-days, when it # confirms east saw the moon during the # collection window, fetches NASA SVS Dial-a-Moon # for that exact UTC hour, renders the moon # full-screen on a black background, and posts to # Mattermost. Handles full moon, first quarter # (waxing half), and third quarter (waning half). # # # 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-.timer (SCHEDULE_SEASONS_) # → 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 # # moon-track.sh # ← called by systemd sky-cam-moon-track.timer (SCHEDULE_MOON_TRACK) # → if renamed: update install.sh (the moon-track write_service block) # calls → moon_detect.py (Python lib via heredoc), ffmpeg # # moon-phase-monthly.sh # ← called by systemd sky-cam-moon-phase.timer (SCHEDULE_MOON_PHASE) # → if renamed: update install.sh (the moon-phase write_service block) # calls → moon_phase_monthly.py # # moon_phase_monthly.py # ← called by moon-phase-monthly.sh # → if renamed: update moon-phase-monthly.sh (the exec line at the bottom) # calls → moon_phase.py, moon_detect.py, moon_composite.py, notify.sh # # moon_phase.py / moon_detect.py / moon_composite.py # ← Python libraries used by moon-track.sh and moon_phase_monthly.py # → if renamed: update moon-track.sh, moon_phase_monthly.py, README # # ============================================================================= # ── 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//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//YYYY-MM-DD/HH-MM-SS.m4a (ambient) # AUDIO_DIR/sunrise//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_ (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 # Moon jobs (see "Moon jobs" section further down) SCHEDULE_MOON_TRACK=02:30:00 # nightly tracker — runs after midnight, before seasons SCHEDULE_MOON_PHASE=09:30:00 # daily check; no-ops except on phase post-days # 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 18–30. # 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 (0–255) < SEASONS_GREY_STDDEV_MIN (frame is uniform) # 2. mean brightness (0–255) > 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__. # # 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_=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_ 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//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; 64–96k 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 # ── Moon jobs (nightly tracker + monthly phase close-ups) ──────────────────── # All moon jobs operate on the SUNRISE_CAM (the east-facing camera that already # has a clear view of the eastern sky). They share three Python helpers # (moon_phase.py, moon_detect.py, moon_composite.py) and one cached lunar # reference texture downloaded by install.sh. # # moon-track.sh nightly batch — detects the moon in each frame, # crops a 480×480 box around it, stitches the # tracked sequence into an mp4. Output: # $MOVIES_DIR//moon-track/YYYY/YYYY-MM-DD-moon-track.mp4 # # moon-phase-monthly.sh daily check; runs whichever phase composite is # due today. Three phases handled: # 🌕 Full Moon posts D + MOON_FULL_POST_DELAY_DAYS # 🌓 First Quarter posts D + MOON_QUARTER_POST_DELAY_DAYS # 🌗 Third Quarter posts D + MOON_QUARTER_POST_DELAY_DAYS # East acts as the WITNESS — it confirms the moon # was actually visible in your sky during the # collection window and supplies the timestamp. # The image itself is a NASA SVS Dial-a-Moon # render for that exact UTC hour, sized to fill # the frame on a black background (like the look # of a long-telephoto shot). Output: # $MOVIES_DIR//full-moons/YYYY-MM-full.jpg # $MOVIES_DIR//first-quarter/YYYY-MM-first-quarter.jpg # $MOVIES_DIR//third-quarter/YYYY-MM-third-quarter.jpg # # Honest-by-design: a 38-px white blob from a wide-field IP camera cannot be # enhanced into crater detail. The image you see in Mattermost is a NASA # render for the exact moment east captured the moon — the strongest possible # match for what your sky actually looked like, with full real-physics # crater shadows, libration, and phase. East's role is verifying that you # had a clear view of the moon that night. # # Geometry caveat: east only sees the eastern sky. At first quarter the moon # is up only from noon to midnight, transiting south at sunset, so east only # catches it during DAYTIME. The brightness-based detector often fails on # daytime moon shots — first quarter is best-effort. Full moon and third # quarter both rise after dark and stay in east's view; those should land # cleanly most months. # # Toggles (any can be disabled independently): MOON_TRACK_ENABLED=true MOON_FULL_ENABLED=true MOON_FIRST_QUARTER_ENABLED=true # set false if daytime-detection misses are noisy MOON_THIRD_QUARTER_ENABLED=true # Set false to skip east verification entirely and post the NASA render for # the exact phase moment regardless of clouds / camera outage. Default true # means "only post when east actually saw the moon that month." MOON_REQUIRE_EAST_VERIFY=true # Nightly tracker tuning ────────────────────────────────────────────────────── MOON_TRACK_CROP_PX=480 # pixels — box size around the moon (source coords) MOON_TRACK_FPS=12 # output mp4 framerate MOON_TRACK_CRF=24 # output mp4 CRF MOON_TRACK_RETENTION_DAYS=90 # delete tracker mp4s older than this; 0 = forever # Observation time ──────────────────────────────────────────────────────────── # Local time that defines the observation window: MOON_OBS_TIME_LOCAL to # MOON_OBS_TIME_LOCAL+1h. East frames in this window are checked for a clear # moon detection (quality >= MOON_MIN_QUALITY). If any frame qualifies, the # NASA Dial-a-Moon image for MOON_OBS_TIME_LOCAL UTC is fetched and posted. # If no frame shows a clear moon, the month is skipped entirely. # # 22:00 aligns directly with NASA's hourly renders. Full moon and third # quarter both rise after dark and are visible at this hour. First quarter # is up only until ~midnight from a new-moon start, so it may not be visible # at 22:00 depending on the exact date — set MOON_FIRST_QUARTER_ENABLED=false # if first-quarter posts are consistently missed. MOON_OBS_TIME_LOCAL=22:00 # Post delay ────────────────────────────────────────────────────────────────── # How many days after the exact phase to run the post. The post delay gives # time for the obs-night frames to land on disk before the job runs. MOON_FULL_POST_DELAY_DAYS=1 # post the morning after the full moon MOON_QUARTER_POST_DELAY_DAYS=1 # post the morning after each quarter MOON_QUARTER_MIN_ILLUMINATION=0.46 MOON_QUARTER_MAX_ILLUMINATION=0.54 # Frame-acceptance thresholds — a candidate must beat all three to qualify. # Lower = more permissive (accept hazier nights / lower moon). If you find # the script never finds a clear shot, loosen these. MOON_MIN_QUALITY=0.55 # 0..1 from moon_detect (roundness × halo × isolation) MOON_MIN_ALTITUDE_DEG=15 # below this the moon is in trees / on the horizon MOON_FULL_MIN_ILLUMINATION=0.96 # ±4% of 100%: accept 96–100% illumination for full moon # Output frame. Default 1920×1080 to match the sunrise videos. MOON_OUTPUT_W=1920 MOON_OUTPUT_H=1080 MOON_HEIGHT_PCT=0.92 # moon disk fills this fraction of frame height (full-screen feel) # NASA SVS Dial-a-Moon ──────────────────────────────────────────────────────── # Hourly pre-rendered moon images, free, public domain, real physics # (correct phase, libration, crater shadows for any UTC hour). # https://svs.gsfc.nasa.gov/api/dialamoon/ # We hit it once per phase event (~36 calls/year), cache the result forever. #MOON_DIALAMOON_API=https://svs.gsfc.nasa.gov/api/dialamoon #MOON_DIALAMOON_CACHE_DIR="$SCRIPT_DIR/moon-ref/dialamoon" MOON_DIALAMOON_TARGET_PX=2048 # cached PNG longest side; downsampled on save MOON_DIALAMOON_TIMEOUT_SEC=30 # ── 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