From 68ae3e1b9a0eec9d49a6e129c56933921d4deea7 Mon Sep 17 00:00:00 2001 From: Hyperling Date: Sat, 17 Oct 2026 14:34:43 -0700 Subject: [PATCH] Add each iteration so far, for funzies. --- sunset_alarm_v1.py | 183 ++++++++++++++++++++++++++++++++++ sunset_alarm_v2.sh | 0 sunset_alarm_v3-UNFINISHED.sh | 77 ++++++++++++++ 3 files changed, 260 insertions(+) create mode 100644 sunset_alarm_v1.py create mode 100644 sunset_alarm_v2.sh create mode 100644 sunset_alarm_v3-UNFINISHED.sh diff --git a/sunset_alarm_v1.py b/sunset_alarm_v1.py new file mode 100644 index 0000000..56046f7 --- /dev/null +++ b/sunset_alarm_v1.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +"""Sunset Alarm — alert at a specified offset before sunset. + +Usage: + sunset-alarm (-l LAT:LONG | -t HH24:MI) (-m MINUTES) [-h] +""" + +import argparse +import json +import subprocess +import sys +import time +from datetime import datetime, timedelta +from urllib.error import URLError +from urllib.request import urlopen + + +def parse_coordinate(value): + """Parse 'lat:long' string into two floats.""" + parts = value.split(":") + if len(parts) != 2: + raise argparse.ArgumentTypeError( + f"invalid coordinate format '{value}': expected LAT:LONG" + ) + try: + lat, lon = float(parts[0]), float(parts[1]) + except ValueError: + raise argparse.ArgumentTypeError( + f"invalid coordinate values in '{value}': must be numbers" + ) + if not -90 <= lat <= 90: + raise argparse.ArgumentTypeError(f"latitude {lat} out of range [-90, 90]") + if not -180 <= lon <= 180: + raise argparse.ArgumentTypeError(f"longitude {lon} out of range [-180, 180]") + return lat, lon + + +def parse_hardcoded_time(value): + """Parse 'HH:MM' string into a time object and validate.""" + try: + t = datetime.strptime(value, "%H:%M").time() + except ValueError: + raise argparse.ArgumentTypeError( + f"invalid time format '{value}': expected HH24:MI" + ) + return t + + +def fetch_sunset(lat, lon): + """Query Open-Meteo for today's sunrise/sunset and return a datetime. + + Uses the `current日出_sunset` parameter set which returns values in UTC. + """ + url = ( + f"https://api.open-meteo.com/v1/forecast?" + f"latitude={lat}&longitude={lon}" + f"¤t=sunrise,sunset" + f"&timezone=auto" + ) + with urlopen(url, timeout=30) as resp: + data = json.loads(resp.read().decode("utf-8")) + + sunrise_utc_str = data["current"]["sunrise"] # e.g. '2026-10-17T04:23' + sunset_utc_str = data["current"]["sunset"] # e.g. '2026-10-17T18:56' + + sunset_dt = datetime.strptime(sunset_utc_str, "%Y-%m-%dT%H:%M") + return sunset_dt + + +def notify_send(title, message): + """Send a desktop notification via notify-send.""" + try: + subprocess.run( + ["notify-send", title, message], + check=False, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + except FileNotFoundError: + pass # notify-send not available; fall through to stdout + + +def main(): + parser = argparse.ArgumentParser( + description="Alert at a specified number of minutes before sunset.", + ) + location_group = parser.add_mutually_exclusive_group(required=True) + location_group.add_argument( + "-l", + type=parse_coordinate, + metavar="LAT:LONG", + help="Latitude and longitude (e.g. 32.123:-111.111)", + ) + location_group.add_argument( + "-t", + type=parse_hardcoded_time, + metavar="HH24:MI", + help="Hardcoded sunset time (e.g. 18:00)", + ) + parser.add_argument( + "-m", + type=int, + required=True, + metavar="MINUTES", + help=( + "Minutes offset before/after sunset to trigger the alert. " + "Positive = before sunset; negative = after." + ), + ) + parser.add_argument( + "-h", + action="help", + help="Show this help message and exit.", + ) + args = parser.parse_args() + + offset_minus = timedelta(minutes=args.m) + + # --- Phase 1: resolve sunset time --------------------------- + while True: + try: + if args.l is not None: + lat, lon = args.l + sunset_dt = fetch_sunset(lat, lon) + print(f"Sunset fetched from Open-Meteo: {sunset_dt.isoformat()}") + break # success — stop retry loop + else: + now = datetime.now() + today_date = today_date_naive = now.replace(hour=0, minute=0, second=0, microsecond=0) + sunset_today = datetime.combine(today_date_naive, args.t) + print(f"Using hardcoded sunset: {args.t}") + break # no network needed for -t + except (URLError, OSError) as exc: + print(f"Network unavailable ({exc}), retrying in 5 minutes...", file=sys.stderr) + time.sleep(300) + + # --- Phase 2: wait / loop and alert ------------------------- + alert_threshold = sunset_dt - offset_minus + + if alert_threshold <= datetime.now(): + # Already past the alert window — fire once and exit. + notify_send( + "Sunset Alarm", + f"Alert triggered: {sunset_dt.strftime('%H:%M')} ({offset_minus.total_seconds()/60:+.0f} min from sunset)", + ) + print(f"Alert: {alert_threshold.isoformat()} -> Now: {datetime.now().isoformat()}") + return + + while True: + now = datetime.now() + + if not (alert_threshold <= now < sunset_dt.replace(year=now.year + 1)): + # Normal path: only alert when within the window. + # Check again in 60 seconds. + time.sleep(60) + continue + + if now.date() > sunset_dt.date(): + # Day has passed — stop monitoring, wait until tomorrow. + print(f"Day {sunset_dt.date()} over; re-fetching for new day...", file=sys.stderr) + # Re-enter fetch loop + while True: + try: + lat, lon = args.l + sunset_dt = fetch_sunset(lat, lon) + break + except (URLError, OSError) as exc: + print(f"Network unavailable ({exc}), retrying in 5 minutes...", file=sys.stderr) + time.sleep(300) + continue + + # Within alert window — send notification. + notify_send( + "Sunset Alarm", + f"Alert triggered: {sunset_dt.strftime('%H:%M')} ({offset_minus.total_seconds()/60:+.0f} min from sunset)", + ) + print(f"ALERT -> {alert_threshold.isoformat()} <= {now.isoformat()} < {sunset_dt.isoformat()}") + + # Keep alerting (don't exit). + + +if __name__ == "__main__": + main() diff --git a/sunset_alarm_v2.sh b/sunset_alarm_v2.sh new file mode 100644 index 0000000..e69de29 diff --git a/sunset_alarm_v3-UNFINISHED.sh b/sunset_alarm_v3-UNFINISHED.sh new file mode 100644 index 0000000..3e56180 --- /dev/null +++ b/sunset_alarm_v3-UNFINISHED.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# sunset-alarm — persistent alert at a calculated offset before sunset +# Usage: sunset-alarm (-l LAT:LONG | -t HH24:MI) (-m MINUTES) [-h] +set -uo pipefail + +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) + +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 + (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; } + +# ── 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 + +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" + # 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 + + 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 a hardcoded HH:MM when -t is used. +set_sunset_from_time() { + 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}')")