接入新加坡 MSS 1分钟实时温度及 MGM/JMA/FMI/KNMI 高频源到 airport_primary
- 新建 singapore_mss_sources.py:拉取 data.gov.sg 1分钟干球温度(S24 樟宜站) - country_networks.py:_airport_primary_from_raw 新增 MGM/JMA/FMI/KNMI/SG_MSS 分支 - weather_sources.py:注入 jma_current/fmi_current/knmi_current/singapore_mss_current - 前端 MonitorPanel:resolveSourceLabel 根据 airport_primary.source_code 显示数据源标签 - 文档:更新 AIRPORT_REALTIME_SOURCES.md 新增新加坡 Tested: ruff check . ✓ npx tsc --noEmit ✓
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
"""Singapore MSS 1-minute real-time temperature data source.
|
||||
|
||||
Fetches air temperature from data.gov.sg public API.
|
||||
The API provides dry-bulb temperature (1-min mean) at ~1 min interval
|
||||
from 15 stations across Singapore. Station S24 (Upper Changi Road North)
|
||||
is the closest to Changi Airport (WSSS).
|
||||
|
||||
URL: https://api.data.gov.sg/v1/environment/air-temperature
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from src.utils.metrics import record_source_call
|
||||
|
||||
SINGAPORE_MSS_TEMP_URL = (
|
||||
"https://api.data.gov.sg/v1/environment/air-temperature"
|
||||
)
|
||||
|
||||
# Preferred station: closest to Changi Airport (WSSS)
|
||||
PREFERRED_STATION_ID = "S24"
|
||||
PREFERRED_STATION_NAME = "Upper Changi Road North"
|
||||
|
||||
|
||||
def _parse_mss_reading(raw: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
"""Extract the preferred station's temperature from an API response."""
|
||||
items = raw.get("items") or []
|
||||
if not items:
|
||||
return None
|
||||
|
||||
metadata = raw.get("metadata") or {}
|
||||
stations = metadata.get("stations") or []
|
||||
|
||||
# Resolve station name
|
||||
station_name = ""
|
||||
for s in stations:
|
||||
if isinstance(s, dict) and s.get("id") == PREFERRED_STATION_ID:
|
||||
station_name = str(s.get("name") or PREFERRED_STATION_NAME)
|
||||
break
|
||||
if not station_name:
|
||||
station_name = PREFERRED_STATION_NAME
|
||||
|
||||
# Take the most recent reading
|
||||
latest = items[-1]
|
||||
readings = latest.get("readings") or []
|
||||
ts = str(latest.get("timestamp") or "")
|
||||
|
||||
for r in readings:
|
||||
if r.get("station_id") == PREFERRED_STATION_ID:
|
||||
temp_c = r.get("value")
|
||||
if temp_c is not None and -20 < temp_c < 55:
|
||||
# Observed at: use the API timestamp, which is UTC+8
|
||||
obs_time = ts
|
||||
if obs_time:
|
||||
try:
|
||||
dt = datetime.fromisoformat(obs_time)
|
||||
obs_time = dt.astimezone(timezone.utc).isoformat()
|
||||
except (ValueError, OverflowError):
|
||||
pass
|
||||
|
||||
return {
|
||||
"station_id": PREFERRED_STATION_ID,
|
||||
"station_name": station_name,
|
||||
"temp_c": round(float(temp_c), 1),
|
||||
"obs_time": obs_time,
|
||||
"source": "sg_mss",
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class SingaporeMssSourceMixin:
|
||||
"""Mixin that adds Singapore MSS 1-min data to WeatherDataCollector."""
|
||||
|
||||
def _sg_mss_http_get(self, url: str) -> Optional[Dict[str, Any]]:
|
||||
getter = getattr(self, "_http_get", None)
|
||||
if callable(getter):
|
||||
resp = getter(url)
|
||||
if hasattr(resp, "json"):
|
||||
return resp.json()
|
||||
return resp
|
||||
try:
|
||||
resp = self.session.get(url, timeout=self.timeout)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def fetch_singapore_mss_current(self) -> Optional[Dict[str, Any]]:
|
||||
"""Fetch latest Singapore MSS temperature for Changi station."""
|
||||
started = time.perf_counter()
|
||||
|
||||
try:
|
||||
raw = self._sg_mss_http_get(SINGAPORE_MSS_TEMP_URL)
|
||||
if not raw:
|
||||
record_source_call(
|
||||
"sg_mss", "current", "empty",
|
||||
(time.perf_counter() - started) * 1000.0,
|
||||
)
|
||||
return None
|
||||
|
||||
result = _parse_mss_reading(raw)
|
||||
if result:
|
||||
logger.info(
|
||||
"Singapore MSS: station={} temp={}°C obs_time={}",
|
||||
result["station_id"],
|
||||
result["temp_c"],
|
||||
result.get("obs_time", "?"),
|
||||
)
|
||||
record_source_call(
|
||||
"sg_mss", "current", "success",
|
||||
(time.perf_counter() - started) * 1000.0,
|
||||
)
|
||||
else:
|
||||
record_source_call(
|
||||
"sg_mss", "current", "no_station",
|
||||
(time.perf_counter() - started) * 1000.0,
|
||||
)
|
||||
return result
|
||||
|
||||
except Exception as exc:
|
||||
logger.warning("Singapore MSS fetch failed: {}", exc)
|
||||
record_source_call(
|
||||
"sg_mss", "current", "error",
|
||||
(time.perf_counter() - started) * 1000.0,
|
||||
)
|
||||
return None
|
||||
Reference in New Issue
Block a user