generated from me/template-mit
Remove the old and unfinished files, will regenerate from scratch.
This commit is contained in:
1 parent
9e294d070f
commit
b14e6a3660
4 files changed
-260
No files matched your search
Whitespace-only changes.
@@ -1,183 +0,0 @@
|
||||
#!/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()
|
||||
Whitespace-only changes.
@@ -1,77 +0,0 @@
|
||||
#!/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}')")
|
||||
Reference in new issue
Block a user