Tel Aviv 机场观测切换至 IMS Lod Airport 实时数据源

新增 IMS (Israel Meteorological Service) 数据抓取模块,以 Lod Airport
(station 225) 官方观测替代 NOAA METAR LLBG 作为 airport_primary。
IMS 数据接入 _airport_primary_from_raw 优先级链,高于 plain METAR
但保留 METAR 集群回退路径。

Constraint: IMS API (ims.gov.il/en/hourly_observations) 仅每小时更新
Confidence: high
Scope-risk: 仅影响 Tel Aviv 城市,其余 51 城数据流不变
Tested: 端到端验证 IMS 取数 → airport_primary → provider 路由链
This commit is contained in:
2569718930@qq.com
2026-05-22 19:16:34 +08:00
parent 79b33599ac
commit 60ac4bae31
4 changed files with 208 additions and 2 deletions
+100
View File
@@ -0,0 +1,100 @@
from __future__ import annotations
from datetime import datetime
from typing import Dict, Optional
from loguru import logger
from src.utils.metrics import record_source_call
class ImsSourceMixin:
"""Fetch realtime observations from Israel Meteorological Service (IMS).
The IMS public API at /en/hourly_observations returns a JSON payload with
data.hourly_observations_map keyed by timestamp and station ID, covering all
active IMS stations for the current day.
Station 225 = Lod Airport (Ben Gurion / LLBG), elevation 40 m.
"""
IMS_LOD_AIRPORT_STATION = "225"
IMS_OBSERVATIONS_URL = "https://ims.gov.il/en/hourly_observations"
def fetch_from_ims(self, station_id: str = "225") -> Optional[Dict]:
started = datetime.now()
def _elapsed_ms() -> float:
return (datetime.now() - started).total_seconds() * 1000.0
try:
resp = self._http_get(self.IMS_OBSERVATIONS_URL, timeout=self.timeout)
if resp.status_code != 200:
logger.warning("IMS API returned HTTP {}", resp.status_code)
record_source_call("ims", "station", "error", _elapsed_ms())
return None
body = resp.json()
obs_map = (body.get("data") or {}).get("hourly_observations_map") or {}
if not obs_map:
record_source_call("ims", "station", "empty", _elapsed_ms())
return None
latest_time = max(obs_map.keys())
latest = obs_map[latest_time].get(station_id) or {}
if not latest:
record_source_call("ims", "station", "empty", _elapsed_ms())
return None
def _f(key: str) -> Optional[float]:
raw = latest.get(key)
if raw is None:
return None
try:
return float(raw)
except (ValueError, TypeError):
return None
temp = _f("TT")
rh = _f("RH")
wind_kmh = _f("FF")
wind_dir = _f("DD")
tx1 = _f("TX1")
tn1 = _f("TN1")
td = _f("TD")
wind_kt = round(wind_kmh / 1.852, 1) if wind_kmh is not None else None
result: Dict = {
"current": {
"temp": temp,
"humidity": rh,
"wind_speed_kmh": wind_kmh,
"wind_speed_kt": wind_kt,
"wind_dir": wind_dir,
"dew_point": td,
"max_temp_so_far": tx1,
"min_temp_so_far": tn1,
},
"obs_time": latest_time,
"station_id": station_id,
"station_label": "Lod Airport",
"lat": 32.002943,
"lon": 34.891534,
"elevation_m": 40,
}
record_source_call("ims", "station", "success", _elapsed_ms())
logger.info(
"IMS Lod Airport (s{}) {}: temp={}°C RH={}% wind={}km/h",
station_id,
latest_time,
temp,
rh,
wind_kmh,
)
return result
except Exception:
logger.exception("IMS fetch failed for station {}", station_id)
record_source_call("ims", "station", "error", _elapsed_ms())
return None