diff --git a/sunset-alarm b/sunset-alarm index 9d0d774..3e56180 100755 --- a/sunset-alarm +++ b/sunset-alarm @@ -1,140 +1,77 @@ #!/usr/bin/env bash -# sunset-alarm — alert at a specified number of minutes before sunset +# sunset-alarm — persistent alert at a calculated offset before sunset # Usage: sunset-alarm (-l LAT:LONG | -t HH24:MI) (-m MINUTES) [-h] -set -euo pipefail +set -uo 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) +SUNSET_EPOCH="" # epoch of today's sunset +ALERT_EPOCH="" # epoch when the window opens (sunset - offset) +OFFSET_MIN=0 # signed minute offset from sunset +LAT="" # latitude (-l/--location) +LON="" # longitude +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 + -l LAT:LONG Latitude and longitude, e.g. 32.123:-111.111 + (queried from Open-Meteo for today's sunset time) + -t HH24:MI Hardcoded sunset time to use instead. + When combined with -l the offset is applied against + this time and the network call is skipped. + -m MINUTES Minutes before (-), at (= 0), or after (+) sunset + to trigger the alert. May be negative. + -h Show this help message and exit. + +Examples: + sunset-alarm -l 32.123:-111.111 -m 90 # 90 min before actual sunset + sunset-alarm -t 18:00 -m -5 # 5 min after the hardcoded time EOF } -die() { echo "ERROR: $*" >&2; exit 1; } +die() { echo "Error: $*" >&2; exit 1; } -warn_reconnect() { echo "Network unavailable, retrying in 5 minutes…" >&2; } +# ── resolve a fallback command at startup ────────────────────────── +NOTIFY_CMD="" +for _cmd in fyi notify-send wall; do + if command -v "$_cmd" >/dev/null 2>&1; then + NOTIFY_CMD="$_cmd"; break + fi +done -# Query Open-Meteo for today's sunset epoch (local timezone). +fire_alert() { + local body="$1" + case "$NOTIFY_CMD" in + fyi) fyi "Sunset Alarm: ${body}" ;; + notify-send) notify-send -u normal "Sunset Alarm" "${body}" 2>/dev/null || true ;; + wall) echo "Sunset Alarm: ${body}" | wall 2>/dev/null || printf 'Sunset Alarm: %s\n' "$body" ;; + esac +} + +# ── open-meteo helper ───────────────────────────────────────────── 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}' \ + # curl –silent + HTTP code tail so we can detect failures. + local resp; resp=$(curl -sfw "\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) + + local code="${resp##*$'\n'}" + resp="${resp%$'\n'*}" # strip the trailing HTTP code + + [[ "${code:-599}" == "200" ]] || return 2 + + local iso; iso=$(printf '%s\n' "$resp" | jq -r '.current.sunset // empty') + [[ -n "$iso" ]] || return 3 # null / missing field + + SUNSET_EPOCH=$(date -d "$iso" +%s) # local-time epoch } -# Build today's sunset epoch from HH:MM when using the hardcoded path (-t). +# Build today's sunset epoch from a hardcoded HH:MM when -t is used. 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 + local hhmm="$1" + local Y M D h m + read -r Y M D h m <<< "$(date +%Y-%m-%d) $(echo "$hhmm" | tr ':' ' ')" + SUNSET_EPOCH=$(date -d "${Y}-${M}-${D} ${h}:${m}:00" 2>/dev/null +%s \ + || die "Invalid time format '${hhmm}')")