diff --git a/4-seasons.sh b/4-seasons.sh index 1d4450e..df1ecd3 100755 --- a/4-seasons.sh +++ b/4-seasons.sh @@ -39,7 +39,7 @@ echo "Processing: $yesterday" # ── Season / movement info from astronomical calculation ────────────────────── _season_info="$(python3 "$SCRIPT_DIR/season_info.py" "$yesterday")" || { - echo "Error: season_info.py failed — check Python dependencies (suntime pytz)" + echo "Error: season_info.py failed — check Python dependencies (pytz)" exit 1 } eval "$_season_info" diff --git a/bootstrap.sh b/bootstrap.sh index ca00f93..0f8a5b9 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -22,8 +22,8 @@ echo "" echo "Done. Next steps:" echo " 1. Install system packages (if not already present):" echo " sudo apt install ffmpeg bc fonts-dejavu" -echo " pip3 install suntime pytz requests" -echo " pip3 install skyfield Pillow numpy scipy # moon jobs (moon-track, moon-phase-monthly)" +echo " pip3 install pytz requests skyfield" +echo " pip3 install Pillow numpy scipy # moon jobs (moon-track, moon-phase-monthly)" echo "" echo " 2. Edit $TARGET/sky-cam.conf" echo " — SCRIPT_DIR full path to the directory you'll run scripts from" diff --git a/montage-mvt.sh b/montage-mvt.sh index 2d79fc7..c01db83 100755 --- a/montage-mvt.sh +++ b/montage-mvt.sh @@ -47,7 +47,7 @@ ATTR_FADE="$MONTAGE_ATTR_FADE" # ── Season / movement info ──────────────────────────────────────────────────── _season_info="$(python3 "$SCRIPT_DIR/season_info.py" ${DATE_ARG:+"$DATE_ARG"})" || { - echo "Error: season_info.py failed — check Python dependencies (suntime pytz)" + echo "Error: season_info.py failed — check Python dependencies (pytz)" exit 1 } eval "$_season_info" diff --git a/sunrise.py b/sunrise.py index 1db47d1..bb04a99 100755 --- a/sunrise.py +++ b/sunrise.py @@ -1,13 +1,24 @@ #!/usr/bin/env python3 -from datetime import datetime +"""sunrise.py — compute today's local sunrise time using skyfield. + +Outputs: YYYY-MM-DD HH:MM:SS (local time, no timezone suffix) +Exits non-zero with a message on stderr if the sun does not rise today +(polar night or midnight sun). + +Reads LATITUDE, LONGITUDE, TIMEZONE from sky-cam.conf / .env. +Requires skyfield and the de421.bsp ephemeris alongside this script +(skyfield downloads de421.bsp automatically on first use if absent). +""" + +from datetime import datetime, timezone, timedelta import pathlib import re import pytz -from suntime import Sun _here = pathlib.Path(__file__).resolve().parent + def _read_conf(path): conf = {} try: @@ -30,6 +41,7 @@ def _read_conf(path): pass return conf + conf = _read_conf(_here / 'sky-cam.conf') # .env overrides sky-cam.conf — mirrors the sourcing order at the bottom of sky-cam.conf conf.update(_read_conf(_here / '.env')) @@ -42,9 +54,41 @@ latitude = float(conf['LATITUDE']) longitude = float(conf['LONGITUDE']) tz = pytz.timezone(conf['TIMEZONE']) -sun = Sun(latitude, longitude) -now = datetime.now(tz) -sunrise_utc = sun.get_sunrise_time(now) -sunrise_time = sunrise_utc.astimezone(tz) +# Search the full local calendar day (midnight→midnight) expressed in UTC. +# This bracket is timezone-correct and handles any UTC offset, including large +# positive offsets (UTC+12/+14) where local midnight falls on the previous UTC day. +now_local = datetime.now(tz) +midnight_local = now_local.replace(hour=0, minute=0, second=0, microsecond=0) +t0_utc = midnight_local.astimezone(timezone.utc) +t1_utc = t0_utc + timedelta(hours=25) # 25 h covers any timezone's full day -print(sunrise_time.strftime('%Y-%m-%d %H:%M:%S')) +from skyfield.api import Loader, wgs84 +from skyfield.almanac import find_discrete, sunrise_sunset + +loader = Loader(str(_here), verbose=False) +ts = loader.timescale() +eph = loader('de421.bsp') # downloaded automatically on first run +topos = wgs84.latlon(latitude, longitude) + +t0 = ts.from_datetime(t0_utc) +t1 = ts.from_datetime(t1_utc) + +times, events = find_discrete(t0, t1, sunrise_sunset(eph, topos)) + +# events: True = sunrise, False = sunset — pick the first sunrise on today's date +sunrise_local = None +for t, is_rise in zip(times, events): + if is_rise: + candidate = t.utc_datetime().astimezone(tz) + if candidate.date() == now_local.date(): + sunrise_local = candidate + break + +if sunrise_local is None: + raise SystemExit( + "sunrise.py: no sunrise found for today " + f"({now_local.date()} at {latitude:.4f}°, {longitude:.4f}°) — " + "polar night or midnight sun?" + ) + +print(sunrise_local.strftime('%Y-%m-%d %H:%M:%S'))