From 3bca2093f096f775cc486522e071c883072fb8f6 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Fri, 15 May 2026 00:09:46 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=AB=98=E9=A2=91=E6=9C=BA?= =?UTF-8?q?=E5=9C=BA=E6=95=B0=E6=8D=AE=E6=BA=90=EF=BC=9AAMOS=E6=8E=A5?= =?UTF-8?q?=E5=85=A5airport=5Fprimary=E3=80=81Taipei=20CWA=E8=A1=A5?= =?UTF-8?q?=E5=85=85mgm=5Fnearby=E3=80=81Paris=20AROME=E7=BC=93=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Seoul/Busan: AMOS 跑道传感器纳入 airport_primary 链路(MADIS之后、MGM之前) - Seoul/Busan: 跑道温度生效时同步更新 current_obs_time,去重不再依赖慢速METAR - Taipei: 新增 _attach_cwa_settlement_nearby,将CWA数据注入 mgm_nearby(icao=RCSS) - Paris: AROME HD 抓取新增 10分钟内存缓存(模型15分钟更新),obs_time 为空时补 UTC 时间 - Istanbul/Ankara MGM 链接修复为直接跳转机场站点页面 Scope-risk: LOW — 170 测试通过,ruff 零告警 Tested: python -m pytest -q (170 passed), ruff check . --- src/data_collection/country_networks.py | 29 ++++++++++++++ src/data_collection/weather_sources.py | 34 +++++++++++++++++ src/utils/telegram_push.py | 50 +++++++++++++++++-------- 3 files changed, 97 insertions(+), 16 deletions(-) diff --git a/src/data_collection/country_networks.py b/src/data_collection/country_networks.py index c0013686..ef4d39e6 100644 --- a/src/data_collection/country_networks.py +++ b/src/data_collection/country_networks.py @@ -273,6 +273,35 @@ def _airport_primary_from_raw(city: str, raw: Dict[str, Any]) -> Dict[str, Any]: }, ) + amos = raw.get("amos") or {} + if amos.get("temp_c") is not None: + return _normalize_station_row( + station_code=amos.get("icao") or meta.get("icao"), + station_label=amos.get("station_label") or meta.get("airport_name") or meta.get("icao"), + temp=amos["temp_c"], + obs_time=amos.get("obs_time") or metar.get("observation_time"), + source_code="amos", + source_label="AMOS", + is_official=True, + is_airport_station=True, + is_settlement_anchor=False, + extra={ + "max_so_far": _safe_float(current.get("max_temp_so_far")), + "max_temp_time": current.get("max_temp_time"), + "obs_age_min": None, + "report_time": metar.get("report_time"), + "receipt_time": metar.get("receipt_time"), + "obs_time_epoch": metar.get("obs_time_epoch"), + "obs_time_utc_offset_seconds": 0, + "wind_speed_kt": _safe_float(current.get("wind_speed_kt")), + "wind_dir": _safe_float(current.get("wind_dir")), + "humidity": _safe_float(current.get("humidity")), + "visibility_mi": _safe_float(current.get("visibility_mi")), + "wx_desc": current.get("wx_desc"), + "raw_metar": current.get("raw_metar"), + }, + ) + mgm = raw.get("mgm") or {} mgm_current = mgm.get("current") or {} if mgm_current.get("temp") is not None: diff --git a/src/data_collection/weather_sources.py b/src/data_collection/weather_sources.py index 13461212..91f77296 100644 --- a/src/data_collection/weather_sources.py +++ b/src/data_collection/weather_sources.py @@ -1018,6 +1018,38 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour except Exception: logger.exception("airport_obs_log append failed for hko_obs city={}", city_lower) + def _attach_cwa_settlement_nearby( + self, results: Dict, city_lower: str, use_fahrenheit: bool + ) -> None: + if city_lower != "taipei": + return + sc = results.get("settlement_current") or {} + if not sc: + return + current = sc.get("current") or {} + temp = current.get("temp") + if temp is None: + return + row = { + "station_code": sc.get("station_code") or "466920", + "station_label": sc.get("station_name") or "Taipei CWA", + "temp": temp, + "obs_time": sc.get("observation_time") or "", + "source_code": "cwa", + "source_label": "CWA", + "icao": "RCSS", + "is_official": True, + "is_airport_station": True, + "is_settlement_anchor": False, + "max_so_far": current.get("max_temp_so_far"), + "max_temp_time": current.get("max_temp_time"), + "humidity": current.get("humidity"), + "wind_speed_kt": current.get("wind_speed_kt"), + "wind_dir": current.get("wind_dir"), + } + results["mgm_nearby"] = [row] + results["nearby_source"] = "cwa" + def _attach_korean_amos_data( self, results: Dict, city_lower: str, use_fahrenheit: bool ) -> None: @@ -1262,6 +1294,7 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour self._attach_fmi_official_nearby(results, city_lower, use_fahrenheit) self._attach_knmi_official_nearby(results, city_lower, use_fahrenheit) self._attach_hko_obs_official_nearby(results, city_lower, use_fahrenheit) + self._attach_cwa_settlement_nearby(results, city_lower, use_fahrenheit) self._attach_russia_official_nearby(results, city_lower, use_fahrenheit) if city_lower == "warsaw": self._attach_warsaw_official_nearby(results, use_fahrenheit) @@ -1309,6 +1342,7 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour self._attach_fmi_official_nearby(results, city_lower, use_fahrenheit) self._attach_knmi_official_nearby(results, city_lower, use_fahrenheit) self._attach_hko_obs_official_nearby(results, city_lower, use_fahrenheit) + self._attach_cwa_settlement_nearby(results, city_lower, use_fahrenheit) self._attach_russia_official_nearby(results, city_lower, use_fahrenheit) if city_lower == "warsaw": self._attach_warsaw_official_nearby(results, use_fahrenheit) diff --git a/src/utils/telegram_push.py b/src/utils/telegram_push.py index 552babd3..5261ca31 100644 --- a/src/utils/telegram_push.py +++ b/src/utils/telegram_push.py @@ -518,8 +518,19 @@ def _save_airport_state(state: Dict[str, Any]) -> None: os.replace(tmp, path) +_AROME_CACHE: Dict[str, Any] = {} +_AROME_CACHE_TTL_SEC = 600 # AROME HD updates every 15 min; cache 10 min + def _fetch_arome_temp() -> Optional[float]: - """Fetch latest AROME France HD 15-min temperature for LFPB from Open-Meteo.""" + """Fetch latest AROME France HD 15-min temperature for LFPB from Open-Meteo. + + Cached for 10 minutes since the model only updates every 15 minutes. + """ + now = time.time() + cached = _AROME_CACHE.get("value") + cached_at = _AROME_CACHE.get("ts", 0) + if cached is not None and (now - cached_at) < _AROME_CACHE_TTL_SEC: + return cached try: import requests url = ( @@ -533,9 +544,12 @@ def _fetch_arome_temp() -> Optional[float]: 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 + result = float(temps[-1]) if temps else None + _AROME_CACHE["value"] = result + _AROME_CACHE["ts"] = now + return result except Exception: - return None + return _AROME_CACHE.get("value") def _build_airport_status_message( @@ -622,19 +636,19 @@ def _get_airport_daily_high(city_weather: Dict[str, Any]): return max_so_far, max_time -# Per-city push interval matching native data refresh rate (seconds) +# Per-city push interval — unified to 60s, obs_time dedup prevents spam _AIRPORT_PUSH_INTERVAL = { - "seoul": 600, # AMOS 1-min → 10min push - "busan": 600, # AMOS 1-min → 10min push - "tokyo": 600, # JMA 10-min - "ankara": 600, # MGM ~10-min - "helsinki": 600, # FMI 10-min - "amsterdam": 600, # KNMI 10-min - "istanbul": 600, # MGM ~10-min - "paris": 900, # AROME HD 15-min model - "hong kong": 60, # HKO 10-min → 60s轮询,obs_time去重 - "lau fau shan": 60, # HKO 10-min → 60s轮询,obs_time去重 - "taipei": 600, # CWA ~10-min + "seoul": 60, + "busan": 60, + "tokyo": 60, + "ankara": 60, + "helsinki": 60, + "amsterdam": 60, + "istanbul": 60, + "paris": 60, + "hong kong": 60, + "lau fau shan": 60, + "taipei": 60, } # Per-city temperature window threshold (°C below DEB predicted high) # Continental airports: wider window (temp rises steadily over land) @@ -749,7 +763,9 @@ def _run_high_freq_airport_cycle( valid_temps = [t for (t, _d) in runway_temps if t is not None] if valid_temps: station_temp = max(valid_temps) - # 跑道数据没有独立的 obs_time,保持 current_obs_time + amos_obs_time = amos.get("observation_time") or "" + if amos_obs_time: + current_obs_time = amos_obs_time current_temp = station_temp if current_temp is None: @@ -759,6 +775,8 @@ def _run_high_freq_airport_cycle( if arome_temp is not None: current_temp = arome_temp city_weather.setdefault("current", {})["temp"] = arome_temp + if not current_obs_time: + current_obs_time = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S") if current_temp is None or deb_pred is None: continue