218 lines
6.0 KiB
Python
218 lines
6.0 KiB
Python
|
"""Main module of the Seasonal Clock"""
|
||
|
|
||
|
__version__ = '0.1.0'
|
||
|
|
||
|
from datetime import datetime, timedelta
|
||
|
from typing import List, Tuple, Union
|
||
|
|
||
|
from astral import Depression, LocationInfo, SunDirection, moon, sun
|
||
|
from pytz import UTC
|
||
|
|
||
|
from .config import load_config
|
||
|
from .hours import seasonal_hours
|
||
|
|
||
|
|
||
|
def get_moon_phase_text(moon_phase: float) -> str:
|
||
|
"""Get the name of the Moon phase"""
|
||
|
|
||
|
name = 'Dark Moon'
|
||
|
|
||
|
if moon_phase < 0.05:
|
||
|
name = 'New Moon'
|
||
|
elif moon_phase < 6.95:
|
||
|
name = 'Waxing Crescent Moon'
|
||
|
elif moon_phase < 7.05:
|
||
|
name = 'Waxing Half Moon'
|
||
|
elif moon_phase < 13.95:
|
||
|
name = 'Waxing Gibbous Moon'
|
||
|
elif moon_phase < 14.05:
|
||
|
name = 'Full Moon'
|
||
|
elif moon_phase < 20.95:
|
||
|
name = 'Waning Gibbous Moon'
|
||
|
elif moon_phase < 21.05:
|
||
|
name = 'Waning Half Moon'
|
||
|
elif moon_phase < 27.95:
|
||
|
name = 'Waning Crescent Moon'
|
||
|
|
||
|
return name
|
||
|
|
||
|
|
||
|
def collect_day_parts(
|
||
|
observer: LocationInfo, date: datetime
|
||
|
) -> List[Tuple[str, datetime, str, str]]:
|
||
|
"""Collect timestamp for all parts of the day on date"""
|
||
|
|
||
|
day_parts = []
|
||
|
|
||
|
midnight = sun.midnight(observer, date=date)
|
||
|
|
||
|
if midnight.date() < date.date():
|
||
|
midnight = sun.midnight(observer, date=date + timedelta(days=1))
|
||
|
elif midnight.date() > date.date():
|
||
|
midnight = sun.midnight(observer, date=date - timedelta(days=1))
|
||
|
|
||
|
day_parts.append(('midnight', midnight, 'Night', 'Midnight'))
|
||
|
|
||
|
try:
|
||
|
morning_blue_start = sun.blue_hour(
|
||
|
observer, date=date, direction=SunDirection.RISING
|
||
|
)[0]
|
||
|
except ValueError:
|
||
|
# At certain times and latitudes there might be no blue hour
|
||
|
pass
|
||
|
else:
|
||
|
day_parts.append(
|
||
|
(
|
||
|
'morning_blue_start',
|
||
|
morning_blue_start,
|
||
|
'Morning blue hour',
|
||
|
'Morning golden hour',
|
||
|
)
|
||
|
)
|
||
|
|
||
|
try:
|
||
|
golden_start, golden_end = sun.golden_hour(
|
||
|
observer, date=date, direction=SunDirection.RISING
|
||
|
)
|
||
|
except ValueError:
|
||
|
# At certain times and latitudes there might be no golden hour
|
||
|
pass
|
||
|
else:
|
||
|
day_parts.append(
|
||
|
('morning_golden_start', golden_start, 'Morning golden hour', 'Full Daytime')
|
||
|
)
|
||
|
day_parts.append(('morning_golden_end', golden_end, 'Morning', 'Noon'))
|
||
|
|
||
|
day_parts.append(('noon', sun.noon(observer, date=date), 'Afternoon', 'Noon'))
|
||
|
|
||
|
try:
|
||
|
evening_golden_start = sun.golden_hour(observer, date=date, direction=SunDirection.SETTING)[0]
|
||
|
except ValueError:
|
||
|
# At certain times and latitudes there might be no golden hour
|
||
|
pass
|
||
|
else:
|
||
|
day_parts.append(
|
||
|
(
|
||
|
'evening_golden_start',
|
||
|
evening_golden_start,
|
||
|
'Evening golden hour',
|
||
|
'Evening blue hour',
|
||
|
)
|
||
|
)
|
||
|
|
||
|
try:
|
||
|
blue_start, blue_end = sun.blue_hour(
|
||
|
observer, date=date, direction=SunDirection.SETTING
|
||
|
)
|
||
|
except ValueError:
|
||
|
# At certain times and latitudes there might be no blue hour
|
||
|
pass
|
||
|
else:
|
||
|
day_parts.append(('evening_blue_start', blue_start, 'Evening blue hour', 'Night'))
|
||
|
day_parts.append(('evening_blue_end', blue_end, 'Night', 'Midnight'))
|
||
|
|
||
|
day_parts.sort(key=lambda elem: elem[1])
|
||
|
|
||
|
return day_parts
|
||
|
|
||
|
|
||
|
def get_rahukaalam(observer: LocationInfo, date: datetime) -> Tuple[bool, datetime]:
|
||
|
"""Get the time of the next Rāhukāla or, if we are in the middle of one, the time of its end"""
|
||
|
|
||
|
yesterday = date - timedelta(days=1)
|
||
|
tomorrow = date + timedelta(days=1)
|
||
|
times: List[Tuple[datetime, datetime]] = []
|
||
|
|
||
|
for day in (yesterday, date, tomorrow):
|
||
|
try:
|
||
|
daytime_rahukaalam = sun.rahukaalam(observer, date=day, daytime=False)
|
||
|
except ValueError:
|
||
|
pass
|
||
|
else:
|
||
|
times.append(daytime_rahukaalam)
|
||
|
|
||
|
try:
|
||
|
night_rahukaalam = sun.rahukaalam(observer, date=day, daytime=True)
|
||
|
except ValueError:
|
||
|
pass
|
||
|
else:
|
||
|
times.append(night_rahukaalam)
|
||
|
|
||
|
times.sort(key=lambda items: items[0])
|
||
|
active: bool = False
|
||
|
next_time: datetime = None
|
||
|
|
||
|
for start, end in times:
|
||
|
if date < start:
|
||
|
active = False
|
||
|
next_time = start
|
||
|
|
||
|
break
|
||
|
|
||
|
if start < date < end:
|
||
|
active = True
|
||
|
next_time = end
|
||
|
|
||
|
break
|
||
|
|
||
|
if next_time is None:
|
||
|
return None, None
|
||
|
|
||
|
return active, next_time
|
||
|
|
||
|
|
||
|
def main() -> None:
|
||
|
"""The Main Thing™"""
|
||
|
|
||
|
config = load_config()
|
||
|
|
||
|
location = LocationInfo(
|
||
|
config['city'],
|
||
|
config['country'],
|
||
|
config['timezone'],
|
||
|
config['latitude'],
|
||
|
config['longitude'],
|
||
|
)
|
||
|
local_tz = location.tzinfo
|
||
|
utc_now = datetime.utcnow().replace(tzinfo=UTC)
|
||
|
local_now = utc_now.astimezone(local_tz)
|
||
|
|
||
|
moon_phase = moon.phase(local_now)
|
||
|
|
||
|
day_parts = collect_day_parts(location.observer, local_now)
|
||
|
|
||
|
final_name = 'Night'
|
||
|
upcoming_name = None
|
||
|
final_idx = -1
|
||
|
|
||
|
for idx, (_, time, name, next_name) in enumerate(day_parts):
|
||
|
if utc_now > time:
|
||
|
final_name = name
|
||
|
upcoming_name = next_name
|
||
|
final_idx = idx
|
||
|
|
||
|
next_time = day_parts[final_idx + 1][1]
|
||
|
|
||
|
if upcoming_name is None:
|
||
|
upcoming_name = day_parts[0][2]
|
||
|
|
||
|
print(
|
||
|
f'Now: {local_now.strftime("%Y-%m-%d %H:%M:%S")} (UTC {utc_now.strftime("%Y-%m-%d %H:%M:%S")})'
|
||
|
)
|
||
|
print(f'{final_name}, {next_time-local_now} until {upcoming_name}')
|
||
|
|
||
|
rahukaalam, next_rahukaalam = get_rahukaalam(location.observer, local_now)
|
||
|
|
||
|
if next_rahukaalam is not None:
|
||
|
if rahukaalam:
|
||
|
print(f'Rāhukāla (until {next_rahukaalam.strftime("%H:%M:%S")})')
|
||
|
else:
|
||
|
print(f'Next Rāhukāla at {next_rahukaalam.strftime("%H:%M:%S")}')
|
||
|
|
||
|
print(get_moon_phase_text(moon_phase))
|
||
|
|
||
|
hour_num = utc_now.hour
|
||
|
hour_short = seasonal_hours[utc_now.hour].short
|
||
|
hour_long = seasonal_hours[utc_now.hour].long
|
||
|
print(f'{hour_long} ({hour_short}, {hour_num})')
|