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
+30 -4
View File
@@ -20,6 +20,7 @@ const I18N = {
settlementAirport: "结算机场", settlementAirport: "结算机场",
hko: "香港天文台 (HKO)", hko: "香港天文台 (HKO)",
cwa: "交通部中央气象署 (CWA)", cwa: "交通部中央气象署 (CWA)",
noaa: "NOAA RCTP(台湾桃园国际机场)",
city: "城市", city: "城市",
refresh: "刷新数据", refresh: "刷新数据",
cityProfile: "城市档案", cityProfile: "城市档案",
@@ -35,6 +36,9 @@ const I18N = {
nearbyMonitoringSuffix: "个参与监控", nearbyMonitoringSuffix: "个参与监控",
today: "今天", today: "今天",
omSeries: "OM预测", omSeries: "OM预测",
noaaSettlementRef: "NOAA RCTP 结算参考",
noaaSettlementLegend:
"台北按 NOAA RCTP 最终完成质控后的最高整度摄氏值结算;图中曲线仅作结算参考。",
loadCityDetailFailed: "加载城市详情失败", loadCityDetailFailed: "加载城市详情失败",
refreshFailed: "刷新温度数据失败", refreshFailed: "刷新温度数据失败",
initFailed: "初始化失败", initFailed: "初始化失败",
@@ -55,6 +59,7 @@ const I18N = {
settlementAirport: "Settlement Airport", settlementAirport: "Settlement Airport",
hko: "Hong Kong Observatory (HKO)", hko: "Hong Kong Observatory (HKO)",
cwa: "Central Weather Administration (CWA)", cwa: "Central Weather Administration (CWA)",
noaa: "NOAA RCTP (Taiwan Taoyuan International Airport)",
city: "City", city: "City",
refresh: "Refresh data", refresh: "Refresh data",
cityProfile: "City Profile", cityProfile: "City Profile",
@@ -70,6 +75,9 @@ const I18N = {
nearbyMonitoringSuffix: " stations monitored", nearbyMonitoringSuffix: " stations monitored",
today: "Today", today: "Today",
omSeries: "OM Forecast", omSeries: "OM Forecast",
noaaSettlementRef: "NOAA RCTP Settlement Reference",
noaaSettlementLegend:
"Taipei settles on NOAA RCTP using the finalized highest rounded whole-degree Celsius reading; the plotted line is only a settlement reference.",
loadCityDetailFailed: "Failed to load city detail", loadCityDetailFailed: "Failed to load city detail",
refreshFailed: "Failed to refresh weather data", refreshFailed: "Failed to refresh weather data",
initFailed: "Initialization failed", initFailed: "Initialization failed",
@@ -394,6 +402,12 @@ function getSettlementSourceDisplay(detail) {
value: t("cwa") value: t("cwa")
}; };
} }
if (source === "noaa") {
return {
label: t("settlementSource"),
value: t("noaa")
};
}
const airport = detail?.risk?.airport || "--"; const airport = detail?.risk?.airport || "--";
const icao = detail?.risk?.icao ? ` (${detail.risk.icao})` : ""; const icao = detail?.risk?.icao ? ` (${detail.risk.icao})` : "";
return { return {
@@ -629,7 +643,10 @@ function drawTrendChart(detail) {
const points = [...trend, ...obs]; const points = [...trend, ...obs];
const hoverPoints = []; const hoverPoints = [];
const tempSymbol = detail?.temp_symbol || "°C"; const tempSymbol = detail?.temp_symbol || "°C";
const obsSeriesLabel = String(detail?.current?.settlement_source_label || "OBS").toUpperCase(); const sourceCode = String(detail?.current?.settlement_source || "").toLowerCase();
const obsSeriesLabel = sourceCode === "noaa"
? t("noaaSettlementRef")
: String(detail?.current?.settlement_source_label || "OBS").toUpperCase();
if (!points.length) { if (!points.length) {
setChartHover([], tempSymbol); setChartHover([], tempSymbol);
ctx.fillStyle = "#8ba0be"; ctx.fillStyle = "#8ba0be";
@@ -788,14 +805,23 @@ function renderDetail(detail) {
drawTrendChart(detail); drawTrendChart(detail);
renderForecast(detail); renderForecast(detail);
const sourceTag = String(detail?.current?.settlement_source_label || "").toUpperCase() || "OBS"; const sourceCode = String(detail?.current?.settlement_source || "").toLowerCase();
const sourceTag = sourceCode === "noaa"
? t("noaaSettlementRef")
: String(detail?.current?.settlement_source_label || "").toUpperCase() || "OBS";
const obs = getObservationRows(detail); const obs = getObservationRows(detail);
if (obs.length >= 2) { if (obs.length >= 2) {
const first = obs[0]; const first = obs[0];
const last = obs[obs.length - 1]; const last = obs[obs.length - 1];
els.chartLegend.textContent = `${sourceTag}: ${first.temp}°C@${first.time} -> ${last.temp}°C@${last.time}`; els.chartLegend.textContent =
sourceCode === "noaa"
? `${sourceTag}: ${first.temp}°C@${first.time} -> ${last.temp}°C@${last.time} | ${t("noaaSettlementLegend")}`
: `${sourceTag}: ${first.temp}°C@${first.time} -> ${last.temp}°C@${last.time}`;
} else { } else {
els.chartLegend.textContent = `${sourceTag}: ${t("noContinuousObs")}`; els.chartLegend.textContent =
sourceCode === "noaa"
? `${sourceTag}: ${t("noContinuousObs")} | ${t("noaaSettlementLegend")}`
: `${sourceTag}: ${t("noContinuousObs")}`;
} }
} }
@@ -452,6 +452,10 @@ export function FutureForecastModal() {
"--score-position": scorePosition, "--score-position": scorePosition,
} as CSSProperties & { "--score-position": string }; } as CSSProperties & { "--score-position": string };
const weatherSummary = getWeatherSummary(detail, locale); const weatherSummary = getWeatherSummary(detail, locale);
const isTaipeiNoaa =
store.selectedCity === "taipei" &&
(detail.current?.settlement_source === "noaa" ||
detail.current?.settlement_source_label === "NOAA");
const marketMidpoint = formatMarketPercent( const marketMidpoint = formatMarketPercent(
marketScan?.market_price ?? marketScan?.yes_token?.implied_probability, marketScan?.market_price ?? marketScan?.yes_token?.implied_probability,
); );
@@ -551,11 +555,15 @@ export function FutureForecastModal() {
? locale === "en-US" ? locale === "en-US"
? "Hong Kong Observatory (HKO)" ? "Hong Kong Observatory (HKO)"
: "香港天文台 (HKO)" : "香港天文台 (HKO)"
: settlementSourceCode === "cwa" : settlementSourceCode === "noaa"
? locale === "en-US" ? locale === "en-US"
? "Central Weather Administration (CWA)" ? "NOAA RCTP (Taiwan Taoyuan International Airport)"
: "交通部中央气象署 (CWA)" : "NOAA RCTP(台湾桃园国际机场)"
: risk.airport : settlementSourceCode === "cwa"
? locale === "en-US"
? "Central Weather Administration (CWA)"
: "交通部中央气象署 (CWA)"
: risk.airport
? `${risk.airport}${risk.icao ? ` (${risk.icao})` : ""}` ? `${risk.airport}${risk.icao ? ` (${risk.icao})` : ""}`
: "--"; : "--";
@@ -652,6 +660,24 @@ export function FutureForecastModal() {
</button> </button>
</div> </div>
<div className="modal-body future-modal-body"> <div className="modal-body future-modal-body">
{isTaipeiNoaa && (
<div
style={{
marginBottom: "16px",
padding: "12px 14px",
border: "1px solid rgba(56, 189, 248, 0.24)",
borderRadius: "12px",
background: "rgba(14, 165, 233, 0.08)",
color: "var(--text-secondary)",
fontSize: "13px",
lineHeight: 1.6,
}}
>
{locale === "en-US"
? "Taipei now settles against NOAA RCTP (Taiwan Taoyuan International Airport). The market uses the highest rounded whole-degree Celsius reading in the Temp column after the day is finalized."
: "台北当前按 NOAA RCTP(台湾桃园国际机场)结算。市场最终采用该日 Temp 列完成质控后的最高整度摄氏值,不按小数温度结算。"}
</div>
)}
{isToday ? ( {isToday ? (
<div className="future-v2-layout"> <div className="future-v2-layout">
<aside className="future-v2-left"> <aside className="future-v2-left">
+28 -2
View File
@@ -12,6 +12,7 @@ function HistoryChart() {
const store = useDashboardStore(); const store = useDashboardStore();
const { locale } = useI18n(); const { locale } = useI18n();
const { data } = useHistoryData(); const { data } = useHistoryData();
const isTaipei = store.selectedCity === "taipei";
const summary = useMemo( const summary = useMemo(
() => getHistorySummary(data, store.selectedDetail?.local_date), () => getHistorySummary(data, store.selectedDetail?.local_date),
[data, store.selectedDetail?.local_date], [data, store.selectedDetail?.local_date],
@@ -33,7 +34,13 @@ function HistoryChart() {
borderColor: "#f87171", borderColor: "#f87171",
borderWidth: 2, borderWidth: 2,
data: summary.actuals, data: summary.actuals,
label: locale === "en-US" ? "Observed High" : "实测最高温", label: isTaipei
? locale === "en-US"
? "NOAA Settled High (RCTP)"
: "NOAA 结算最高温 (RCTP)"
: locale === "en-US"
? "Observed High"
: "实测最高温",
pointBackgroundColor: "#f87171", pointBackgroundColor: "#f87171",
pointBorderColor: "#fff", pointBorderColor: "#fff",
pointHoverRadius: 7, pointHoverRadius: 7,
@@ -135,7 +142,7 @@ function HistoryChart() {
}, },
type: "line", type: "line",
} satisfies ChartConfiguration<"line">; } satisfies ChartConfiguration<"line">;
}, [hasBestBaseline, hasMgm, summary, locale]); }, [hasBestBaseline, hasMgm, isTaipei, summary, locale]);
if (!summary.recentData.length) return null; if (!summary.recentData.length) return null;
@@ -152,6 +159,7 @@ export function HistoryModal() {
const { data, error, isLoading, isOpen } = useHistoryData(); const { data, error, isLoading, isOpen } = useHistoryData();
const isPro = store.proAccess.subscriptionActive; const isPro = store.proAccess.subscriptionActive;
const isProLoading = store.proAccess.loading; const isProLoading = store.proAccess.loading;
const isTaipei = store.selectedCity === "taipei";
const summary = useMemo( const summary = useMemo(
() => getHistorySummary(data, store.selectedDetail?.local_date), () => getHistorySummary(data, store.selectedDetail?.local_date),
[data, store.selectedDetail?.local_date], [data, store.selectedDetail?.local_date],
@@ -200,6 +208,24 @@ export function HistoryModal() {
</button> </button>
</div> </div>
<div className="modal-body"> <div className="modal-body">
{isTaipei && (
<div
style={{
marginBottom: "16px",
padding: "12px 14px",
border: "1px solid rgba(56, 189, 248, 0.24)",
borderRadius: "12px",
background: "rgba(14, 165, 233, 0.08)",
color: "var(--text-secondary)",
fontSize: "13px",
lineHeight: 1.6,
}}
>
{t("lang") === "en-US"
? "Taipei historical actuals are aligned to NOAA RCTP settlement rules: use the highest rounded whole-degree Celsius reading after the date is finalized."
: "台北历史对账已按 NOAA RCTP 结算口径对齐:采用该日最终完成质控后的最高整度摄氏值。"}
</div>
)}
<div className="history-stats"> <div className="history-stats">
{isLoading ? ( {isLoading ? (
<span style={{ color: "var(--text-muted)" }}> <span style={{ color: "var(--text-muted)" }}>
+4 -4
View File
@@ -60,8 +60,8 @@ const CITY_SPECIFIC_SOURCES: Record<string, OfficialSourceLink[]> = {
], ],
taipei: [ taipei: [
{ {
label: "CWA 中央气象署", label: "NOAA RCTP Timeseries",
href: "https://www.cwa.gov.tw/V8/E/", href: "https://www.weather.gov/wrh/timeseries?site=RCTP",
kind: "agency", kind: "agency",
}, },
{ {
@@ -70,8 +70,8 @@ const CITY_SPECIFIC_SOURCES: Record<string, OfficialSourceLink[]> = {
kind: "airport", kind: "airport",
}, },
{ {
label: "RCSS METAR", label: "RCTP METAR",
href: "https://aviationweather.gov/data/metar/?id=RCSS&decoded=1&taf=1", href: "https://aviationweather.gov/data/metar/?id=RCTP&decoded=1&taf=1",
kind: "metar", kind: "metar",
}, },
], ],
+29 -8
View File
@@ -40,7 +40,7 @@ function getObservationSourceCode(detail: CityDetail): string {
.trim() .trim()
.toLowerCase(); .toLowerCase();
if (city === "hong kong") return "hko"; if (city === "hong kong") return "hko";
if (city === "taipei") return "cwa"; if (city === "taipei") return "noaa";
return "metar"; return "metar";
} }
@@ -52,6 +52,7 @@ function getObservationSourceTag(detail: CityDetail): string {
const code = getObservationSourceCode(detail); const code = getObservationSourceCode(detail);
if (code === "hko") return "HKO"; if (code === "hko") return "HKO";
if (code === "cwa") return "CWA"; if (code === "cwa") return "CWA";
if (code === "noaa") return "NOAA";
if (code === "mgm") return "MGM"; if (code === "mgm") return "MGM";
return "METAR"; return "METAR";
} }
@@ -239,7 +240,7 @@ export function getTemperatureChartData(
const observationTag = getObservationSourceTag(detail); const observationTag = getObservationSourceTag(detail);
const observationCode = getObservationSourceCode(detail); const observationCode = getObservationSourceCode(detail);
const settlementSource = const settlementSource =
observationCode === "hko" || observationCode === "cwa"; observationCode === "hko" || observationCode === "cwa" || observationCode === "noaa";
const officialObservationSource = const officialObservationSource =
settlementSource settlementSource
? detail.settlement_today_obs?.length ? detail.settlement_today_obs?.length
@@ -267,7 +268,11 @@ export function getTemperatureChartData(
return `${icao} METAR`; return `${icao} METAR`;
})(); })();
const observationDisplayTag = const observationDisplayTag =
settlementSource && shouldUseMetarFallback ? metarFallbackTag : observationTag; settlementSource && shouldUseMetarFallback
? metarFallbackTag
: observationCode === "noaa"
? "NOAA RCTP"
: observationTag;
const metarPoints = new Array(times.length).fill(null); const metarPoints = new Array(times.length).fill(null);
observationSource.forEach((item) => { observationSource.forEach((item) => {
@@ -361,6 +366,12 @@ export function getTemperatureChartData(
? `Official ${observationTag} feed is sparse today, so the continuous observation line switches to ${metarFallbackTag}.` ? `Official ${observationTag} feed is sparse today, so the continuous observation line switches to ${metarFallbackTag}.`
: `今日官方 ${observationTag} 点位较稀疏,连续实测线改用 ${metarFallbackTag}`, : `今日官方 ${observationTag} 点位较稀疏,连续实测线改用 ${metarFallbackTag}`,
); );
} else if (observationCode === "noaa") {
legendParts.push(
isEnglish(locale)
? "Taipei settles on NOAA RCTP using the finalized highest rounded whole-degree Celsius reading; the plotted line is a settlement reference."
: "台北按 NOAA RCTP 最终完成质控后的最高整度摄氏值结算;图中曲线仅作为结算参考线。",
);
} }
return { return {
@@ -374,9 +385,14 @@ export function getTemperatureChartData(
offset, offset,
temps, temps,
}, },
observationLabel: isEnglish(locale) observationLabel:
? `${observationDisplayTag} Observation` observationCode === "noaa" && !shouldUseMetarFallback
: `${observationDisplayTag} 实况`, ? isEnglish(locale)
? `${observationDisplayTag} Settlement Reference`
: `${observationDisplayTag} 结算参考`
: isEnglish(locale)
? `${observationDisplayTag} Observation`
: `${observationDisplayTag} 实况`,
legendText: legendParts.join(" | "), legendText: legendParts.join(" | "),
max, max,
min, min,
@@ -1416,7 +1432,7 @@ export function getCityProfileStats(detail: CityDetail, locale: Locale = "zh-CN"
const current = detail.current || {}; const current = detail.current || {};
const nearbyCount = Array.isArray(detail.mgm_nearby) ? detail.mgm_nearby.length : 0; const nearbyCount = Array.isArray(detail.mgm_nearby) ? detail.mgm_nearby.length : 0;
const sourceCode = getObservationSourceCode(detail); const sourceCode = getObservationSourceCode(detail);
const isOfficialSource = sourceCode === "hko" || sourceCode === "cwa"; const isOfficialSource = sourceCode === "hko" || sourceCode === "cwa" || sourceCode === "noaa";
const sourceDisplay = (() => { const sourceDisplay = (() => {
if (sourceCode === "hko") { if (sourceCode === "hko") {
@@ -1429,6 +1445,11 @@ export function getCityProfileStats(detail: CityDetail, locale: Locale = "zh-CN"
? "Central Weather Administration (CWA)" ? "Central Weather Administration (CWA)"
: "交通部中央气象署 (CWA)"; : "交通部中央气象署 (CWA)";
} }
if (sourceCode === "noaa") {
return isEnglish(locale)
? "NOAA RCTP (Taiwan Taoyuan)"
: "NOAA RCTP(台湾桃园国际机场)";
}
const tag = getObservationSourceTag(detail); const tag = getObservationSourceTag(detail);
if (sourceCode === "mgm") { if (sourceCode === "mgm") {
return isEnglish(locale) ? `MGM (${tag})` : `MGM (${tag})`; return isEnglish(locale) ? `MGM (${tag})` : `MGM (${tag})`;
@@ -1491,7 +1512,7 @@ export function getSettlementRiskNarrative(
) { ) {
const risk = detail.risk || {}; const risk = detail.risk || {};
const sourceCode = getObservationSourceCode(detail); const sourceCode = getObservationSourceCode(detail);
const stationTerm = sourceCode === "hko" || sourceCode === "cwa" const stationTerm = sourceCode === "hko" || sourceCode === "cwa" || sourceCode === "noaa"
? isEnglish(locale) ? isEnglish(locale)
? "settlement reference station" ? "settlement reference station"
: "结算参考站" : "结算参考站"
+2 -2
View File
@@ -52,7 +52,7 @@ const MESSAGES: Record<Locale, Record<string, string>> = {
"guide.title": "📎 PolyWeather 系统技术说明", "guide.title": "📎 PolyWeather 系统技术说明",
"guide.closeAria": "关闭技术说明", "guide.closeAria": "关闭技术说明",
"guide.footer": "guide.footer":
"数据源以 METAR、香港天文台(HKO)、中央气象署(CWA)、Turkish MGM、Open-Meteo、weather.gov 为主。", "数据源以 METAR、香港天文台(HKO)、NOAA RCTP、Turkish MGM、Open-Meteo、weather.gov 为主。",
"history.title": "📊 历史准确率对账 - {city}", "history.title": "📊 历史准确率对账 - {city}",
"history.closeAria": "关闭历史对账", "history.closeAria": "关闭历史对账",
@@ -214,7 +214,7 @@ const MESSAGES: Record<Locale, Record<string, string>> = {
"guide.title": "📎 PolyWeather Technical Overview", "guide.title": "📎 PolyWeather Technical Overview",
"guide.closeAria": "Close technical overview", "guide.closeAria": "Close technical overview",
"guide.footer": "guide.footer":
"Primary data sources are METAR, Hong Kong Observatory (HKO), CWA (Taiwan), Turkish MGM, Open-Meteo, and weather.gov.", "Primary data sources are METAR, Hong Kong Observatory (HKO), NOAA RCTP, Turkish MGM, Open-Meteo, and weather.gov.",
"history.title": "📊 Historical Reconciliation - {city}", "history.title": "📊 Historical Reconciliation - {city}",
"history.closeAria": "Close history reconciliation", "history.closeAria": "Close history reconciliation",
+2 -1
View File
@@ -37,6 +37,7 @@ def _resolve_settlement_source(city_meta: Dict[str, Any]) -> Tuple[str, str]:
"metar": "METAR", "metar": "METAR",
"hko": "HKO", "hko": "HKO",
"cwa": "CWA", "cwa": "CWA",
"noaa": "NOAA",
"mgm": "MGM", "mgm": "MGM",
} }
return source, source_label_map.get(source, source.upper()) return source, source_label_map.get(source, source.upper())
@@ -333,7 +334,7 @@ def build_city_query_report(
sc_current = {} sc_current = {}
city_meta = CITY_REGISTRY.get(city_name.lower(), {}) city_meta = CITY_REGISTRY.get(city_name.lower(), {})
settlement_source, settlement_source_label = _resolve_settlement_source(city_meta) settlement_source, settlement_source_label = _resolve_settlement_source(city_meta)
use_settlement_current = settlement_source in {"hko", "cwa"} and bool(sc_current) use_settlement_current = settlement_source in {"hko", "cwa", "noaa"} and bool(sc_current)
fallback_utc_offset = int(city_meta.get("tz_offset", 0)) fallback_utc_offset = int(city_meta.get("tz_offset", 0))
nws_periods = ((weather_data.get("nws") or {}).get("forecast_periods") or []) nws_periods = ((weather_data.get("nws") or {}).get("forecast_periods") or [])
if nws_periods: if nws_periods:
+1 -2
View File
@@ -24,7 +24,7 @@ def is_exact_settlement_city(city: str) -> bool:
if not city: if not city:
return False return False
c = str(city).lower().strip() c = str(city).lower().strip()
return c in ["hong kong", "hk", "taipei", "tpe", "臺北", "台北", "香港"] return c in ["hong kong", "hk", "香港"]
def apply_city_settlement(city: str, value: Optional[Number]) -> Optional[int]: def apply_city_settlement(city: str, value: Optional[Number]) -> Optional[int]:
@@ -38,4 +38,3 @@ def apply_city_settlement(city: str, value: Optional[Number]) -> Optional[int]:
if is_exact_settlement_city(city): if is_exact_settlement_city(city):
return int(math.floor(float(value))) return int(math.floor(float(value)))
return wu_round(value) return wu_round(value)
+1
View File
@@ -28,6 +28,7 @@ SETTLEMENT_SOURCE_LABELS = {
"metar": "METAR", "metar": "METAR",
"hko": "HKO", "hko": "HKO",
"cwa": "CWA", "cwa": "CWA",
"noaa": "NOAA",
"mgm": "MGM", "mgm": "MGM",
} }
+7 -7
View File
@@ -75,18 +75,18 @@ CITY_REGISTRY = {
}, },
"taipei": { "taipei": {
"name": "Taipei", "name": "Taipei",
"lat": 25.0377, "lat": 25.0777,
"lon": 121.5149, "lon": 121.2330,
"icao": "RCSS", "icao": "RCTP",
"settlement_source": "cwa", "settlement_source": "noaa",
"tz_offset": 28800, "tz_offset": 28800,
"use_fahrenheit": False, "use_fahrenheit": False,
"is_major": True, "is_major": True,
"risk_level": "low", "risk_level": "low",
"risk_emoji": "🟢", "risk_emoji": "🟢",
"airport_name": "中央气象署台北站", "airport_name": "台湾桃园国际机场",
"distance_km": 1.5, "distance_km": 28.1,
"warning": "盆地地形叠加都市热岛,夏季午后对流前后温度波动较快", "warning": "市场现按 NOAA RCTP 整度°C口径结算,机场与台北市区温度不可混用",
}, },
"shanghai": { "shanghai": {
"name": "Shanghai", "name": "Shanghai",
+114 -1
View File
@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import csv import csv
import math
import time import time
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
@@ -11,6 +12,8 @@ from loguru import logger
class SettlementSourceMixin: class SettlementSourceMixin:
IMGW_METEO_API_BASE = "https://meteo.imgw.pl/api/v1" IMGW_METEO_API_BASE = "https://meteo.imgw.pl/api/v1"
IMGW_METEO_API_TOKEN = "p4DXKjsYadfBV21TYrDk" 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]]: def _get_settlement_cache(self, key: str) -> Optional[Dict[str, Any]]:
now_ts = time.time() now_ts = time.time()
@@ -81,6 +84,13 @@ class SettlementSourceMixin:
except Exception: except Exception:
return None 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 @staticmethod
def _pick_station_row( def _pick_station_row(
rows: List[Dict[str, str]], candidates: List[str] rows: List[Dict[str, str]], candidates: List[str]
@@ -263,6 +273,109 @@ class SettlementSourceMixin:
logger.warning(f"CWA Forecast request failed: {exc}") logger.warning(f"CWA Forecast request failed: {exc}")
return None 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]]: def _imgw_api_get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]:
try: try:
query = {"token": self.IMGW_METEO_API_TOKEN} query = {"token": self.IMGW_METEO_API_TOKEN}
@@ -346,5 +459,5 @@ class SettlementSourceMixin:
if normalized == "hong kong": if normalized == "hong kong":
return self.fetch_hko_settlement_current() return self.fetch_hko_settlement_current()
if normalized == "taipei": if normalized == "taipei":
return self.fetch_cwa_taipei_settlement_current() return self.fetch_noaa_rctp_settlement_current()
return None return None
+1 -5
View File
@@ -539,7 +539,7 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour
if normalized == "hong kong": if normalized == "hong kong":
self._settlement_cache.pop("hko:hong_kong", None) self._settlement_cache.pop("hko:hong_kong", None)
elif normalized == "taipei": 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: def _uses_fahrenheit(self, city_lower: str) -> bool:
return city_lower in self.US_CITIES return city_lower in self.US_CITIES
@@ -557,10 +557,6 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour
hko_forecast = self.fetch_hko_forecast() hko_forecast = self.fetch_hko_forecast()
if hko_forecast: if hko_forecast:
results["hko_forecast"] = 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: def _attach_turkish_mgm_data(self, results: Dict, city_lower: str) -> None:
if city_lower not in self.TURKISH_PROVINCES: if city_lower not in self.TURKISH_PROVINCES:
+3
View File
@@ -0,0 +1,3 @@
// This Synoptic API key is for weather.gov websites/queries
var mesoToken='7c76618b66c74aee913bdbae4b448bdd';
+3098
View File
File diff suppressed because it is too large Load Diff
+1238
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -74,7 +74,7 @@ def _analyze(city: str, force_refresh: bool = False) -> Dict[str, Any]:
mc = metar.get("current", {}) if metar else {} mc = metar.get("current", {}) if metar else {}
mg_cur = mgm.get("current", {}) if mgm else {} mg_cur = mgm.get("current", {}) if mgm else {}
sc_cur = settlement_current.get("current", {}) if settlement_current else {} sc_cur = settlement_current.get("current", {}) if settlement_current else {}
use_settlement_current = settlement_source in {"hko", "cwa"} and bool(sc_cur) use_settlement_current = settlement_source in {"hko", "cwa", "noaa"} and bool(sc_cur)
primary_current = sc_cur if use_settlement_current else mc primary_current = sc_cur if use_settlement_current else mc
cur_temp = _sf(primary_current.get("temp")) cur_temp = _sf(primary_current.get("temp"))
if cur_temp is None: if cur_temp is None:
+1
View File
@@ -74,6 +74,7 @@ SETTLEMENT_SOURCE_LABELS: Dict[str, str] = {
"metar": "METAR", "metar": "METAR",
"hko": "HKO", "hko": "HKO",
"cwa": "CWA", "cwa": "CWA",
"noaa": "NOAA",
"mgm": "MGM", "mgm": "MGM",
} }