891b4d2422
AEROWEB: 接入 aviation.meteo.fr,LFPB METAR 2 分钟内可获取, 实测温度替代 AROME 模式预报作为 Paris 电报推送数据源。 登录流程: PHPSESSID + MD5 密码 → ajax/login_valid.php, Session 20 分钟自动续期,XML 解析提取 tempe/td/dd/ff/qnh。 NCM: 沙特气象局 Meteomatics API 代码框架,待凭证激活。 Jeddah settlement_source 从废弃的 wunderground 迁移至 ncm。 同时清理: 日志文件 trading_system.log → polyweather.log, 删除 2026-02-07 的 1.2MB 废弃交易引擎日志。 Constraint: AEROWEB 需 AEROWEB_USERNAME/AEROWEB_PASSWORD 环境变量 Constraint: NCM 需 NCM_API_USERNAME/NCM_API_PASSWORD 环境变量 Confidence: high Tested: AEROWEB 登录→取数→解析端到端验证通过
128 lines
4.9 KiB
Python
128 lines
4.9 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
from datetime import datetime
|
|
from typing import Dict, Optional
|
|
|
|
from loguru import logger
|
|
|
|
from src.utils.metrics import record_source_call
|
|
|
|
|
|
class NcmSourceMixin:
|
|
"""Fetch realtime observations from Saudi NCM via the Meteomatics API.
|
|
|
|
API base: https://api-mm.ncm.gov.sa/
|
|
Docs: https://api-doc.ncm.gov.sa/en/api/getting-started
|
|
|
|
Auth via HTTP Basic Auth: username + password (set in .env as
|
|
NCM_API_USERNAME / NCM_API_PASSWORD).
|
|
|
|
Station for Jeddah: "Mataar Municipality" (ID 36) near OEJN.
|
|
Uses source=mix-obs for real-time station observations.
|
|
|
|
Parameters:
|
|
t_2m:C — temperature at 2 m (°C)
|
|
wind_speed_10m:ms — wind speed at 10 m (m/s)
|
|
sfc_pressure:hPa — surface pressure (hPa)
|
|
dew_point_2m:C — dew point at 2 m (°C)
|
|
relative_humidity_2m:p — relative humidity (%)
|
|
"""
|
|
|
|
NCM_API_BASE = "https://api-mm.ncm.gov.sa"
|
|
# Station 36 = Mataar Municipality (21.60 N, 39.25 E), closest to OEJN.
|
|
# Use lat/lon for automatic nearest-station routing.
|
|
JEDDAH_LAT = "21.6702"
|
|
JEDDAH_LON = "39.1525"
|
|
JEDDAH_STATION_ID = "36"
|
|
PARAMS = "t_2m:C,wind_speed_10m:ms,sfc_pressure:hPa,dew_point_2m:C,relative_humidity_2m:p"
|
|
|
|
def _ncm_auth(self) -> Optional[tuple]:
|
|
username = os.getenv("NCM_API_USERNAME", "").strip()
|
|
password = os.getenv("NCM_API_PASSWORD", "").strip()
|
|
if not username or not password:
|
|
return None
|
|
return (username, password)
|
|
|
|
def fetch_from_ncm(self) -> Optional[Dict]:
|
|
"""Fetch latest observation for Jeddah from NCM."""
|
|
started = datetime.now()
|
|
auth = self._ncm_auth()
|
|
if not auth:
|
|
logger.warning("NCM credentials not configured (NCM_API_USERNAME / NCM_API_PASSWORD)")
|
|
record_source_call("ncm", "station", "noauth", 0)
|
|
return None
|
|
|
|
def _elapsed_ms() -> float:
|
|
return (datetime.now() - started).total_seconds() * 1000.0
|
|
|
|
now_utc = datetime.utcnow()
|
|
valid_time = now_utc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
location = f"{self.JEDDAH_LAT},{self.JEDDAH_LON}"
|
|
url = f"{self.NCM_API_BASE}/{valid_time}/{self.PARAMS}/{location}/json?source=mix-obs"
|
|
|
|
try:
|
|
resp = self._http_get(url, auth=auth, timeout=self.timeout)
|
|
if resp.status_code == 401:
|
|
logger.error("NCM API authentication failed — check NCM_API_USERNAME / NCM_API_PASSWORD")
|
|
record_source_call("ncm", "station", "auth_error", _elapsed_ms())
|
|
return None
|
|
if resp.status_code != 200:
|
|
logger.warning("NCM API returned HTTP {}", resp.status_code)
|
|
record_source_call("ncm", "station", "error", _elapsed_ms())
|
|
return None
|
|
|
|
body = resp.json()
|
|
if not body:
|
|
record_source_call("ncm", "station", "empty", _elapsed_ms())
|
|
return None
|
|
|
|
def _first_value(key: str) -> Optional[float]:
|
|
"""NCM returns per-parameter arrays; extract the first value."""
|
|
arr = body.get(key) if isinstance(body, dict) else None
|
|
if arr and isinstance(arr, list) and len(arr) > 0:
|
|
try:
|
|
return float(arr[0])
|
|
except (ValueError, TypeError):
|
|
pass
|
|
return None
|
|
|
|
temp = _first_value("t_2m:C")
|
|
wind_ms = _first_value("wind_speed_10m:ms")
|
|
pressure = _first_value("sfc_pressure:hPa")
|
|
dew_point = _first_value("dew_point_2m:C")
|
|
humidity = _first_value("relative_humidity_2m:p")
|
|
|
|
wind_kmh = round(wind_ms * 3.6, 1) if wind_ms is not None else None
|
|
wind_kt = round(wind_ms * 1.94384, 1) if wind_ms is not None else None
|
|
|
|
result: Dict = {
|
|
"current": {
|
|
"temp": temp,
|
|
"humidity": humidity,
|
|
"wind_speed_kmh": wind_kmh,
|
|
"wind_speed_kt": wind_kt,
|
|
"pressure": pressure,
|
|
"dew_point": dew_point,
|
|
},
|
|
"obs_time": now_utc.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
"station_label": "Jeddah OEJN (NCM)",
|
|
"lat": float(self.JEDDAH_LAT),
|
|
"lon": float(self.JEDDAH_LON),
|
|
}
|
|
record_source_call("ncm", "station", "success", _elapsed_ms())
|
|
logger.info(
|
|
"NCM Jeddah {}: temp={}°C RH={}% wind={}km/h p={}hPa",
|
|
valid_time,
|
|
temp,
|
|
humidity,
|
|
wind_kmh,
|
|
pressure,
|
|
)
|
|
return result
|
|
|
|
except Exception:
|
|
logger.exception("NCM fetch failed for Jeddah")
|
|
record_source_call("ncm", "station", "error", _elapsed_ms())
|
|
return None
|