Files
sunset-alarm-cli/sunset-alarm
T
2026-10-17 14:27:58 -07:00

141 lines
5.4 KiB
Bash
Executable File
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.
#!/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}&current=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