115 lines
4.2 KiB
Python
115 lines
4.2 KiB
Python
"""日出日落时间计算(NOAA 简化算法)与白天/夜晚判定。
|
||
|
||
用于"自动"主题模式:根据用户天气配置的经纬度 + 本地时区,判断当前
|
||
时刻处于白天(浅色主题)还是夜晚(深色主题)。
|
||
|
||
算法来源:Ed Williams《Sunrise/Sunset Algorithm》
|
||
(https://edwilliams.org/sunrise_sunset_algorithm.htm)
|
||
"""
|
||
import math
|
||
from datetime import datetime, timezone, timedelta
|
||
|
||
# 太阳天顶角:日出日落定义(含大气折射)
|
||
ZENITH = 90.833
|
||
|
||
|
||
def _day_of_year(dt: datetime) -> int:
|
||
"""计算一年中的第几天(NOAA 公式)。"""
|
||
y, m, d = dt.year, dt.month, dt.day
|
||
n1 = math.floor(275 * m / 9)
|
||
n2 = math.floor((m + 9) / 12)
|
||
n3 = 1 + math.floor((y - 4 * math.floor(y / 4) + 2) / 3)
|
||
return n1 - (n2 * n3) + d - 30
|
||
|
||
|
||
def _hour_angle(lat: float, lon: float, day_of_year: int, is_sunrise: bool) -> float | None:
|
||
"""返回日出/日落的 UTC 小时(0~24),None 表示极昼/极夜(当天无日出日落)。"""
|
||
lng_hour = lon / 15.0
|
||
if is_sunrise:
|
||
t = day_of_year + ((6 - lng_hour) / 24.0)
|
||
else:
|
||
t = day_of_year + ((18 - lng_hour) / 24.0)
|
||
|
||
m = (0.9856 * t) - 3.289
|
||
l = (m + 1.916 * math.sin(math.radians(m))
|
||
+ 0.020 * math.sin(math.radians(2 * m)) + 282.634) % 360
|
||
|
||
ra = math.degrees(math.atan(0.91764 * math.tan(math.radians(l)))) % 360
|
||
# 校正 RA 象限
|
||
l_quad = math.floor(l / 90) * 90
|
||
ra_quad = math.floor(ra / 90) * 90
|
||
ra = ra + (l_quad - ra_quad)
|
||
ra /= 15.0 # 转为小时
|
||
|
||
sin_dec = 0.39782 * math.sin(math.radians(l))
|
||
cos_dec = math.cos(math.asin(sin_dec))
|
||
cos_h = ((math.cos(math.radians(ZENITH)) - sin_dec * math.sin(math.radians(lat)))
|
||
/ (cos_dec * math.cos(math.radians(lat))))
|
||
|
||
if cos_h > 1 or cos_h < -1:
|
||
return None # 极昼/极夜
|
||
|
||
if is_sunrise:
|
||
h = 360 - math.degrees(math.acos(cos_h))
|
||
else:
|
||
h = math.degrees(math.acos(cos_h))
|
||
h /= 15.0
|
||
|
||
# NOAA:T = H + RA - 0.06571*t - 6.622(小时),UT = T - lngHour(mod 24)
|
||
local_t = h + ra - 0.06571 * t - 6.622
|
||
return (local_t - lng_hour) % 24.0
|
||
|
||
|
||
def sun_times_utc(lat: float, lon: float, date: datetime) -> tuple[datetime | None, datetime | None]:
|
||
"""返回 (日出UTC, 日落UTC),极昼/极夜时对应项为 None。
|
||
date 为任意带时区 datetime(取其日期)。"""
|
||
lat = float(lat)
|
||
lon = float(lon)
|
||
day = _day_of_year(date.astimezone(timezone.utc))
|
||
|
||
sunrise_h = _hour_angle(lat, lon, day, True)
|
||
sunset_h = _hour_angle(lat, lon, day, False)
|
||
if sunrise_h is None and sunset_h is None:
|
||
return None, None # 极昼或极夜,调用方按本地小时粗判
|
||
|
||
def to_utc(h: float) -> datetime:
|
||
base = date.astimezone(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
|
||
return base + timedelta(hours=h)
|
||
|
||
sunrise = to_utc(sunrise_h) if sunrise_h is not None else None
|
||
sunset = to_utc(sunset_h) if sunset_h is not None else None
|
||
return sunrise, sunset
|
||
|
||
|
||
def is_daytime(lat: float, lon: float, now_local: datetime) -> bool:
|
||
"""判断本地时刻是否为白天。
|
||
now_local 为带时区的本地 datetime(使用用户配置时区)。
|
||
|
||
用 UTC 小时比较(忽略日期归属),自动处理日出/日落跨 UTC 午夜的情况。
|
||
"""
|
||
now_utc = now_local.astimezone(timezone.utc)
|
||
sunrise, sunset = sun_times_utc(lat, lon, now_local)
|
||
|
||
if sunrise is None or sunset is None:
|
||
# 极昼/极夜:根据本地小时粗判(6-18 视为白天)
|
||
return 6 <= now_local.hour < 18
|
||
|
||
sr_h = sunrise.hour + sunrise.minute / 60.0
|
||
ss_h = sunset.hour + sunset.minute / 60.0
|
||
now_h = now_utc.hour + now_utc.minute / 60.0
|
||
|
||
if ss_h < sr_h:
|
||
ss_h += 24.0 # 跨 UTC 午夜(东半球)
|
||
if now_h < sr_h:
|
||
now_h += 24.0
|
||
return sr_h <= now_h <= ss_h
|
||
|
||
|
||
def day_fraction_today(lat: float, lon: float, now_local: datetime) -> float:
|
||
"""当天白昼占全天比例(0~1),用于自动模式的平滑切换(预留)。"""
|
||
sunrise, sunset = sun_times_utc(lat, lon, now_local)
|
||
if sunrise is None or sunset is None:
|
||
return 0.5
|
||
total = (sunset - sunrise).total_seconds()
|
||
return max(0.0, min(1.0, total / 86400.0))
|