Align Taipei settlement to NOAA RCTP across web and extension
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user