#!/bin/sh # 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 <&2; usage ;; esac done # 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 } ############################################################################### # 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'`" #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." if [[ -z "$RESPONSE" ]]; then echo "ERROR: Did not receive response. $REQUEST" >&2 return 1 fi # Extract sunset timestamp like "2025-06-15T19:32:00". SUNSET_RAW=$(echo "$RESPONSE" | jq -r '.daily.sunset[0]') 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 } ############################################################################### # 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