"""日出日落时间计算(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))