From 6f83b9d7ead0c4277937b171064540b8d69f9ce6 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Tue, 12 May 2026 23:59:06 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8E=A5=E5=85=A5=E5=B7=B4=E9=BB=8E=20Le=20Bou?= =?UTF-8?q?rget=20AROME=20HD=2015=E5=88=86=E9=92=9F=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E9=A2=84=E6=8A=A5=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Météo-France 站点实测无法获取,改为通过 Open-Meteo 取 AROME France HD 15分钟模型温度。非实测数据,标注为模型预报类型。 巴黎跳过 obs_log 峰值检测(无站点观测日志)。 Constraint: AROME 为模型格点值非实测,与 AMOS/JMA/FMI 精度不同 Tested: pytest 176 passed, AROME API 实测返回 16.2°C --- docs/AIRPORT_REALTIME_SOURCES.md | 2 +- src/analysis/market_alert_engine.py | 2 +- src/bot/runtime_coordinator.py | 2 +- src/utils/telegram_push.py | 37 +++++++++++++++++++++++++---- 4 files changed, 35 insertions(+), 8 deletions(-) diff --git a/docs/AIRPORT_REALTIME_SOURCES.md b/docs/AIRPORT_REALTIME_SOURCES.md index 489fb57c..e5fc3654 100644 --- a/docs/AIRPORT_REALTIME_SOURCES.md +++ b/docs/AIRPORT_REALTIME_SOURCES.md @@ -11,6 +11,7 @@ | 伊斯坦布尔 | 伊斯坦布尔机场 | 17058 | MGM (`servis.mgm.gov.tr`) | 5-15 分钟 | 机场站点实时温度 | 免费 | | 赫尔辛基 | Vantaa | EFHK | FMI (`opendata.fmi.fi`) | 10 分钟 | 机场站点实时温度 | 免费 | | 阿姆斯特丹 | Schiphol | EHAM | KNMI (`dataplatform.knmi.nl`) | 10 分钟 | 机场站点实时温度 | 免费(需注册) | +| 巴黎 | Le Bourget | LFPB | AROME HD (`api.open-meteo.com`) | 15 分钟 | 模型预报(非实测) | 免费 | ## 推送机制 @@ -43,7 +44,6 @@ Seoul / Incheon 16:03 | 城市 | 原因 | |------|------| -| 巴黎/Le Bourget | Météo-France 注册认证失败 | | 马德里/Barajas | AEMET 注册页面失效 | | 伦敦/Heathrow | Met Office 仅 1 小时更新 | | 慕尼黑 | DWD 延迟 ~1 小时 | diff --git a/src/analysis/market_alert_engine.py b/src/analysis/market_alert_engine.py index 1a15e250..b5023677 100644 --- a/src/analysis/market_alert_engine.py +++ b/src/analysis/market_alert_engine.py @@ -813,7 +813,7 @@ def _build_advice_cn( return ",".join(parts) + "。" -_AIRPORT_ICAO_MAP = {"seoul": "RKSI", "busan": "RKPK", "tokyo": "RJTT", "ankara": "17128", "helsinki": "EFHK", "amsterdam": "EHAM", "istanbul": "17058"} +_AIRPORT_ICAO_MAP = {"seoul": "RKSI", "busan": "RKPK", "tokyo": "RJTT", "ankara": "17128", "helsinki": "EFHK", "amsterdam": "EHAM", "istanbul": "17058", "paris": "LFPB"} def _calc_airport_rapid_temp_change( diff --git a/src/bot/runtime_coordinator.py b/src/bot/runtime_coordinator.py index f7a7a8d2..9a0458ee 100644 --- a/src/bot/runtime_coordinator.py +++ b/src/bot/runtime_coordinator.py @@ -190,7 +190,7 @@ class StartupCoordinator: details = { "mode": "airport-periodic", "interval_sec": interval, - "cities": ["seoul", "busan", "tokyo", "ankara", "helsinki", "amsterdam", "istanbul"], + "cities": ["seoul", "busan", "tokyo", "ankara", "helsinki", "amsterdam", "istanbul", "paris"], "chat_targets": len(chat_ids), "window": "DEB proximity ≤3°C", } diff --git a/src/utils/telegram_push.py b/src/utils/telegram_push.py index 5a8d5f1a..783f11eb 100644 --- a/src/utils/telegram_push.py +++ b/src/utils/telegram_push.py @@ -690,8 +690,8 @@ def start_trade_alert_push_loop(bot: Any, config: Dict[str, Any]) -> Optional[th # ── high-freq airport push loop ── -HIGH_FREQ_AIRPORT_CITIES = {"seoul", "busan", "tokyo", "ankara", "helsinki", "amsterdam", "istanbul"} -HIGH_FREQ_AIRPORT_ICAO = {"seoul": "RKSI", "busan": "RKPK", "tokyo": "RJTT", "ankara": "17128", "helsinki": "EFHK", "amsterdam": "EHAM", "istanbul": "17058"} +HIGH_FREQ_AIRPORT_CITIES = {"seoul", "busan", "tokyo", "ankara", "helsinki", "amsterdam", "istanbul", "paris"} +HIGH_FREQ_AIRPORT_ICAO = {"seoul": "RKSI", "busan": "RKPK", "tokyo": "RJTT", "ankara": "17128", "helsinki": "EFHK", "amsterdam": "EHAM", "istanbul": "17058", "paris": "LFPB"} _AIRPORT_PUSH_STATE_PATH = os.path.join( os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), @@ -723,6 +723,26 @@ def _save_airport_state(state: Dict[str, Any]) -> None: os.replace(tmp, path) +def _fetch_arome_temp() -> Optional[float]: + """Fetch latest AROME France HD 15-min temperature for LFPB from Open-Meteo.""" + try: + import requests + url = ( + "https://api.open-meteo.com/v1/forecast?" + "latitude=48.9673&longitude=2.4277" + "&models=meteofrance_arome_france_hd" + "&minutely_15=temperature_2m" + "&timezone=Europe/Paris" + "&forecast_minutely_15=2" + ) + resp = requests.get(url, timeout=8) + data = resp.json() + temps = (data.get("minutely_15") or {}).get("temperature_2m") or [] + return float(temps[-1]) if temps else None + except Exception: + return None + + def _build_airport_status_message( city: str, city_weather: Dict[str, Any], @@ -731,7 +751,7 @@ def _build_airport_status_message( ) -> str: _AIRPORT_EN = {"seoul": "Incheon", "busan": "Gimhae", "tokyo": "Haneda", "ankara": "Esenboğa", "helsinki": "Vantaa", "amsterdam": "Schiphol", - "istanbul": "Airport"} + "istanbul": "Airport", "paris": "Le Bourget"} en_name = city.title() ap_name = _AIRPORT_EN.get(city, "") time_suffix = f" {local_time}" if local_time else "" @@ -792,6 +812,7 @@ _AIRPORT_PUSH_INTERVAL = { "helsinki": 600, # FMI 10-min "amsterdam": 600, # KNMI 10-min "istanbul": 600, # MGM ~10-min + "paris": 900, # AROME HD 15-min model } _DEB_PROXIMITY_THRESHOLD_C = 3.0 @@ -827,6 +848,12 @@ def _run_high_freq_airport_cycle( current = city_weather.get("current") or {} current_temp = current.get("temp") + # Paris: use AROME 15-min model temp instead of METAR + if city == "paris": + arome_temp = _fetch_arome_temp() + if arome_temp is not None: + current_temp = arome_temp + city_weather.setdefault("current", {})["temp"] = arome_temp if current_temp is None or deb_pred is None: continue @@ -834,8 +861,8 @@ def _run_high_freq_airport_cycle( proximity = deb_pred - current_temp in_window = proximity <= _DEB_PROXIMITY_THRESHOLD_C - # Stop if past peak: current below daily max in recent hour and declining - if in_window: + # Stop if past peak (skip for Paris: no obs_log for AROME model data) + if in_window and city != "paris": try: from src.database.db_manager import DBManager icao = HIGH_FREQ_AIRPORT_ICAO.get(city, "")