feat: Implement core PolyWeather application with city weather query service, trend analysis, data collection, and dashboard UI.
This commit is contained in:
@@ -13,10 +13,6 @@ TELEGRAM_ALERT_CITIES=ankara,london,paris,seoul,toronto,buenos aires,wellington,
|
||||
# AI
|
||||
GROQ_API_KEY=your_groq_api_key_here
|
||||
|
||||
# Meteoblue
|
||||
METEOBLUE_API_KEY=your_meteoblue_api_key_here
|
||||
METEOBLUE_CACHE_TTL_SEC=7200
|
||||
|
||||
# Open-Meteo (forecast data changes ~hourly, no need to refresh more often)
|
||||
OPEN_METEO_CACHE_TTL_SEC=7200
|
||||
OPEN_METEO_ENSEMBLE_CACHE_TTL_SEC=7200
|
||||
@@ -76,4 +72,3 @@ POLYMARKET_WALLET_ACTIVITY_NOTIFY_CLOSED=false
|
||||
POLYMARKET_WALLET_ACTIVITY_BOOTSTRAP_ALERT=false
|
||||
POLYMARKET_WALLET_ACTIVITY_AVG_PRICE_SHOW_MIN=0.01
|
||||
POLYMARKET_WALLET_ACTIVITY_AVG_PRICE_SHOW_MAX=0.99
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
# Weather API Configuration
|
||||
weather:
|
||||
meteoblue_api_key: null # Set via METEOBLUE_API_KEY env var
|
||||
timeout: 30
|
||||
|
||||
# Target Cities
|
||||
|
||||
@@ -211,7 +211,6 @@
|
||||
|
||||
- **Open-Meteo**
|
||||
- **weather.gov**(美国城市)
|
||||
- **Meteoblue**(部分城市)
|
||||
- **多模型集成**: ECMWF / GFS / ICON / GEM / JMA
|
||||
|
||||
---
|
||||
|
||||
@@ -22,7 +22,7 @@ const GUIDE_CARDS = {
|
||||
title: "Ankara 专属增强",
|
||||
},
|
||||
{
|
||||
body: "点击多日预报后的模态框,主要用于分析下一个交易日。6-48 小时趋势以 weather.gov 和 Open-Meteo 为主,部分城市补充 Meteoblue;0-2 小时临近判断优先看 METAR 与周边站。",
|
||||
body: "点击多日预报后的模态框,主要用于分析下一个交易日。6-48 小时趋势以 weather.gov 和 Open-Meteo 为主;0-2 小时临近判断优先看 METAR 与周边站。",
|
||||
title: "未来日期分析",
|
||||
},
|
||||
{
|
||||
@@ -48,7 +48,7 @@ const GUIDE_CARDS = {
|
||||
title: "Ankara-specific Enhancement",
|
||||
},
|
||||
{
|
||||
body: "The multi-day modal focuses on next-session analysis. 6-48h trend mainly relies on weather.gov and Open-Meteo, with Meteoblue in selected cities; 0-2h nowcast prioritizes METAR and nearby stations.",
|
||||
body: "The multi-day modal focuses on next-session analysis. 6-48h trend mainly relies on weather.gov and Open-Meteo; 0-2h nowcast prioritizes METAR and nearby stations.",
|
||||
title: "Future-date Analysis",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -552,18 +552,8 @@ export function ModelForecast({
|
||||
}) {
|
||||
const { locale, t } = useI18n();
|
||||
const view = getModelView(detail, targetDate);
|
||||
const fallbackMeteoblue = detail.source_forecasts?.meteoblue?.today_high;
|
||||
const modelsMap = { ...view.models };
|
||||
|
||||
// 强行插入 Meteoblue,防止 helper 函数或日期对齐导致其被剔除
|
||||
if (
|
||||
modelsMap["Meteoblue"] == null &&
|
||||
fallbackMeteoblue != null &&
|
||||
(!targetDate || targetDate === detail.local_date)
|
||||
) {
|
||||
modelsMap["Meteoblue"] = fallbackMeteoblue;
|
||||
}
|
||||
|
||||
const modelEntries = Object.entries(modelsMap).filter(
|
||||
([, value]) =>
|
||||
value !== null && value !== undefined && Number.isFinite(Number(value)),
|
||||
@@ -591,12 +581,6 @@ export function ModelForecast({
|
||||
? Math.max(...comparisonValues) + 1
|
||||
: 1;
|
||||
const range = Math.max(maxValue - minValue, 1);
|
||||
const getModelDisplayName = (name: string) => {
|
||||
if (String(name).trim().toLowerCase() === "meteoblue") {
|
||||
return "Meteoblue";
|
||||
}
|
||||
return name;
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="models-section">
|
||||
@@ -615,7 +599,7 @@ export function ModelForecast({
|
||||
return (
|
||||
<div key={name} className="model-row">
|
||||
<div className="model-name" title={name}>
|
||||
{getModelDisplayName(name)}
|
||||
{name}
|
||||
</div>
|
||||
<div className="model-bar-track">
|
||||
<div
|
||||
|
||||
@@ -168,12 +168,6 @@ export interface SourceForecasts {
|
||||
weather_gov?: {
|
||||
forecast_periods?: WeatherGovPeriod[];
|
||||
};
|
||||
meteoblue?: {
|
||||
today_high?: number | null;
|
||||
daily_highs?: Array<number | null>;
|
||||
unit?: string | null;
|
||||
url?: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface DailyModelForecast {
|
||||
|
||||
@@ -330,67 +330,18 @@ export function getProbabilityView(detail: CityDetail, targetDate?: string | nul
|
||||
}
|
||||
|
||||
export function getModelView(detail: CityDetail, targetDate?: string | null) {
|
||||
const toFiniteNumber = (value: unknown): number | null => {
|
||||
if (value === null || value === undefined || value === "") return null;
|
||||
const numeric = Number(value);
|
||||
return Number.isFinite(numeric) ? numeric : null;
|
||||
};
|
||||
|
||||
const pickMeteoblueForDate = (dateStr: string) => {
|
||||
const meteoblue = detail.source_forecasts?.meteoblue;
|
||||
if (!meteoblue) return null;
|
||||
|
||||
const dates = detail.forecast?.daily?.map((item) => item.date) || [];
|
||||
const index = dates.findIndex((date) => date === dateStr);
|
||||
const todayHigh = toFiniteNumber(meteoblue.today_high);
|
||||
|
||||
// Today must always use Meteoblue daily max (today_high) first.
|
||||
if (dateStr === detail.local_date && todayHigh != null) {
|
||||
return todayHigh;
|
||||
}
|
||||
|
||||
const dailyHighs = Array.isArray(meteoblue.daily_highs)
|
||||
? meteoblue.daily_highs
|
||||
: [];
|
||||
if (index >= 0) {
|
||||
const byDailyHigh = toFiniteNumber(dailyHighs[index]);
|
||||
if (byDailyHigh != null) return byDailyHigh;
|
||||
}
|
||||
|
||||
if (index === 0 && todayHigh != null) return todayHigh;
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const withMeteoblue = (
|
||||
models: Record<string, number | null>,
|
||||
dateStr: string,
|
||||
) => {
|
||||
const nextModels = { ...models };
|
||||
const existing = toFiniteNumber(nextModels.Meteoblue);
|
||||
if (existing == null) {
|
||||
const meteoblueVal = pickMeteoblueForDate(dateStr);
|
||||
if (meteoblueVal != null) {
|
||||
nextModels.Meteoblue = meteoblueVal;
|
||||
}
|
||||
} else {
|
||||
nextModels.Meteoblue = existing;
|
||||
}
|
||||
return nextModels;
|
||||
};
|
||||
|
||||
const date = targetDate || detail.local_date;
|
||||
const daily = detail.multi_model_daily?.[date];
|
||||
if (daily) {
|
||||
return {
|
||||
deb: daily.deb?.prediction ?? null,
|
||||
models: withMeteoblue(daily.models || {}, date),
|
||||
models: daily.models || {},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
deb: detail.deb?.prediction ?? null,
|
||||
models: withMeteoblue(detail.multi_model || {}, date),
|
||||
models: detail.multi_model || {},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -712,10 +663,6 @@ export function computeFrontTrendSignal(
|
||||
? isEnglish(locale)
|
||||
? "Temperature declines with pressure rebound and/or northerly shift. Next 6-48h leans cold-front suppression."
|
||||
: "温度下滑、气压回升或风向转北,未来 6-48 小时更像冷锋或冷平流压制。"
|
||||
: detail.name !== "ankara" && Boolean(detail.source_forecasts?.meteoblue)
|
||||
? isEnglish(locale)
|
||||
? "Structured trend layer mainly uses weather.gov, Open-Meteo and Meteoblue for 6-48h warm/cold flow judgement."
|
||||
: "结构化来源以 weather.gov、Open-Meteo、Meteoblue 为主,用于判断未来 6-48 小时冷暖平流趋势。"
|
||||
: isEnglish(locale)
|
||||
? "Structured trend layer mainly uses weather.gov and Open-Meteo for 6-48h warm/cold flow judgement."
|
||||
: "结构化来源以 weather.gov 和 Open-Meteo 为主,用于判断未来 6-48 小时冷暖平流趋势。",
|
||||
|
||||
@@ -42,7 +42,7 @@ const MESSAGES: Record<Locale, Record<string, string>> = {
|
||||
"guide.title": "📎 PolyWeather 系统技术说明",
|
||||
"guide.closeAria": "关闭技术说明",
|
||||
"guide.footer":
|
||||
"数据源以 Aviation Weather / METAR、Turkish MGM、Open-Meteo、weather.gov 为主,部分城市补充 Meteoblue。",
|
||||
"数据源以 Aviation Weather / METAR、Turkish MGM、Open-Meteo、weather.gov 为主。",
|
||||
|
||||
"history.title": "📊 历史准确率对账 - {city}",
|
||||
"history.closeAria": "关闭历史对账",
|
||||
@@ -143,7 +143,7 @@ const MESSAGES: Record<Locale, Record<string, string>> = {
|
||||
"guide.title": "📎 PolyWeather Technical Overview",
|
||||
"guide.closeAria": "Close technical overview",
|
||||
"guide.footer":
|
||||
"Primary data sources are Aviation Weather / METAR, Turkish MGM, Open-Meteo, and weather.gov, with Meteoblue added for selected cities.",
|
||||
"Primary data sources are Aviation Weather / METAR, Turkish MGM, Open-Meteo, and weather.gov.",
|
||||
|
||||
"history.title": "📊 Historical Reconciliation - {city}",
|
||||
"history.closeAria": "Close history reconciliation",
|
||||
|
||||
@@ -186,7 +186,6 @@ export interface ModelComparison {
|
||||
JMA?: number;
|
||||
MGM?: number;
|
||||
NWS?: number;
|
||||
Meteoblue?: number;
|
||||
}
|
||||
|
||||
export interface DEBAnalysis {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
@@ -151,7 +151,7 @@
|
||||
</div>
|
||||
<div class="guide-card">
|
||||
<h3>📆 未来日期分析</h3>
|
||||
<p>点击多日预报后打开的模态框,主要用于分析下一个交易日。 6-48 小时趋势以 weather.gov 和 Open-Meteo 为主,部分城市可会补充 Meteoblue; 0-2 小时临近判断则优先看 METAR 与周边站。</p>
|
||||
<p>点击多日预报后打开的模态框,主要用于分析下一个交易日。 6-48 小时趋势以 weather.gov 和 Open-Meteo 为主; 0-2 小时临近判断则优先看 METAR 与周边站。</p>
|
||||
</div>
|
||||
<div class="guide-card">
|
||||
<h3>📊 历史对账规则</h3>
|
||||
@@ -159,7 +159,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="guide-footer">
|
||||
<p>※ 数据源以 Aviation Weather / METAR、Turkish MGM、Open-Meteo、weather.gov 为主,部分城市补充 Meteoblue。</p>
|
||||
<p>※ 数据源以 Aviation Weather / METAR、Turkish MGM、Open-Meteo、weather.gov 为主。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -236,6 +236,3 @@
|
||||
|
||||
</html>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -175,7 +175,6 @@ def _append_future_forecast_lines(
|
||||
mgm_daily[date_key] = day_high
|
||||
mm_raw = weather_data.get("multi_model") or {}
|
||||
mm_daily = mm_raw.get("daily_forecasts", {}) if isinstance(mm_raw, dict) else {}
|
||||
mb_daily = (weather_data.get("meteoblue") or {}).get("daily_highs", []) or []
|
||||
nws_periods = (weather_data.get("nws") or {}).get("forecast_periods", []) or []
|
||||
|
||||
if len(dates) > 1:
|
||||
@@ -231,18 +230,6 @@ def _append_future_forecast_lines(
|
||||
lines.append("📅 " + " | ".join(future))
|
||||
return
|
||||
|
||||
if isinstance(mb_daily, list) and len(mb_daily) > 1:
|
||||
future = []
|
||||
for idx in range(1, min(3, len(mb_daily))):
|
||||
day_temp = _sf(mb_daily[idx])
|
||||
if day_temp is None:
|
||||
continue
|
||||
day = (local_now + timedelta(days=idx)).strftime("%m-%d")
|
||||
future.append(f"{day}: MB {day_temp:.1f}{temp_symbol}")
|
||||
if future:
|
||||
lines.append("📅 " + " | ".join(future))
|
||||
return
|
||||
|
||||
if isinstance(nws_periods, list) and nws_periods:
|
||||
future = []
|
||||
seen_days = set()
|
||||
@@ -358,14 +345,13 @@ def build_city_query_report(
|
||||
|
||||
nws_high = _sf((weather_data.get("nws") or {}).get("today_high"))
|
||||
mgm_high = _sf((mgm.get("today_high") if isinstance(mgm, dict) else None))
|
||||
mb_high = _sf((weather_data.get("meteoblue") or {}).get("today_high"))
|
||||
metar_max_so_far = _sf((metar.get("current") or {}).get("max_temp_so_far"))
|
||||
|
||||
today_t = _sf(max_temps[0]) if max_temps else None
|
||||
fallback_source = None
|
||||
metar_only_fallback = False
|
||||
if today_t is None:
|
||||
for source_name, candidate in (("MB", mb_high), ("NWS", nws_high), ("MGM", mgm_high)):
|
||||
for source_name, candidate in (("NWS", nws_high), ("MGM", mgm_high)):
|
||||
if candidate is not None:
|
||||
today_t = candidate
|
||||
fallback_source = source_name
|
||||
@@ -378,11 +364,6 @@ def build_city_query_report(
|
||||
sources = ["Open-Meteo"] if max_temps else []
|
||||
comp_parts: List[str] = []
|
||||
|
||||
if mb_high is not None:
|
||||
if "MB" not in sources:
|
||||
sources.append("MB")
|
||||
if fallback_source != "MB":
|
||||
comp_parts.append(f"MB: {mb_high:.1f}{temp_symbol}")
|
||||
if nws_high is not None:
|
||||
if "NWS" not in sources:
|
||||
sources.append("NWS")
|
||||
@@ -402,18 +383,10 @@ def build_city_query_report(
|
||||
if not sources:
|
||||
sources = ["N/A"]
|
||||
|
||||
divergence_warning = ""
|
||||
base_for_divergence = _sf(max_temps[0]) if max_temps else today_t
|
||||
if mb_high is not None and base_for_divergence is not None:
|
||||
diff = abs(mb_high - base_for_divergence)
|
||||
threshold = 5.0 if city_is_fahrenheit else 2.5
|
||||
if diff > threshold:
|
||||
divergence_warning = f" ⚠️ <b>模型显著分歧 ({diff:.1f}{temp_symbol})</b>"
|
||||
|
||||
comp_str = f" ({' | '.join(comp_parts)})" if comp_parts else ""
|
||||
msg_lines.append(f"\n📊 <b>预报 ({' | '.join(sources)})</b>")
|
||||
msg_lines.append(
|
||||
f"👉 <b>今天: {today_t_display}{temp_symbol}{comp_str}</b>{divergence_warning}"
|
||||
f"👉 <b>今天: {today_t_display}{temp_symbol}{comp_str}</b>"
|
||||
)
|
||||
|
||||
_append_future_forecast_lines(
|
||||
|
||||
@@ -62,7 +62,6 @@ def analyze_weather_trend(
|
||||
metar = weather_data.get("metar", {})
|
||||
open_meteo = weather_data.get("open-meteo", {})
|
||||
mgm = weather_data.get("mgm") or {}
|
||||
mb = weather_data.get("meteoblue", {})
|
||||
nws = weather_data.get("nws", {})
|
||||
|
||||
empty_result = ("", "", {})
|
||||
@@ -81,8 +80,6 @@ def analyze_weather_trend(
|
||||
current_forecasts: Dict[str, Optional[float]] = {}
|
||||
if daily.get("temperature_2m_max"):
|
||||
current_forecasts["Open-Meteo"] = _sf(daily.get("temperature_2m_max")[0])
|
||||
if mb.get("today_high") is not None:
|
||||
current_forecasts["Meteoblue"] = _sf(mb.get("today_high"))
|
||||
if nws.get("today_high") is not None:
|
||||
current_forecasts["NWS"] = _sf(nws.get("today_high"))
|
||||
|
||||
|
||||
@@ -1263,6 +1263,7 @@ class PolymarketReadOnlyLayer:
|
||||
|
||||
ranked: List[
|
||||
Tuple[
|
||||
float,
|
||||
float,
|
||||
float,
|
||||
Dict[str, Any],
|
||||
@@ -1315,6 +1316,7 @@ class PolymarketReadOnlyLayer:
|
||||
(
|
||||
market_prob,
|
||||
volume,
|
||||
bucket_temp,
|
||||
market,
|
||||
yes_token,
|
||||
no_token,
|
||||
@@ -1331,7 +1333,16 @@ class PolymarketReadOnlyLayer:
|
||||
max_items = max(1, int(limit or 4))
|
||||
primary_slug = str(primary_market.get("slug") or "").strip().lower()
|
||||
|
||||
for market_prob, _volume, market, yes_token, no_token, yes_prices, no_prices in ranked[
|
||||
for (
|
||||
market_prob,
|
||||
_volume,
|
||||
bucket_temp,
|
||||
market,
|
||||
yes_token,
|
||||
no_token,
|
||||
yes_prices,
|
||||
no_prices,
|
||||
) in ranked[
|
||||
:max_items
|
||||
]:
|
||||
yes_buy = _extract_price(yes_prices.get("buy"))
|
||||
|
||||
@@ -42,25 +42,10 @@ class WeatherDataCollector:
|
||||
"munich": ["EDDM", "EDMO", "EDJA"],
|
||||
}
|
||||
|
||||
# Meteoblue 仅在增益最大的城市启用(减少配额消耗与冗余请求)
|
||||
METEOBLUE_PRIORITY_CITIES = {
|
||||
"ankara",
|
||||
"london",
|
||||
"paris",
|
||||
"seoul",
|
||||
"toronto",
|
||||
"buenos aires",
|
||||
"wellington",
|
||||
"lucknow",
|
||||
"sao paulo",
|
||||
"munich",
|
||||
}
|
||||
|
||||
def __init__(self, config: dict):
|
||||
self.config = config
|
||||
weather_cfg = config.get("weather", {})
|
||||
self.wunderground_key = weather_cfg.get("wunderground_api_key")
|
||||
self.meteoblue_key = weather_cfg.get("meteoblue_api_key")
|
||||
|
||||
self.timeout = 30 # 增加超时以支持高延迟 VPS
|
||||
self.session = requests.Session()
|
||||
@@ -91,11 +76,6 @@ class WeatherDataCollector:
|
||||
)
|
||||
self._open_meteo_last_call_ts: float = 0.0
|
||||
self._open_meteo_call_lock = threading.Lock()
|
||||
self.meteoblue_cache_ttl_sec = int(
|
||||
os.getenv("METEOBLUE_CACHE_TTL_SEC", "7200")
|
||||
)
|
||||
self._meteoblue_cache: Dict[str, Dict] = {}
|
||||
self._meteoblue_cache_lock = threading.Lock()
|
||||
self.metar_cache_ttl_sec = int(
|
||||
os.getenv("METAR_CACHE_TTL_SEC", "600") # 默认 10 分钟
|
||||
)
|
||||
@@ -1558,105 +1538,6 @@ class WeatherDataCollector:
|
||||
return fallback
|
||||
return None
|
||||
|
||||
def fetch_from_meteoblue(
|
||||
self,
|
||||
lat: float,
|
||||
lon: float,
|
||||
timezone_name: str = "UTC",
|
||||
use_fahrenheit: bool = False,
|
||||
) -> Optional[Dict]:
|
||||
"""
|
||||
通过 Meteoblue 官方 API 获取高精度预测数据
|
||||
带本地缓存,避免频繁请求触发 429。
|
||||
"""
|
||||
if not self.meteoblue_key:
|
||||
logger.warning("Meteoblue API Key 未配置,跳过抓取。")
|
||||
return None
|
||||
|
||||
cache_key = f"{round(float(lat), 4)}:{round(float(lon), 4)}:{'f' if use_fahrenheit else 'c'}"
|
||||
now_ts = time.time()
|
||||
with self._meteoblue_cache_lock:
|
||||
cached = self._meteoblue_cache.get(cache_key)
|
||||
if (
|
||||
cached
|
||||
and now_ts - float(cached.get("t", 0)) < self.meteoblue_cache_ttl_sec
|
||||
):
|
||||
cached_data = cached.get("data")
|
||||
if isinstance(cached_data, dict):
|
||||
return dict(cached_data)
|
||||
|
||||
try:
|
||||
# 1. 调用官方 API (使用 basic-day 包,它是多模型 ML 融合结果)
|
||||
# 格式: https://my.meteoblue.com/packages/basic-day?apikey=KEY&lat=LAT&lon=LON&format=json
|
||||
url = "https://my.meteoblue.com/packages/basic-day"
|
||||
params = {
|
||||
"apikey": self.meteoblue_key,
|
||||
"lat": lat,
|
||||
"lon": lon,
|
||||
"format": "json",
|
||||
"as_daylight": "true",
|
||||
}
|
||||
|
||||
response = self.session.get(url, params=params, timeout=self.timeout)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
day_data = data.get("data_day", {})
|
||||
max_temps = day_data.get("temperature_max", [])
|
||||
|
||||
if not max_temps:
|
||||
logger.warning(
|
||||
f"Meteoblue API 返回数据中找不到最高温 (坐标: {lat},{lon})"
|
||||
)
|
||||
return None
|
||||
|
||||
# 2. 转换单位
|
||||
def c_to_f(c):
|
||||
return round((c * 9 / 5) + 32, 1)
|
||||
|
||||
result = {
|
||||
"source": "meteoblue",
|
||||
"today_high": None,
|
||||
"daily_highs": [],
|
||||
"unit": "fahrenheit" if use_fahrenheit else "celsius",
|
||||
"url": f"https://www.meteoblue.com/en/weather/week/{lat}N{lon}E", # 仅供参考
|
||||
}
|
||||
|
||||
# 提取今日最高
|
||||
mb_today_c = max_temps[0]
|
||||
result["today_high"] = c_to_f(mb_today_c) if use_fahrenheit else mb_today_c
|
||||
|
||||
# 提取接下来几天的最高温
|
||||
if use_fahrenheit:
|
||||
result["daily_highs"] = [c_to_f(t) for t in max_temps]
|
||||
else:
|
||||
result["daily_highs"] = max_temps
|
||||
|
||||
with self._meteoblue_cache_lock:
|
||||
self._meteoblue_cache[cache_key] = {
|
||||
"t": now_ts,
|
||||
"data": dict(result),
|
||||
}
|
||||
|
||||
logger.info(
|
||||
f"✅ Meteoblue API 获取成功 ({lat},{lon}): 今天 {result['today_high']}{result['unit']}"
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
status_code = getattr(getattr(e, "response", None), "status_code", None)
|
||||
if status_code == 429:
|
||||
logger.warning("Meteoblue API 限流(429),尝试使用本地缓存回退。")
|
||||
else:
|
||||
logger.error(f"Meteoblue API fetch failed: {e}")
|
||||
|
||||
with self._meteoblue_cache_lock:
|
||||
stale = self._meteoblue_cache.get(cache_key)
|
||||
if stale and isinstance(stale.get("data"), dict):
|
||||
fallback = dict(stale["data"])
|
||||
fallback["stale_cache"] = True
|
||||
return fallback
|
||||
return None
|
||||
|
||||
def extract_date_from_title(self, title: str) -> Optional[str]:
|
||||
"""
|
||||
从标题中提取日期并标准化为 YYYY-MM-DD
|
||||
@@ -1836,7 +1717,6 @@ class WeatherDataCollector:
|
||||
unit = "f" if use_fahrenheit else "c"
|
||||
open_meteo_key = f"{base}:14:{unit}"
|
||||
ensemble_key = f"{base}:{unit}"
|
||||
meteoblue_key = ensemble_key
|
||||
multi_model_key = ensemble_key
|
||||
|
||||
with self._open_meteo_cache_lock:
|
||||
@@ -1845,8 +1725,6 @@ class WeatherDataCollector:
|
||||
self._ensemble_cache.pop(ensemble_key, None)
|
||||
with self._multi_model_cache_lock:
|
||||
self._multi_model_cache.pop(multi_model_key, None)
|
||||
with self._meteoblue_cache_lock:
|
||||
self._meteoblue_cache.pop(meteoblue_key, None)
|
||||
|
||||
icao = self.get_icao_code(city)
|
||||
if icao:
|
||||
@@ -1962,16 +1840,6 @@ class WeatherDataCollector:
|
||||
# 获取时区偏移以过滤 METAR
|
||||
utc_offset = open_meteo.get("utc_offset", 0)
|
||||
|
||||
if city_lower in self.METEOBLUE_PRIORITY_CITIES:
|
||||
mb_data = self.fetch_from_meteoblue(
|
||||
lat,
|
||||
lon,
|
||||
timezone_name=open_meteo.get("timezone", "UTC"),
|
||||
use_fahrenheit=use_fahrenheit,
|
||||
)
|
||||
if mb_data:
|
||||
results["meteoblue"] = mb_data
|
||||
|
||||
# 对美国城市,额外获取 NWS 高精预报
|
||||
if use_fahrenheit:
|
||||
nws_data = self.fetch_nws(lat, lon)
|
||||
@@ -2023,15 +1891,6 @@ class WeatherDataCollector:
|
||||
if cluster_data:
|
||||
results["mgm_nearby"] = cluster_data
|
||||
|
||||
if city_lower in self.METEOBLUE_PRIORITY_CITIES:
|
||||
mb_data = self.fetch_from_meteoblue(
|
||||
lat,
|
||||
lon,
|
||||
timezone_name="UTC",
|
||||
use_fahrenheit=use_fahrenheit,
|
||||
)
|
||||
if mb_data:
|
||||
results["meteoblue"] = mb_data
|
||||
if use_fahrenheit:
|
||||
nws_data = self.fetch_nws(lat, lon)
|
||||
if nws_data:
|
||||
|
||||
@@ -19,7 +19,6 @@ def load_config():
|
||||
"openweather_api_key": get_env_or_none("OPENWEATHER_API_KEY"),
|
||||
"wunderground_api_key": get_env_or_none("WUNDERGROUND_API_KEY"),
|
||||
"visualcrossing_api_key": get_env_or_none("VISUALCROSSING_API_KEY"),
|
||||
"meteoblue_api_key": get_env_or_none("METEOBLUE_API_KEY"),
|
||||
"proxy": os.getenv("HTTPS_PROXY") or os.getenv("HTTP_PROXY"),
|
||||
},
|
||||
"telegram": {
|
||||
|
||||
@@ -62,7 +62,6 @@ def _make_weather_data(
|
||||
"p90": ens_p90,
|
||||
},
|
||||
"multi_model": {"forecasts": multi_model or {}},
|
||||
"meteoblue": {},
|
||||
"nws": {},
|
||||
}
|
||||
return data
|
||||
|
||||
@@ -252,13 +252,10 @@ def _analyze(city: str, force_refresh: bool = False) -> Dict[str, Any]:
|
||||
forecast_daily = [{"date": d, "max_temp": t} for d, t in zip(dates, maxtemps)]
|
||||
if om_today is None:
|
||||
nws_high = _sf(raw.get("nws", {}).get("today_high"))
|
||||
mb_high = _sf(raw.get("meteoblue", {}).get("today_high"))
|
||||
mgm_high = _sf(mgm.get("today_high")) if mgm else None
|
||||
fallback_high = (
|
||||
nws_high
|
||||
if nws_high is not None
|
||||
else mb_high
|
||||
if mb_high is not None
|
||||
else mgm_high
|
||||
if mgm_high is not None
|
||||
else max_so_far
|
||||
@@ -291,9 +288,6 @@ def _analyze(city: str, force_refresh: bool = False) -> Dict[str, Any]:
|
||||
nws_high = _sf(raw.get("nws", {}).get("today_high"))
|
||||
if nws_high is not None:
|
||||
current_forecasts["NWS"] = nws_high
|
||||
mb_high = _sf(raw.get("meteoblue", {}).get("today_high"))
|
||||
if mb_high is not None:
|
||||
current_forecasts["Meteoblue"] = mb_high
|
||||
mgm_high = _sf(mgm.get("today_high")) if mgm else None
|
||||
if mgm_high is not None:
|
||||
current_forecasts["MGM"] = mgm_high
|
||||
@@ -680,7 +674,6 @@ def _analyze(city: str, force_refresh: bool = False) -> Dict[str, Any]:
|
||||
},
|
||||
"source_forecasts": {
|
||||
"weather_gov": raw.get("nws") or {},
|
||||
"meteoblue": raw.get("meteoblue") or {},
|
||||
},
|
||||
"multi_model": {k: v for k, v in current_forecasts.items() if v is not None},
|
||||
"multi_model_daily": multi_model_daily,
|
||||
|
||||
Reference in New Issue
Block a user