#!/usr/bin/env python3 """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 _here = pathlib.Path(__file__).resolve().parent def _read_conf(path): conf = {} try: with open(path) as f: for line in f: line = line.strip() if not line or line.startswith('#') or '=' not in line: continue k, v = line.split('=', 1) k = k.strip() v = v.strip() v = re.sub(r'\s+#.*$', '', v) # strip inline comments v = v.strip('"').strip("'") # Extract default from bash ${VAR:-default} pattern m = re.match(r'^\$\{[^}]+:-([^}]*)\}$', v) if m: v = m.group(1).strip('"').strip("'") conf[k] = v except FileNotFoundError: 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')) for _key in ('LATITUDE', 'LONGITUDE', 'TIMEZONE'): if not conf.get(_key): raise SystemExit(f"sunrise.py: {_key} not set in sky-cam.conf or .env") latitude = float(conf['LATITUDE']) longitude = float(conf['LONGITUDE']) tz = pytz.timezone(conf['TIMEZONE']) # 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 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'))