feat: introduce WeatherDataCollector for fetching multi-source weather data from OpenWeatherMap, Visual Crossing, and NOAA METAR.
This commit is contained in:
@@ -517,6 +517,63 @@ def analyze_weather_trend(weather_data, temp_symbol):
|
|||||||
else:
|
else:
|
||||||
insights.append(f"⏰ <b>入场时机:不建议</b> — {factors_str}。不确定性大,等更多数据。")
|
insights.append(f"⏰ <b>入场时机:不建议</b> — {factors_str}。不确定性大,等更多数据。")
|
||||||
|
|
||||||
|
# === 明日预览:当今日峰值已过,自动显示明天的模型共识 ===
|
||||||
|
if is_peak_passed:
|
||||||
|
tomorrow_forecasts = {}
|
||||||
|
tomorrow_date = None
|
||||||
|
|
||||||
|
# 从 multi_model 的 daily_forecasts 中取明天数据
|
||||||
|
mm_daily = multi_model.get("daily_forecasts", {})
|
||||||
|
mm_dates = multi_model.get("dates", [])
|
||||||
|
|
||||||
|
if len(mm_dates) >= 2:
|
||||||
|
tomorrow_date = mm_dates[1]
|
||||||
|
tomorrow_forecasts = mm_daily.get(tomorrow_date, {})
|
||||||
|
|
||||||
|
# 明天的 Open-Meteo 预报
|
||||||
|
tomorrow_om = None
|
||||||
|
om_max_list = daily.get("temperature_2m_max", [])
|
||||||
|
om_dates = daily.get("time", [])
|
||||||
|
if len(om_max_list) >= 2:
|
||||||
|
tomorrow_om = om_max_list[1]
|
||||||
|
if tomorrow_date is None and len(om_dates) >= 2:
|
||||||
|
tomorrow_date = om_dates[1]
|
||||||
|
|
||||||
|
if tomorrow_date and (tomorrow_forecasts or tomorrow_om is not None):
|
||||||
|
# 格式化日期 (02-24)
|
||||||
|
date_short = tomorrow_date[5:] if tomorrow_date else "明天"
|
||||||
|
|
||||||
|
preview_parts = [f"\n📋 <b>明日预览 ({date_short})</b>"]
|
||||||
|
|
||||||
|
if tomorrow_om is not None:
|
||||||
|
preview_parts.append(f"📊 Open-Meteo 预报: {tomorrow_om}{temp_symbol}")
|
||||||
|
|
||||||
|
if tomorrow_forecasts:
|
||||||
|
t_values = list(tomorrow_forecasts.values())
|
||||||
|
t_max = max(t_values)
|
||||||
|
t_min = min(t_values)
|
||||||
|
t_spread = t_max - t_min
|
||||||
|
|
||||||
|
is_f = (temp_symbol == "°F")
|
||||||
|
tight = 1.5 if is_f else 0.8
|
||||||
|
mid = 3.0 if is_f else 1.5
|
||||||
|
|
||||||
|
parts = " | ".join([f"{name} {val}{temp_symbol}" for name, val in tomorrow_forecasts.items()])
|
||||||
|
|
||||||
|
if t_spread <= tight:
|
||||||
|
preview_parts.append(f"🎯 模型共识:高 — {parts},极差 {t_spread:.1f}°")
|
||||||
|
elif t_spread <= mid:
|
||||||
|
preview_parts.append(f"⚖️ 模型共识:中 — {parts},极差 {t_spread:.1f}°")
|
||||||
|
else:
|
||||||
|
highest = max(tomorrow_forecasts.items(), key=lambda x: x[1])
|
||||||
|
lowest = min(tomorrow_forecasts.items(), key=lambda x: x[1])
|
||||||
|
preview_parts.append(
|
||||||
|
f"⚠️ 模型共识:低 — {parts},极差 {t_spread:.1f}°!"
|
||||||
|
f"{highest[0]} 最高 vs {lowest[0]} 最低"
|
||||||
|
)
|
||||||
|
|
||||||
|
insights.extend(preview_parts)
|
||||||
|
|
||||||
if not insights:
|
if not insights:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|||||||
@@ -653,6 +653,8 @@ class WeatherDataCollector:
|
|||||||
- ICON (德国气象局 DWD)
|
- ICON (德国气象局 DWD)
|
||||||
- GEM (加拿大气象局)
|
- GEM (加拿大气象局)
|
||||||
- JMA (日本气象厅)
|
- JMA (日本气象厅)
|
||||||
|
|
||||||
|
返回 3 天的预报数据,支持今日+明日共识分析
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
url = "https://api.open-meteo.com/v1/forecast"
|
url = "https://api.open-meteo.com/v1/forecast"
|
||||||
@@ -663,7 +665,7 @@ class WeatherDataCollector:
|
|||||||
"daily": "temperature_2m_max",
|
"daily": "temperature_2m_max",
|
||||||
"models": models,
|
"models": models,
|
||||||
"timezone": "auto",
|
"timezone": "auto",
|
||||||
"forecast_days": 1,
|
"forecast_days": 3,
|
||||||
"_t": int(time.time()),
|
"_t": int(time.time()),
|
||||||
}
|
}
|
||||||
if use_fahrenheit:
|
if use_fahrenheit:
|
||||||
@@ -678,13 +680,8 @@ class WeatherDataCollector:
|
|||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
# Open-Meteo 多模型返回格式:
|
|
||||||
# "daily": {
|
|
||||||
# "temperature_2m_max_ecmwf_ifs025": [12.3],
|
|
||||||
# "temperature_2m_max_gfs_seamless": [11.8],
|
|
||||||
# ...
|
|
||||||
# }
|
|
||||||
daily = data.get("daily", {})
|
daily = data.get("daily", {})
|
||||||
|
dates = daily.get("time", [])
|
||||||
|
|
||||||
model_labels = {
|
model_labels = {
|
||||||
"ecmwf_ifs025": "ECMWF",
|
"ecmwf_ifs025": "ECMWF",
|
||||||
@@ -694,23 +691,34 @@ class WeatherDataCollector:
|
|||||||
"jma_seamless": "JMA",
|
"jma_seamless": "JMA",
|
||||||
}
|
}
|
||||||
|
|
||||||
forecasts = {}
|
# 按天提取每个模型的预报
|
||||||
for model_key, label in model_labels.items():
|
daily_forecasts = {} # {"2026-02-23": {"ECMWF": 7.9, "GFS": 6.5, ...}, ...}
|
||||||
key = f"temperature_2m_max_{model_key}"
|
for day_idx, date_str in enumerate(dates):
|
||||||
values = daily.get(key, [])
|
day_data = {}
|
||||||
if values and values[0] is not None:
|
for model_key, label in model_labels.items():
|
||||||
forecasts[label] = round(values[0], 1)
|
key = f"temperature_2m_max_{model_key}"
|
||||||
|
values = daily.get(key, [])
|
||||||
|
if day_idx < len(values) and values[day_idx] is not None:
|
||||||
|
day_data[label] = round(values[day_idx], 1)
|
||||||
|
if day_data:
|
||||||
|
daily_forecasts[date_str] = day_data
|
||||||
|
|
||||||
if not forecasts:
|
if not daily_forecasts:
|
||||||
logger.warning("Multi-model: 无有效模型数据")
|
logger.warning("Multi-model: 无有效模型数据")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# 今天的预报 (向后兼容)
|
||||||
|
today_date = dates[0] if dates else None
|
||||||
|
forecasts = daily_forecasts.get(today_date, {})
|
||||||
|
|
||||||
labels_str = ", ".join([f"{k}={v}" for k, v in forecasts.items()])
|
labels_str = ", ".join([f"{k}={v}" for k, v in forecasts.items()])
|
||||||
logger.info(f"🔬 Multi-model ({len(forecasts)}个): {labels_str}")
|
logger.info(f"🔬 Multi-model ({len(forecasts)}个, {len(daily_forecasts)}天): {labels_str}")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"source": "multi_model",
|
"source": "multi_model",
|
||||||
"forecasts": forecasts, # {"ECMWF": 12.3, "GFS": 11.8, ...}
|
"forecasts": forecasts, # 今天 {"ECMWF": 12.3, "GFS": 11.8, ...} (向后兼容)
|
||||||
|
"daily_forecasts": daily_forecasts, # 按天 {"2026-02-23": {...}, "2026-02-24": {...}}
|
||||||
|
"dates": dates,
|
||||||
"unit": "fahrenheit" if use_fahrenheit else "celsius",
|
"unit": "fahrenheit" if use_fahrenheit else "celsius",
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
Reference in New Issue
Block a user