From f7e7e0ea20d05dca693b1f92756d9a5696035995 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Wed, 11 Mar 2026 06:33:25 +0800 Subject: [PATCH] feat: Add multi-source weather data collection with a new Flask API and refine the bot's forecast display logic. --- bot_listener.py | 36 +++++++++++++++++++-- src/data_collection/weather_sources.py | 45 ++++++++++++++++++++++++++ web/app.py | 10 ++++++ 3 files changed, 88 insertions(+), 3 deletions(-) diff --git a/bot_listener.py b/bot_listener.py index 7b3c83a7..7b20d163 100644 --- a/bot_listener.py +++ b/bot_listener.py @@ -464,17 +464,21 @@ def start_bot(): # 今天对比 today_t = _sf(max_temps[0]) if max_temps else None fallback_source = None + metar_only_fallback = False if today_t is None: for source_name, candidate in ( ("MB", mb_high), ("NWS", nws_high), ("MGM", mgm_high), - ("METAR", metar_max_so_far), ): if candidate is not None: today_t = candidate fallback_source = source_name 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 = ( 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: 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: sources = ["N/A"] @@ -534,9 +542,9 @@ def start_bot(): ) # 明后天 + mgm_daily = mgm.get("daily_forecasts", {}) or {} if len(dates) > 1: future_forecasts = [] - mgm_daily = mgm.get("daily_forecasts", {}) or {} for d, t in zip(dates[1:], max_temps[1:]): # 检查 MGM 是否有该日期的预报 mgm_f = mgm_daily.get(d) @@ -547,6 +555,26 @@ def start_bot(): else: future_forecasts.append(f"{d[5:]}: {t}{temp_symbol}") 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 日出日落 + 日照时长 --- sunrises = daily.get("sunrise", []) @@ -831,7 +859,9 @@ def start_bot(): # 构建更全的背景数据给 AI # 补充多模型分歧 - mm = weather_data.get("multi_model", {}) + mm = weather_data.get("multi_model", {}) or {} + if not isinstance(mm, dict): + mm = {} if mm.get("forecasts"): mm_str = " | ".join( [ diff --git a/src/data_collection/weather_sources.py b/src/data_collection/weather_sources.py index 1e63569b..8826faa1 100644 --- a/src/data_collection/weather_sources.py +++ b/src/data_collection/weather_sources.py @@ -1810,6 +1810,12 @@ class WeatherDataCollector: # 严格判断是否为美国市场(必须完全匹配列表或缩写) 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: logger.info(f"🌡️ {city} 使用华氏度 (°F)") else: @@ -1907,6 +1913,35 @@ class WeatherDataCollector: ) if 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: mb_data = self.fetch_from_meteoblue( lat, @@ -1920,6 +1955,16 @@ class WeatherDataCollector: nws_data = self.fetch_nws(lat, lon) if 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: # 降级方案(无经纬度) metar_data = self.fetch_metar(city, use_fahrenheit=use_fahrenheit) diff --git a/web/app.py b/web/app.py index e73e25c9..ca1c437f 100644 --- a/web/app.py +++ b/web/app.py @@ -111,6 +111,16 @@ def _analyze(city: str, force_refresh: bool = False) -> Dict[str, Any]: mgm = raw.get("mgm") or {} ens_raw = raw.get("ensemble", {}) 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, {}) # ── 2. Current conditions (METAR primary, MGM fallback) ──