#!/usr/bin/env bash # sunset-alarm — alert at a specified number of minutes before sunset # Usage: sunset-alarm (-l LAT:LONG | -t HH24:MI) (-m MINUTES) [-h] set -euo pipefail SUNSET="" # epoch seconds for today's sunset ALERT_EPOCH="" # epoch when alert should fire (sunset - offset) OFFSET_MIN=0 # signed minute offset from sun -t/--hardcoded LAT="" LON="" # coordinates (-l/--location) HARDCODED_TIME="" # HH:MM string (-t/--hardcoded) # ── helpers ──────────────────────────────────────────────────────── usage() { cat <<'EOF' Usage: sunset-alarm (-l LAT:LONG | -t HH24:MI) (-m MINUTES) [-h] Options: -l LAT:LONG Latitude and longitude (e.g. 32.123:-111.111) -t HH24:MI Hardcoded sunset time (ignored when combined with -l) -m MINUTES Minutes offset before (+) or after (-) sunset to alert -h Show this help and exit EOF } die() { echo "ERROR: $*" >&2; exit 1; } warn_reconnect() { echo "Network unavailable, retrying in 5 minutes…" >&2; } # Query Open-Meteo for today's sunset epoch (local timezone). fetch_sunset_epoch() { local lat="$1" lon="$2" local json resp_code # curl –silent + show HTTP code (-w) so we can detect server errors. json=$(curl -sf -w '\n%{http_code}' \ "https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}¤t=sunset&timezone=auto" \ --connect-timeout 10 --max-time 30) || return 1 resp_code="${json##*$'\n'}" json="${json%$'\n'*}" [[ "$resp_code" == "200" ]] || return 2 # jq → "YYYY-MM-DDTHH:MM" (ISO-8601 local-time string for the sunset field) local iso_ts iso_ts=$(echo "$json" | jq -r '.current.sunset' | grep -v '^null$') [[ -n "$iso_ts" ]] || return 3 # date -d parses ISO-8601 timestamps directly. SUNSET=$(date -d "$iso_ts" +%s) } # Build today's sunset epoch from HH:MM when using the hardcoded path (-t). set_sunset_from_time() { local hhmm="$1" hh mm read -r hh mm <<< "$(echo "$hhmm" | tr ':' ' ')" # Pad hours / minutes in case user passed "8:30" etc. (they won't via arg # parsing, but defensive never hurts). SUNSET=$(date -d "$(date +%Y-%m-%d) ${hh}:${mm}:00" +%s) } # Fire a desktop notification (best-effort). notify() { local title="$1" body="$2" notify-send -u normal "$title" "$body" 2>/dev/null || true printf '%s — %s\n' "$(date '+%F %T')" "ALERT: $body" } # ── argument parsing ─────────────────────────────────────────────── while [[ $# -gt 0 ]]; do case "$1" in -l) [[ $# -ge 2 ]] || die "-l requires an argument (LAT:LONG)" IFS=':' read -r LAT LON <<< "$2" shift 2 ;; -t) [[ $# -ge 2 ]] || die "-t requires an argument (HH24:MI)" HARDCODED_TIME="$2" shift 2 ;; -m) [[ $# -ge 2 ]] || die "-m requires an argument (MINUTES)" OFFSET_MIN="$2" shift 2 ;; -h|--help) usage; exit 0 ;; *) die "Unknown option: $1" ;; esac done # ── guardrails ───────────────────────────────────────────────────── [[ $# -eq 0 ]] || die "extra arguments provided — see --help" (( OFFSET_MIN >= -60 )) || [[ $OFFSET_MIN -le 0 ]] 2>/dev/null \ 1>/dev/null || die "-m must be ≥ -60 (or any value ≤ 0)" # Need either -l or -t. if [[ -z "$LAT" && -z "$HARDCODED_TIME" ]]; then die "Need at least one of: -l LAT:LONG, -t HH24:MI" fi # ── Phase 1 — resolve today's sunset (epoch) ─────────────────────── if [[ -n "$LAT" && -n "$LON" ]]; then while true; do fetch_sunset_epoch "$LAT" "$LON" && break delay=$(( 5 * 60 )) warn_reconnect; sleep "$delay" || warn_reconnect done else set_sunset_from_time "$HARDCODED_TIME" fi # ── Phase 2 — compute alert time and loop ────────────────────────── ALERT_EPOCH=$(( SUNSET - OFFSET_MIN * 60 )) now_epoch=$(date +%s) if (( now_epoch >= ALERT_EPOCH )); then # Already past the window — fire once, exit. notify "Sunset Alarm" \ "Triggered (offset: ${OFFSET_MIN:++}$(printf '%d' "$OFFSET_MIN")). Sunset at $(date -d @"$SUNSET" '+%H:%M')." exit 0 fi while true; do now_epoch=$(date +%s) if (( ALERT_EPOCH <= now_epoch && now_epoch < SUNSET )); then notify "Sunset Alarm" \ "Triggered (offset: ${OFFSET_MIN:++}$(printf '%d' "$OFFSET_MIN")). Sunset at $(date -d @"$SUNSET" '+%H:%M')." elif (( now_epoch > SUNSET )); then # Day is over — re-fetch for tomorrow (if we have -l). if [[ -n "$LAT" && -n "$LON" ]]; then warn_reconnect=true # just print a quick line, don't sleep while true; do fetch_sunset_epoch "$LAT" "$LON" && break delay=$(( 5 * 60 )) warn_reconnect; sleep "$delay" || warn_reconnect done else set_sunset_from_time "$HARDCODED_TIME" fi ALERT_EPOCH=$(( SUNSET - OFFSET_MIN * 60 )) continue fi sleep 60 done