Files
PolyWeather/src/data_collection/ncm_sources.py
T
2569718930@qq.com 891b4d2422 新增 AEROWEB (Météo-France) 和 NCM (沙特) 实时气象数据源
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 登录→取数→解析端到端验证通过
2026-05-22 21:50:37 +08:00

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