sunrise.py: replace suntime with skyfield for accurate sunrise calculation
The suntime library uses a simplified approximation that degrades at high latitudes as sunrise approaches very early hours near the summer solstice. The computed sunrise drifted progressively earlier than the actual event, shrinking the capture window until today it ended before the sun rose. Switch to skyfield's sunrise_sunset almanac (DE421 ephemeris), which is accurate to within a second at any latitude — the same library already used by moon_phase.py for sun events. The search window is anchored to local midnight→midnight UTC so the correct calendar-day sunrise is always returned regardless of timezone offset. Also promote skyfield to the core pip install in bootstrap.sh (dropping suntime, which is no longer needed) and tidy the stale "suntime" hints in 4-seasons.sh and montage-mvt.sh error messages. https://claude.ai/code/session_01S4oUbU9xNj91cbV5bj5NRn
This commit is contained in:
+1
-1
@@ -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"
|
||||
|
||||
+2
-2
@@ -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"
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
+51
-7
@@ -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'))
|
||||
|
||||
Reference in New Issue
Block a user