This commit is contained in:
me committed 2026-10-17 13:32:50 -07:00
1 parent 15c909d954
commit 6ce4a9bbf0
1 file changed
+183
Executable
+183
View File
@@ -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"&current=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()