Align Taipei settlement to NOAA RCTP across web and extension

This commit is contained in:
2569718930@qq.com
2026-03-23 14:39:41 +08:00
parent 7cdd6102c6
commit ace9c71743
17 changed files with 4590 additions and 41 deletions
+7 -7
View File
@@ -75,18 +75,18 @@ CITY_REGISTRY = {
},
"taipei": {
"name": "Taipei",
"lat": 25.0377,
"lon": 121.5149,
"icao": "RCSS",
"settlement_source": "cwa",
"lat": 25.0777,
"lon": 121.2330,
"icao": "RCTP",
"settlement_source": "noaa",
"tz_offset": 28800,
"use_fahrenheit": False,
"is_major": True,
"risk_level": "low",
"risk_emoji": "🟢",
"airport_name": "中央气象署台北站",
"distance_km": 1.5,
"warning": "盆地地形叠加都市热岛,夏季午后对流前后温度波动较快",
"airport_name": "台湾桃园国际机场",
"distance_km": 28.1,
"warning": "市场现按 NOAA RCTP 整度°C口径结算,机场与台北市区温度不可混用",
},
"shanghai": {
"name": "Shanghai",
+114 -1
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import csv
import math
import time
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Optional
@@ -11,6 +12,8 @@ from loguru import logger
class SettlementSourceMixin:
IMGW_METEO_API_BASE = "https://meteo.imgw.pl/api/v1"
IMGW_METEO_API_TOKEN = "p4DXKjsYadfBV21TYrDk"
NOAA_WRH_MESO_TOKEN = "7c76618b66c74aee913bdbae4b448bdd"
NOAA_WRH_TIMESERIES_REFERER = "https://www.weather.gov/wrh/timeseries?site=RCTP"
def _get_settlement_cache(self, key: str) -> Optional[Dict[str, Any]]:
now_ts = time.time()
@@ -81,6 +84,13 @@ class SettlementSourceMixin:
except Exception:
return None
@staticmethod
def _js_round(value: Any) -> Optional[int]:
parsed = SettlementSourceMixin._safe_float(value)
if parsed is None:
return None
return int(math.floor(float(parsed) + 0.5))
@staticmethod
def _pick_station_row(
rows: List[Dict[str, str]], candidates: List[str]
@@ -263,6 +273,109 @@ class SettlementSourceMixin:
logger.warning(f"CWA Forecast request failed: {exc}")
return None
def fetch_noaa_rctp_settlement_current(self) -> Optional[Dict[str, Any]]:
cache_key = "noaa:rctp"
cached = self._get_settlement_cache(cache_key)
if cached:
return cached
try:
response = self.session.get(
"https://api.synopticdata.com/v2/stations/timeseries",
params={
"STID": "RCTP",
"showemptystations": 1,
"recent": 2880,
"complete": 1,
"token": self.NOAA_WRH_MESO_TOKEN,
"obtimezone": "local",
},
headers={
"Referer": self.NOAA_WRH_TIMESERIES_REFERER,
"Origin": "https://www.weather.gov",
"User-Agent": "Mozilla/5.0",
},
timeout=self.timeout,
)
response.raise_for_status()
payload = response.json() if response.content else {}
stations = payload.get("STATION") or []
station = stations[0] if isinstance(stations, list) and stations else None
if not isinstance(station, dict):
return None
obs = station.get("OBSERVATIONS") or {}
stamps = obs.get("date_time") or []
temps = obs.get("air_temp_set_1") or []
humidity_list = obs.get("relative_humidity_set_1") or []
wind_speed_list = obs.get("wind_speed_set_1") or []
wind_dir_list = obs.get("wind_direction_set_1") or []
if not isinstance(stamps, list) or not isinstance(temps, list) or not stamps or not temps:
return None
target_date = datetime.now(timezone(timedelta(hours=8))).date()
today_rows: List[tuple[datetime, int]] = []
latest_dt: Optional[datetime] = None
latest_temp: Optional[int] = None
latest_humidity: Optional[float] = None
latest_wind_speed_ms: Optional[float] = None
latest_wind_dir: Optional[float] = None
for idx, stamp in enumerate(stamps):
raw_temp = temps[idx] if idx < len(temps) else None
rounded_temp = self._js_round(raw_temp)
if rounded_temp is None:
continue
try:
dt = datetime.strptime(str(stamp), "%Y-%m-%dT%H:%M:%S%z")
except Exception:
continue
if dt.date() == target_date:
today_rows.append((dt, rounded_temp))
if latest_dt is None or dt >= latest_dt:
latest_dt = dt
latest_temp = rounded_temp
latest_humidity = self._safe_float(humidity_list[idx] if idx < len(humidity_list) else None)
latest_wind_speed_ms = self._safe_float(wind_speed_list[idx] if idx < len(wind_speed_list) else None)
latest_wind_dir = self._safe_float(wind_dir_list[idx] if idx < len(wind_dir_list) else None)
if latest_dt is None or latest_temp is None:
return None
max_so_far = None
max_temp_time = None
today_low = None
if today_rows:
max_so_far = max(temp for _, temp in today_rows)
today_low = min(temp for _, temp in today_rows)
for dt, temp in today_rows:
if temp == max_so_far:
max_temp_time = dt.strftime("%H:%M")
break
result = {
"source": "noaa",
"source_label": "NOAA",
"station_code": "RCTP",
"station_name": str(station.get("NAME") or "Taiwan Taoyuan International Airport"),
"observation_time": latest_dt.isoformat(),
"current": {
"temp": latest_temp,
"max_temp_so_far": max_so_far,
"max_temp_time": max_temp_time,
"today_low": today_low,
"humidity": round(latest_humidity, 1) if latest_humidity is not None else None,
"wind_speed_kt": round(float(latest_wind_speed_ms) * 1.943844, 1) if latest_wind_speed_ms is not None else None,
"wind_dir": latest_wind_dir,
},
"unit": "celsius",
}
self._set_settlement_cache(cache_key, result)
return result
except Exception as exc:
logger.warning(f"NOAA RCTP settlement fetch failed: {exc}")
return None
def _imgw_api_get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]:
try:
query = {"token": self.IMGW_METEO_API_TOKEN}
@@ -346,5 +459,5 @@ class SettlementSourceMixin:
if normalized == "hong kong":
return self.fetch_hko_settlement_current()
if normalized == "taipei":
return self.fetch_cwa_taipei_settlement_current()
return self.fetch_noaa_rctp_settlement_current()
return None
+1 -5
View File
@@ -539,7 +539,7 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour
if normalized == "hong kong":
self._settlement_cache.pop("hko:hong_kong", None)
elif normalized == "taipei":
self._settlement_cache.pop("cwa:taipei:466920", None)
self._settlement_cache.pop("noaa:rctp", None)
def _uses_fahrenheit(self, city_lower: str) -> bool:
return city_lower in self.US_CITIES
@@ -557,10 +557,6 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour
hko_forecast = self.fetch_hko_forecast()
if hko_forecast:
results["hko_forecast"] = hko_forecast
elif city_lower in ["taipei", "台北", "臺北", "tpe"]:
cwa_forecast = self.fetch_cwa_taipei_forecast()
if cwa_forecast:
results["cwa_forecast"] = cwa_forecast
def _attach_turkish_mgm_data(self, results: Dict, city_lower: str) -> None:
if city_lower not in self.TURKISH_PROVINCES: