diff --git a/bot_listener.py b/bot_listener.py index 1d8fb916..bcd7bf50 100644 --- a/bot_listener.py +++ b/bot_listener.py @@ -13,7 +13,7 @@ from src.utils.config_loader import load_config from src.data_collection.weather_sources import WeatherDataCollector def analyze_weather_trend(weather_data, temp_symbol): - """根据实测与预测分析气温态势""" + """根据实测与预测分析气温态势,增加峰值时刻预测""" insights = [] metar = weather_data.get("metar", {}) @@ -23,46 +23,70 @@ def analyze_weather_trend(weather_data, temp_symbol): return "" curr_temp = metar.get("current", {}).get("temp") - forecast_high = open_meteo.get("daily", {}).get("temperature_2m_max", [None])[0] + daily = open_meteo.get("daily", {}) + forecast_high = daily.get("temperature_2m_max", [None])[0] wind_speed = metar.get("current", {}).get("wind_speed_kt", 0) # 获取当地时间小时 - local_time_str = open_meteo.get("current", {}).get("local_time", "") + local_time_full = open_meteo.get("current", {}).get("local_time", "") try: - local_hour = int(local_time_str.split(" ")[1].split(":")[0]) + local_date_str = local_time_full.split(" ")[0] # YYYY-MM-DD + local_hour = int(local_time_full.split(" ")[1].split(":")[0]) except: - local_hour = datetime.now().hour # 降级方案 + local_date_str = datetime.now().strftime("%Y-%m-%d") + local_hour = datetime.now().hour + + # --- 增加:峰值时刻预测逻辑 --- + hourly = open_meteo.get("hourly", {}) + times = hourly.get("time", []) + # 优先寻找高精模型的逐小时数据 + temps = hourly.get("temperature_2m_hrrr_conus") or hourly.get("temperature_2m_ecmwf_ifs") or hourly.get("temperature_2m", []) + + peak_hours = [] + if times and temps and forecast_high is not None: + for t_str, temp in zip(times, temps): + if t_str.startswith(local_date_str): + # 记录所有接近最高温的小时 (容差 0.2) + if abs(temp - forecast_high) <= 0.2: + hour = t_str.split("T")[1][:5] + peak_hours.append(hour) + if peak_hours: + window = f"{peak_hours[0]} - {peak_hours[-1]}" if len(peak_hours) > 1 else peak_hours[0] + insights.append(f"⏱️ 预计峰值时刻:今天 {window} 之间。") + if local_hour < int(peak_hours[0].split(":")[0]): + insights.append(f"🎯 博弈建议:关注该时段实测能否站稳 {forecast_high}{temp_symbol}。") + if curr_temp is not None and forecast_high is not None: diff = forecast_high - curr_temp # 1. 气温节奏判定 - if local_hour >= 16: + if local_hour >= 17: if curr_temp >= forecast_high - 0.5: - insights.append(f"✅ 今日峰值已达:当前 {curr_temp}{temp_symbol} 已触及预报最高温,后续将进入回落通道。") + insights.append(f"✅ 今日峰值已达:当前已触及预报最高,大概率已定格。") else: - insights.append(f"📉 处于降温期:气温已开始从峰值下滑,今日大概率不会再反弹。") - elif 11 <= local_hour < 16: - if diff > 1.5: - insights.append(f"📈 升温进程中:距离预报最高温还有约 {diff:.1f}° 空间,午后余热尚存。") + insights.append(f"📉 处于降温期:气温已跌落峰值,今日反弹乏力。") + elif 10 <= local_hour < 17: + if diff > 1.2: + insights.append(f"📈 升温进程中:距离峰值还有约 {diff:.1f}° 空间,正向高点冲击。") else: - insights.append(f"⚖️ 高位横盘:气温已基本涨满,将在当前水平小幅波动,直至日落。") + insights.append(f"⚖️ 高位横盘:气温已在高位,将在当前水平小幅波动。") else: - insights.append(f"🌅 早间爬升:气温正在起步。") + insights.append(f"🌅 早间爬升:气温正快速起步,等待午后冲击。") # 2. 湿度与露点带来的“粘性”分析 humidity = metar.get("current", {}).get("humidity") dewpoint = metar.get("current", {}).get("dewpoint") - if humidity and humidity > 80: - insights.append(f"💦 闷热高湿:空气湿度极大 ({humidity}%),这会像保温层一样锁住热量,导致夜间降温非常缓慢。") + if humidity and humidity > 80 and local_hour >= 18: + insights.append(f"💦 闷热高湿:湿度极高 ({humidity}%),将显著锁住夜间热量。") if dewpoint is not None and curr_temp - dewpoint < 2.0 and local_hour >= 18: - insights.append(f"🌡️ 触及露点底线:气温已非常接近露点,进一步下降的空间将被强力压缩,气温将“跌不动了”。") + insights.append(f"🌡️ 触及露点支撑:气温已跌至露点支撑位,降温将变慢。") - # 3. 风力带来的剧烈波动预警 + # 3. 风力 if wind_speed >= 15: - insights.append(f"🌬️ 大风预警 ({wind_speed}kt):强风可能带来锋面过境,注意气温可能出现非正常的剧烈跳变。") + insights.append(f"🌬️ 大风预判:当前风力较大 ({wind_speed}kt),气温可能出现非线性波动。") elif wind_speed >= 10: insights.append(f"🍃 清劲风:空气流动快,虽然有助于散热,但可能伴随阵风引起微小波动。") @@ -163,10 +187,22 @@ 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") 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 " " - msg_lines.append(f"{indicator}{day_label}: 最高 {t}{temp_symbol}") + + # 如果是今天且存在模型分歧,则特别标注 + 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: + msg_lines.append(f"{indicator}{day_label}: 最高 {t}{temp_symbol} ⚠️") + msg_lines.append(f" (模型分歧: ECMWF {ecmwf} | HRRR {hrrr})") + else: + msg_lines.append(f"{indicator}{day_label}: 最高 {t}{temp_symbol}") + else: + msg_lines.append(f"{indicator}{day_label}: 最高 {t}{temp_symbol}") if metar: icao = metar.get("icao", "") diff --git a/src/data_collection/weather_sources.py b/src/data_collection/weather_sources.py index 35a4b682..5b6906fc 100644 --- a/src/data_collection/weather_sources.py +++ b/src/data_collection/weather_sources.py @@ -343,9 +343,10 @@ class WeatherDataCollector: "_t": int(time.time()), # 禁用缓存,强制刷新 } - # 对于美国市场,使用华氏度 + # 对于美国市场,使用华氏度并请求更多的模型共识 if use_fahrenheit: params["temperature_unit"] = "fahrenheit" + params["models"] = "ecmwf_ifs,hrrr_conus" response = self.session.get( url, @@ -360,7 +361,23 @@ class WeatherDataCollector: utc_offset = data.get("utc_offset_seconds", 0) timezone_name = data.get("timezone", "UTC") - # 计算精确的当地时间而不是气象站 bucket 时间 + # 处理多模型数据 (如果请求了 models 参数,返回结构会变化) + daily_data = data.get("daily", {}) + if "temperature_2m_max_ecmwf_ifs" in daily_data: + # 获取首日的各模型峰值比较 + ecmwf_max = daily_data.get("temperature_2m_max_ecmwf_ifs", []) + hrrr_max = daily_data.get("temperature_2m_max_hrrr_conus", []) + + # 记录多模型分歧 + daily_data["model_split"] = { + "ecmwf": ecmwf_max[0] if ecmwf_max else None, + "hrrr": hrrr_max[0] if hrrr_max else None + } + # 设置主显示值为 HRRR (当地高精) + if hrrr_max: + daily_data["temperature_2m_max"] = hrrr_max + + # 计算精确的当地时间 now_utc = datetime.utcnow() local_now = now_utc + timedelta(seconds=utc_offset) local_time_str = local_now.strftime("%Y-%m-%d %H:%M") @@ -375,7 +392,7 @@ class WeatherDataCollector: "local_time": local_time_str, }, "hourly": data.get("hourly", {}), - "daily": data.get("daily", {}), + "daily": daily_data, "unit": "fahrenheit" if use_fahrenheit else "celsius", } except Exception as e: