feat: Add multi-source weather data collection with a new Flask API and refine the bot's forecast display logic.
This commit is contained in:
+33
-3
@@ -464,17 +464,21 @@ def start_bot():
|
|||||||
# 今天对比
|
# 今天对比
|
||||||
today_t = _sf(max_temps[0]) if max_temps else None
|
today_t = _sf(max_temps[0]) if max_temps else None
|
||||||
fallback_source = None
|
fallback_source = None
|
||||||
|
metar_only_fallback = False
|
||||||
if today_t is None:
|
if today_t is None:
|
||||||
for source_name, candidate in (
|
for source_name, candidate in (
|
||||||
("MB", mb_high),
|
("MB", mb_high),
|
||||||
("NWS", nws_high),
|
("NWS", nws_high),
|
||||||
("MGM", mgm_high),
|
("MGM", mgm_high),
|
||||||
("METAR", metar_max_so_far),
|
|
||||||
):
|
):
|
||||||
if candidate is not None:
|
if candidate is not None:
|
||||||
today_t = candidate
|
today_t = candidate
|
||||||
fallback_source = source_name
|
fallback_source = source_name
|
||||||
break
|
break
|
||||||
|
if today_t is None and metar_max_so_far is not None:
|
||||||
|
# Last-resort display only: do not treat METAR as a forecast source
|
||||||
|
today_t = metar_max_so_far
|
||||||
|
metar_only_fallback = True
|
||||||
today_t_display = (
|
today_t_display = (
|
||||||
f"{today_t:.1f}" if isinstance(today_t, (int, float)) else "N/A"
|
f"{today_t:.1f}" if isinstance(today_t, (int, float)) else "N/A"
|
||||||
)
|
)
|
||||||
@@ -511,6 +515,10 @@ def start_bot():
|
|||||||
)
|
)
|
||||||
if fallback_source and fallback_source not in sources:
|
if fallback_source and fallback_source not in sources:
|
||||||
sources.append(fallback_source)
|
sources.append(fallback_source)
|
||||||
|
if metar_only_fallback:
|
||||||
|
if not sources:
|
||||||
|
sources = ["Model unavailable"]
|
||||||
|
comp_parts.append(f"METAR实测回退: {metar_max_so_far:.1f}{temp_symbol}")
|
||||||
if not sources:
|
if not sources:
|
||||||
sources = ["N/A"]
|
sources = ["N/A"]
|
||||||
|
|
||||||
@@ -534,9 +542,9 @@ def start_bot():
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 明后天
|
# 明后天
|
||||||
|
mgm_daily = mgm.get("daily_forecasts", {}) or {}
|
||||||
if len(dates) > 1:
|
if len(dates) > 1:
|
||||||
future_forecasts = []
|
future_forecasts = []
|
||||||
mgm_daily = mgm.get("daily_forecasts", {}) or {}
|
|
||||||
for d, t in zip(dates[1:], max_temps[1:]):
|
for d, t in zip(dates[1:], max_temps[1:]):
|
||||||
# 检查 MGM 是否有该日期的预报
|
# 检查 MGM 是否有该日期的预报
|
||||||
mgm_f = mgm_daily.get(d)
|
mgm_f = mgm_daily.get(d)
|
||||||
@@ -547,6 +555,26 @@ def start_bot():
|
|||||||
else:
|
else:
|
||||||
future_forecasts.append(f"{d[5:]}: {t}{temp_symbol}")
|
future_forecasts.append(f"{d[5:]}: {t}{temp_symbol}")
|
||||||
msg_lines.append("📅 " + " | ".join(future_forecasts))
|
msg_lines.append("📅 " + " | ".join(future_forecasts))
|
||||||
|
elif mgm_daily:
|
||||||
|
# Open-Meteo missing: still show next 2 days from MGM daily forecast
|
||||||
|
from datetime import datetime, timezone, timedelta
|
||||||
|
|
||||||
|
local_now = datetime.now(timezone.utc).astimezone(
|
||||||
|
timezone(timedelta(seconds=int(fallback_utc_offset)))
|
||||||
|
)
|
||||||
|
today_local = local_now.strftime("%Y-%m-%d")
|
||||||
|
future_forecasts = []
|
||||||
|
for d in sorted(mgm_daily.keys()):
|
||||||
|
if d <= today_local:
|
||||||
|
continue
|
||||||
|
t = mgm_daily.get(d)
|
||||||
|
if t is None:
|
||||||
|
continue
|
||||||
|
future_forecasts.append(f"{d[5:]}: {t}{temp_symbol}")
|
||||||
|
if len(future_forecasts) >= 2:
|
||||||
|
break
|
||||||
|
if future_forecasts:
|
||||||
|
msg_lines.append("📅 " + " | ".join(future_forecasts))
|
||||||
|
|
||||||
# --- 3.5 日出日落 + 日照时长 ---
|
# --- 3.5 日出日落 + 日照时长 ---
|
||||||
sunrises = daily.get("sunrise", [])
|
sunrises = daily.get("sunrise", [])
|
||||||
@@ -831,7 +859,9 @@ def start_bot():
|
|||||||
# 构建更全的背景数据给 AI
|
# 构建更全的背景数据给 AI
|
||||||
|
|
||||||
# 补充多模型分歧
|
# 补充多模型分歧
|
||||||
mm = weather_data.get("multi_model", {})
|
mm = weather_data.get("multi_model", {}) or {}
|
||||||
|
if not isinstance(mm, dict):
|
||||||
|
mm = {}
|
||||||
if mm.get("forecasts"):
|
if mm.get("forecasts"):
|
||||||
mm_str = " | ".join(
|
mm_str = " | ".join(
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -1810,6 +1810,12 @@ class WeatherDataCollector:
|
|||||||
# 严格判断是否为美国市场(必须完全匹配列表或缩写)
|
# 严格判断是否为美国市场(必须完全匹配列表或缩写)
|
||||||
use_fahrenheit = city_lower in us_cities
|
use_fahrenheit = city_lower in us_cities
|
||||||
|
|
||||||
|
# Turkish cities: keep MGM model fallback alive when Open-Meteo is rate-limited.
|
||||||
|
turkish_provinces = {
|
||||||
|
"ankara": ("17128", "Ankara"), # settlement reference: Esenboğa airport
|
||||||
|
"istanbul": ("17060", "Istanbul"),
|
||||||
|
}
|
||||||
|
|
||||||
if use_fahrenheit:
|
if use_fahrenheit:
|
||||||
logger.info(f"🌡️ {city} 使用华氏度 (°F)")
|
logger.info(f"🌡️ {city} 使用华氏度 (°F)")
|
||||||
else:
|
else:
|
||||||
@@ -1907,6 +1913,35 @@ class WeatherDataCollector:
|
|||||||
)
|
)
|
||||||
if metar_data:
|
if metar_data:
|
||||||
results["metar"] = metar_data
|
results["metar"] = metar_data
|
||||||
|
|
||||||
|
# Turkish fallback: keep MGM forecasts and nearby stations available
|
||||||
|
if city_lower in turkish_provinces:
|
||||||
|
istno, province = turkish_provinces[city_lower]
|
||||||
|
mgm_data = self.fetch_from_mgm(istno)
|
||||||
|
if istno == "17128":
|
||||||
|
mgm_city_center = self.fetch_from_mgm("17130")
|
||||||
|
if mgm_city_center and mgm_data:
|
||||||
|
mgm_data["today_high"] = mgm_city_center.get("today_high")
|
||||||
|
mgm_data["daily_forecasts"] = mgm_city_center.get(
|
||||||
|
"daily_forecasts"
|
||||||
|
)
|
||||||
|
if mgm_data:
|
||||||
|
results["mgm"] = mgm_data
|
||||||
|
nearby = self.fetch_mgm_nearby_stations(
|
||||||
|
province, root_ist_no=istno
|
||||||
|
)
|
||||||
|
if nearby:
|
||||||
|
results["mgm_nearby"] = nearby
|
||||||
|
|
||||||
|
# Global nearby fallback from METAR clusters
|
||||||
|
if city_lower in self.CITY_METAR_CLUSTERS and "mgm_nearby" not in results:
|
||||||
|
cluster_icaos = self.CITY_METAR_CLUSTERS[city_lower]
|
||||||
|
cluster_data = self.fetch_metar_nearby_cluster(
|
||||||
|
cluster_icaos, use_fahrenheit=use_fahrenheit
|
||||||
|
)
|
||||||
|
if cluster_data:
|
||||||
|
results["mgm_nearby"] = cluster_data
|
||||||
|
|
||||||
if city_lower in self.METEOBLUE_PRIORITY_CITIES:
|
if city_lower in self.METEOBLUE_PRIORITY_CITIES:
|
||||||
mb_data = self.fetch_from_meteoblue(
|
mb_data = self.fetch_from_meteoblue(
|
||||||
lat,
|
lat,
|
||||||
@@ -1920,6 +1955,16 @@ class WeatherDataCollector:
|
|||||||
nws_data = self.fetch_nws(lat, lon)
|
nws_data = self.fetch_nws(lat, lon)
|
||||||
if nws_data:
|
if nws_data:
|
||||||
results["nws"] = nws_data
|
results["nws"] = nws_data
|
||||||
|
|
||||||
|
# Still try ensemble / multi-model from stale cache while OM is cooling down
|
||||||
|
ens_data = self.fetch_ensemble(lat, lon, use_fahrenheit=use_fahrenheit)
|
||||||
|
if ens_data:
|
||||||
|
results["ensemble"] = ens_data
|
||||||
|
mm_data = self.fetch_multi_model(
|
||||||
|
lat, lon, use_fahrenheit=use_fahrenheit
|
||||||
|
)
|
||||||
|
if mm_data:
|
||||||
|
results["multi_model"] = mm_data
|
||||||
else:
|
else:
|
||||||
# 降级方案(无经纬度)
|
# 降级方案(无经纬度)
|
||||||
metar_data = self.fetch_metar(city, use_fahrenheit=use_fahrenheit)
|
metar_data = self.fetch_metar(city, use_fahrenheit=use_fahrenheit)
|
||||||
|
|||||||
+10
@@ -111,6 +111,16 @@ def _analyze(city: str, force_refresh: bool = False) -> Dict[str, Any]:
|
|||||||
mgm = raw.get("mgm") or {}
|
mgm = raw.get("mgm") or {}
|
||||||
ens_raw = raw.get("ensemble", {})
|
ens_raw = raw.get("ensemble", {})
|
||||||
mm = raw.get("multi_model", {})
|
mm = raw.get("multi_model", {})
|
||||||
|
if not isinstance(om, dict):
|
||||||
|
om = {}
|
||||||
|
if not isinstance(metar, dict):
|
||||||
|
metar = {}
|
||||||
|
if not isinstance(mgm, dict):
|
||||||
|
mgm = {}
|
||||||
|
if not isinstance(ens_raw, dict):
|
||||||
|
ens_raw = {}
|
||||||
|
if not isinstance(mm, dict):
|
||||||
|
mm = {}
|
||||||
risk = CITY_RISK_PROFILES.get(city, {})
|
risk = CITY_RISK_PROFILES.get(city, {})
|
||||||
|
|
||||||
# ── 2. Current conditions (METAR primary, MGM fallback) ──
|
# ── 2. Current conditions (METAR primary, MGM fallback) ──
|
||||||
|
|||||||
Reference in New Issue
Block a user