generated from me/template-mit
Compare commits
19
Commits
3a36ec9651
..
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
177bcafe95 | ||
|
|
5fd554e7d5 | ||
|
|
28c0f61696 | ||
|
|
949e1baacd | ||
|
|
7d9bc589c5 | ||
|
|
9cc194fcb4 | ||
|
|
e1995dff80 | ||
|
|
4aed123213 | ||
|
|
b0acb7793a | ||
|
|
1fc42b4e4e | ||
|
|
35db6c2c5f | ||
|
|
f671e3d94d | ||
|
|
68b703ff7e | ||
|
|
21f02945bb | ||
|
|
fae03c94d9 | ||
|
|
ffc29eabb2 | ||
|
|
b14e6a3660 | ||
|
|
9e294d070f | ||
|
|
68ae3e1b9a |
+234
-59
@@ -1,77 +1,252 @@
|
||||
#!/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
|
||||
#!/bin/sh
|
||||
|
||||
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)
|
||||
# sunset-alarm -- Alert before or after sunset.
|
||||
# Displays an alert when current time is within (sunset - specified minutes).
|
||||
|
||||
###############################################################################
|
||||
# Defaults
|
||||
###############################################################################
|
||||
declare LATLONG="" SUNSET_TIME="" MINUTES="" LAST_DATE="" PERSIST="" NOTIFIED=""
|
||||
typeset -i SUNSET_EPOCH=0 CURRENT_TIME=0
|
||||
|
||||
###############################################################################
|
||||
# Usage
|
||||
###############################################################################
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: sunset-alarm (-l LAT:LONG | -t HH24:MI) (-m MINUTES) [-h]
|
||||
cat <<EOF
|
||||
Usage: sunset-alarm (-l LAT:LONG | -t HH24:MI) (-m MINUTES) [-p] [-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.
|
||||
Display a notification when a certain time related to sunset has been reached.
|
||||
|
||||
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
|
||||
Parameters:
|
||||
-l) Takes a LAT:LONG parameter to check Open Meteo for the sunset time if
|
||||
there is an Internet connection. Otherwise may fail to notify.
|
||||
-t) Takes a hard-coded 24-hour formatted time for guessing when sunset is.
|
||||
-m) Takes the number of minutes that the alert should appear before sunset.
|
||||
Using a negative time is allowed if you prefer having the alert after.
|
||||
-p) Persist the notification, showing back up every 5 minutes to midnight.
|
||||
-h) Display this usage.
|
||||
|
||||
Example: Show the alert 90 minutes before sunset in Colorado City AZ.
|
||||
sunset-alarm -l 37:-113 -m 90
|
||||
|
||||
Example: Show the alert at exactly 8:30 PM.
|
||||
sunset-alarm -t 20:30 -m 0
|
||||
|
||||
Example: Show the alert 30 minutes after sunset in Flagstaff AZ.
|
||||
sunset-alarm -l 35.1987671:-111.651813 -m -30
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
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
|
||||
###############################################################################
|
||||
# Parse arguments (handles negative values for -m by using "$@")
|
||||
###############################################################################
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-l) LATLONG="$2"; shift 2 ;;
|
||||
-t) SUNSET_TIME="$2"; shift 2 ;;
|
||||
-m) MINUTES="$2"; shift 2 ;;
|
||||
-p) PERSIST="true"; shift 1 ;;
|
||||
-h) usage ;;
|
||||
--) shift; break ;;
|
||||
*) echo "ERROR: unknown argument '$1'" >&2; usage ;;
|
||||
esac
|
||||
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
|
||||
# Validate inputs.
|
||||
if [ -z "$MINUTES" ]; then
|
||||
echo "ERROR: -m MINUTES is required." >&2
|
||||
usage
|
||||
fi
|
||||
|
||||
if [ -z "$LATLONG" ] && [ -z "$SUNSET_TIME" ]; then
|
||||
echo "ERROR: must specify either -l LAT:LONG or -t HH:MI." >&2
|
||||
usage
|
||||
fi
|
||||
|
||||
LATLONG=${LATLONG//\,/:}
|
||||
LATLONG=${LATLONG//\ /:}
|
||||
LATLONG=${LATLONG//\_/:}
|
||||
LATLONG=${LATLONG//\&/:}
|
||||
LATLONG=${LATLONG//\?/:}
|
||||
if [[ -n "$LATLONG" && "$LATLONG" != *":"* ]]; then
|
||||
echo "ERROR: LAT:LONG is missing the colon." >&2
|
||||
usage
|
||||
fi
|
||||
|
||||
###############################################################################
|
||||
# Internet connectivity check
|
||||
###############################################################################
|
||||
check_internet() {
|
||||
if curl -s --max-time 5 -o /dev/null https://hyperling.com; then
|
||||
echo "Confirmed connectivity with Hyperling."
|
||||
return 0
|
||||
fi
|
||||
if curl -s --max-time 5 -o /dev/null https://mullvad.net; then
|
||||
echo "Confirmed connectivity with Mullvad."
|
||||
return 0
|
||||
fi
|
||||
echo "ERROR: Connectivity checks failed." >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
# ── 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
|
||||
###############################################################################
|
||||
# Get sunset from Open Meteo API
|
||||
###############################################################################
|
||||
fetch_sunset_time() {
|
||||
echo "Checking for sunset time of lat:long ${LATLONG}."
|
||||
LAT=$(echo "$LATLONG" | cut -d: -f1)
|
||||
LON=$(echo "$LATLONG" | cut -d: -f2)
|
||||
TODAY="`date +'%Y-%m-%d'`"
|
||||
|
||||
local code="${resp##*$'\n'}"
|
||||
resp="${resp%$'\n'*}" # strip the trailing HTTP code
|
||||
#REQUEST="https://api.open-meteo.com/v1/forecast?latitude=${LAT}&longitude=${LON}&timezone=auto&daily=sunset&date=today"
|
||||
#REQUEST="https://api.open-meteo.com/v1/forecast?latitude=${LAT}&longitude=${LON}&timezone=auto&daily=sunset&forecast_days=1"
|
||||
REQUEST="https://api.open-meteo.com/v1/forecast?latitude=${LAT}&longitude=${LON}&timezone=auto&daily=sunset&start_date=${TODAY}&end_date=${TODAY}"
|
||||
RESPONSE=$(curl -s "$REQUEST")
|
||||
status="$?"
|
||||
echo "Curl command completed with a status of $status."
|
||||
|
||||
[[ "${code:-599}" == "200" ]] || return 2
|
||||
if [[ -z "$RESPONSE" ]]; then
|
||||
echo "ERROR: Did not receive response. $REQUEST" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local iso; iso=$(printf '%s\n' "$resp" | jq -r '.current.sunset // empty')
|
||||
[[ -n "$iso" ]] || return 3 # null / missing field
|
||||
# Extract sunset timestamp like "2025-06-15T19:32:00".
|
||||
SUNSET_RAW=$(echo "$RESPONSE" | jq -r '.daily.sunset[0]')
|
||||
|
||||
SUNSET_EPOCH=$(date -d "$iso" +%s) # local-time epoch
|
||||
if [[ -z "$SUNSET_RAW" ]]; then
|
||||
echo "ERROR: Could not parse time. $RESPONSE" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Convert to epoch.
|
||||
SUNSET_EPOCH=$(date -d "$(echo "$SUNSET_RAW" | cut -dT -f1) $(echo "$SUNSET_RAW" | cut -dT -f2)" +%s 2>/dev/null)
|
||||
|
||||
if [[ -z "$SUNSET_EPOCH" ]]; then
|
||||
echo "ERROR: Could not calculate epoch. $SUNSET_RAW" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "Got ${SUNSET_RAW} and converted it to epoch ${SUNSET_EPOCH}."
|
||||
return 0
|
||||
}
|
||||
|
||||
# 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}')")
|
||||
###############################################################################
|
||||
# Show alert via fallback chain: fyi -> notify-send -> wall
|
||||
###############################################################################
|
||||
show_alert() {
|
||||
ALERT=$(date -d @${ALERT_WINDOW_START} +"%H:%M")
|
||||
SUNSET=$(date -d @${SUNSET_EPOCH} +"%H:%M")
|
||||
PLURAL=$(if (( $MINUTES != 1 )); then echo "s"; fi)
|
||||
|
||||
TITLE="Sunset Alarm ($ALERT)"
|
||||
MESSAGE="It's ${MINUTES} minute${PLURAL} to $SUNSET."
|
||||
|
||||
keep_alive="-t 0 --wait"
|
||||
icon="-i weather-clear -u critical"
|
||||
|
||||
if command -v fyi >/dev/null 2>&1; then
|
||||
fyi $icon $keep_alive "$TITLE" "$MESSAGE"
|
||||
elif command -v notify-send >/dev/null 2>&1; then
|
||||
notify-send $icon $keep_alive "$TITLE" "$MESSAGE"
|
||||
elif command -v wall >/dev/null 2>&1; then
|
||||
echo "${TITLE}: ${MESSAGE}" | wall
|
||||
else
|
||||
echo "ERROR: No notification tool found." >&2
|
||||
echo "Install one of: fyi, notify-send (libnotify-bin), wall (bsd-write)" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Sleep until a certain amount of time
|
||||
###############################################################################
|
||||
rest() {
|
||||
if [[ "$CURRENT_TIME" -ge 0 ]]; then
|
||||
TARGET="$1"
|
||||
sleep_seconds=$(( TARGET - CURRENT_TIME ))
|
||||
echo "Sleeping ${sleep_seconds}s until $(date -d @$TARGET +%H:%M:%S)."
|
||||
sleep "$sleep_seconds"
|
||||
fi
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Main loop
|
||||
###############################################################################
|
||||
while true; do
|
||||
TODAY=$(date +%Y-%m-%d)
|
||||
CURRENT_TIME=$(date +%s)
|
||||
|
||||
# Check if we need to fetch new sunset time for a new day.
|
||||
if [[ "$LAST_DATE" != "$TODAY" || "$SUNSET_EPOCH" == 0 ]]
|
||||
then
|
||||
LAST_DATE="$TODAY"
|
||||
|
||||
if [[ -n "$LATLONG" ]]; then
|
||||
# Need internet to get sunset time.
|
||||
if ! check_internet; then
|
||||
echo "ERROR: No internet connection. Retrying in 5 minutes." >&2
|
||||
sleep 300
|
||||
continue
|
||||
fi
|
||||
|
||||
fetch_sunset_time || {
|
||||
echo "ERROR: Failed fetching sunset time. Retrying in 5 minutes." >&2
|
||||
sleep 300
|
||||
continue
|
||||
}
|
||||
|
||||
if [[ "$SUNSET_EPOCH" == 0 ]]; then
|
||||
echo "ERROR: Sunset time is still null. Retrying in 5 minutes." >&2
|
||||
sleep 300
|
||||
continue
|
||||
fi
|
||||
else
|
||||
# User provided sunset time directly, convert to epoch.
|
||||
SUNSET_EPOCH=$(date -d "${TODAY} ${SUNSET_TIME}" +%s 2>/dev/null)
|
||||
|
||||
if [[ -z "$SUNSET_EPOCH" ]]; then
|
||||
echo "ERROR: Could not parse time '${SUNSET_TIME}'." >&2
|
||||
sleep 60
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Calculate alert window start (sunset minus specified minutes).
|
||||
ALERT_WINDOW_START=$(( SUNSET_EPOCH - (MINUTES * 60) ))
|
||||
|
||||
# Calculate midnight of the current day
|
||||
MIDNIGHT=$(date -d "${TODAY} +1 day" +%s)
|
||||
|
||||
# Alert while current time is within the window (from window start until midnight).
|
||||
if [[ "$SUNSET_EPOCH" -ge 0 \
|
||||
&& ( -z "$NOTIFIED" || -n "$PERSIST" ) \
|
||||
&& "$CURRENT_TIME" -ge "$ALERT_WINDOW_START" \
|
||||
&& "$CURRENT_TIME" -lt "$MIDNIGHT" ]]
|
||||
then
|
||||
echo "Alert time has been reached, show message."
|
||||
show_alert
|
||||
NOTIFIED="true"
|
||||
echo "Alert has been dismissed."
|
||||
if [[ -n "$PERSIST" ]]; then
|
||||
echo "Sleeping for 5 minutes before next notification."
|
||||
sleep 300
|
||||
fi
|
||||
elif [[ "$SUNSET_EPOCH" == 0 ]]; then
|
||||
echo "ERROR: Sunset time has not been calculated yet, cannot alert." >&2
|
||||
elif [[ -n "$NOTIFIED" && -z "$PERSIST" ]]; then
|
||||
echo "Already notified for today ($TODAY)."
|
||||
rest $MIDNIGHT
|
||||
else
|
||||
echo "Not time to notify yet."
|
||||
rest $ALERT_WINDOW_START
|
||||
fi
|
||||
|
||||
# Reset when we've passed midnight so we fetch the new time for the day.
|
||||
if [[ "$CURRENT_TIME" -ge "$MIDNIGHT" ]]; then
|
||||
LAST_DATE=""
|
||||
NOTIFIED=""
|
||||
fi
|
||||
done
|
||||
Reference in new issue
Block a user