修复高频机场数据源:AMOS接入airport_primary、Taipei CWA补充mgm_nearby、Paris AROME缓存
- 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 .
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
+34
-16
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user