From da76e98227c48246239915044620ec817c207058 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Sun, 8 Feb 2026 02:35:55 +0800 Subject: [PATCH] feat: introduce multi-source weather data collection module supporting OpenWeatherMap, Visual Crossing, and NOAA METAR. --- bot_listener.py | 17 ++++---- src/data_collection/weather_sources.py | 58 +++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 10 deletions(-) diff --git a/bot_listener.py b/bot_listener.py index bcd7bf50..449368b5 100644 --- a/bot_listener.py +++ b/bot_listener.py @@ -187,20 +187,21 @@ def start_bot(): city_today_str = city_now.strftime("%Y-%m-%d") msg_lines.append(f"\n📊 Open-Meteo 7天预测") - model_split = daily.get("model_split") + nws = weather_data.get("nws", {}) + nws_high = nws.get("today_high") + for i, (d, t) in enumerate(zip(dates[:7], max_temps[:7])): day_label = "今天" if d == city_today_str else d[5:] indicator = "👉 " if d == city_today_str else " " - # 如果是今天且存在模型分歧,则特别标注 - if d == city_today_str and model_split: - ecmwf = model_split.get("ecmwf") - hrrr = model_split.get("hrrr") - if ecmwf and hrrr and abs(ecmwf - hrrr) > 0.5: + # 如果是今天且有 NWS 数据,显示模型对比 + if d == city_today_str and nws_high is not None: + diff = abs(t - nws_high) + if diff > 1: msg_lines.append(f"{indicator}{day_label}: 最高 {t}{temp_symbol} ⚠️") - msg_lines.append(f" (模型分歧: ECMWF {ecmwf} | HRRR {hrrr})") + msg_lines.append(f" (NWS官方预报: {nws_high}{temp_symbol},差异 {diff:.1f}°)") else: - msg_lines.append(f"{indicator}{day_label}: 最高 {t}{temp_symbol}") + msg_lines.append(f"{indicator}{day_label}: 最高 {t}{temp_symbol} (NWS: {nws_high})") else: msg_lines.append(f"{indicator}{day_label}: 最高 {t}{temp_symbol}") diff --git a/src/data_collection/weather_sources.py b/src/data_collection/weather_sources.py index e16b0e03..30eac67d 100644 --- a/src/data_collection/weather_sources.py +++ b/src/data_collection/weather_sources.py @@ -314,6 +314,55 @@ class WeatherDataCollector: logger.error(f"METAR 数据解析失败 ({icao}): {e}") return None + def fetch_nws(self, lat: float, lon: float) -> Optional[Dict]: + """ + 从 NWS (美国国家气象局) 获取高精度预报 + 仅适用于美国城市,全球 VPS 均可访问 + """ + try: + # 1. 获取网格点 + points_url = f"https://api.weather.gov/points/{lat},{lon}" + headers = {"User-Agent": "PolyWeather/1.0 (weather-bot)"} + + points_resp = self.session.get(points_url, headers=headers, timeout=self.timeout) + points_resp.raise_for_status() + points_data = points_resp.json() + + forecast_url = points_data.get("properties", {}).get("forecast") + if not forecast_url: + return None + + # 2. 获取预报 + forecast_resp = self.session.get(forecast_url, headers=headers, timeout=self.timeout) + forecast_resp.raise_for_status() + forecast_data = forecast_resp.json() + + periods = forecast_data.get("properties", {}).get("periods", []) + if not periods: + return None + + # 3. 提取今日最高温(找 isDaytime=True 的第一个) + today_high = None + for p in periods: + if p.get("isDaytime") and "High" in p.get("name", ""): + today_high = p.get("temperature") + break + # 如果没有明确的 High,取第一个 daytime 的温度 + if today_high is None: + for p in periods: + if p.get("isDaytime"): + today_high = p.get("temperature") + break + + return { + "source": "nws", + "today_high": today_high, + "unit": "fahrenheit", + } + except Exception as e: + logger.warning(f"NWS 请求失败: {e}") + return None + def fetch_from_open_meteo( self, lat: float, @@ -343,10 +392,9 @@ class WeatherDataCollector: "_t": int(time.time()), # 禁用缓存,强制刷新 } - # 对于美国市场,使用华氏度并请求更多的模型共识 + # 对于美国市场,使用华氏度 if use_fahrenheit: params["temperature_unit"] = "fahrenheit" - params["models"] = "ecmwf_ifs,hrrr_conus" response = self.session.get( url, @@ -588,6 +636,12 @@ class WeatherDataCollector: metar_data = self.fetch_metar(city, use_fahrenheit=use_fahrenheit, utc_offset=utc_offset) if metar_data: results["metar"] = metar_data + + # 对美国城市,额外获取 NWS 高精预报 + if use_fahrenheit: + nws_data = self.fetch_nws(lat, lon) + if nws_data: + results["nws"] = nws_data else: # 降级方案 metar_data = self.fetch_metar(city, use_fahrenheit=use_fahrenheit)