Files
iot-dboard/backend/sun_util.py
T
2026-08-06 11:11:11 +08:00

115 lines
4.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""日出日落时间计算(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))