Fix WU historical current observation merge

This commit is contained in:
2569718930@qq.com
2026-05-29 17:20:07 +08:00
parent 8f52b1d80c
commit 9e3a4e2f45
13 changed files with 859 additions and 9 deletions
+13
View File
@@ -790,6 +790,7 @@ def _analyze(
taf = raw.get("taf", {})
mgm = raw.get("mgm") or {}
settlement_current = raw.get("settlement_current") or {}
wunderground_current = raw.get("wunderground_current") or {}
ens_raw = raw.get("ensemble", {})
mm = raw.get("multi_model", {})
if not isinstance(om, dict):
@@ -800,6 +801,8 @@ def _analyze(
mgm = {}
if not isinstance(settlement_current, dict):
settlement_current = {}
if not isinstance(wunderground_current, dict):
wunderground_current = {}
if not isinstance(ens_raw, dict):
ens_raw = {}
if not isinstance(mm, dict):
@@ -1795,6 +1798,7 @@ def _analyze(
"last_observation_local_date": metar.get("observation_local_date") if metar else None,
"current_local_date": local_date_str,
},
"wunderground_current": wunderground_current,
"settlement_station": network_snapshot.get("settlement_station") or {},
"airport_primary": airport_primary_current,
"airport_primary_today_obs": network_snapshot.get("airport_primary_today_obs") or [],
@@ -1941,6 +1945,11 @@ def _analyze_summary(city: str, force_refresh: bool = False) -> Dict[str, Any]:
jobs: Dict[str, Any] = {
"settlement_current": lambda: _weather.fetch_settlement_current(city) or {},
"wunderground_current": lambda: _weather.fetch_wunderground_historical(
city,
use_fahrenheit=is_f,
utc_offset=default_utc_offset,
) or {},
"open_meteo": lambda: _weather.fetch_from_open_meteo(lat, lon, use_fahrenheit=is_f) or {},
"multi_model": lambda: _weather.fetch_multi_model(lat, lon, city=city, use_fahrenheit=is_f) or {},
}
@@ -1969,8 +1978,11 @@ def _analyze_summary(city: str, force_refresh: bool = False) -> Dict[str, Any]:
fetched[key] = future.result()
settlement_current = fetched.get("settlement_current") or {}
wunderground_current = fetched.get("wunderground_current") or {}
open_meteo = fetched.get("open_meteo") or {}
mm = fetched.get("multi_model") or {}
if not isinstance(wunderground_current, dict):
wunderground_current = {}
utc_offset = open_meteo.get("utc_offset")
if utc_offset is None:
utc_offset = default_utc_offset
@@ -2233,6 +2245,7 @@ def _analyze_summary(city: str, force_refresh: bool = False) -> Dict[str, Any]:
"obs_age_min": obs_age_min,
"observation_status": "live" if cur_temp is not None else "missing",
},
"wunderground_current": wunderground_current,
"deb": {
"prediction": _sf(deb_val),
"raw_prediction": _sf(deb_raw_val),
+2
View File
@@ -96,6 +96,7 @@ def _build_terminal_row(
"temp_symbol": data.get("temp_symbol"),
"current_temp": current.get("temp"),
"current_max_so_far": current.get("max_so_far"),
"wunderground_current": data.get("wunderground_current") or {},
"metar_context": metar_context,
"metar_today_obs": metar_context.get("today_obs") or [],
"metar_recent_obs": metar_context.get("recent_obs") or [],
@@ -207,6 +208,7 @@ def _build_quick_row(
"risk_level": risk.get("level"),
"current_temp": curr.get("temp"),
"current_max_so_far": curr.get("max_so_far"),
"wunderground_current": data.get("wunderground_current") or {},
"deb_prediction": deb.get("prediction"),
"model_cluster_sources": (
daily_entry.get("models")
+14 -6
View File
@@ -11,6 +11,14 @@ from loguru import logger
import web.routes as legacy_routes
async def _overlay_cached_wunderground(city: str, payload: Dict[str, Any]) -> Dict[str, Any]:
return await run_in_threadpool(
legacy_routes._overlay_latest_wunderground_current,
city,
payload,
)
def _build_cities_payload() -> Dict[str, Any]:
out = []
deb_recent_index = legacy_routes._build_recent_deb_performance_index()
@@ -86,7 +94,7 @@ async def get_city_detail_payload(
if cached_entry:
if not legacy_routes._city_cache_is_fresh(cached_entry, legacy_routes.CITY_FULL_CACHE_TTL_SEC):
return await run_in_threadpool(legacy_routes._refresh_city_full_cache, city, False)
return cached_entry.get("payload") or {}
return await _overlay_cached_wunderground(city, cached_entry.get("payload") or {})
return await run_in_threadpool(legacy_routes._refresh_city_full_cache, city, False)
if detail_mode == "panel":
if force_refresh:
@@ -95,7 +103,7 @@ async def get_city_detail_payload(
if cached_entry:
if not legacy_routes._city_cache_is_fresh(cached_entry, legacy_routes.CITY_PANEL_CACHE_TTL_SEC):
return await run_in_threadpool(legacy_routes._refresh_city_panel_cache, city, False)
return cached_entry.get("payload") or {}
return await _overlay_cached_wunderground(city, cached_entry.get("payload") or {})
return await run_in_threadpool(legacy_routes._refresh_city_panel_cache, city, False)
if detail_mode == "nearby":
if force_refresh:
@@ -104,7 +112,7 @@ async def get_city_detail_payload(
if cached_entry:
if not legacy_routes._city_cache_is_fresh(cached_entry, legacy_routes.CITY_NEARBY_CACHE_TTL_SEC):
return await run_in_threadpool(legacy_routes._refresh_city_nearby_cache, city, False)
return cached_entry.get("payload") or {}
return await _overlay_cached_wunderground(city, cached_entry.get("payload") or {})
return await run_in_threadpool(legacy_routes._refresh_city_nearby_cache, city, False)
if detail_mode == "market":
if force_refresh:
@@ -113,7 +121,7 @@ async def get_city_detail_payload(
if cached_entry:
if not legacy_routes._market_analysis_cache_is_fresh(cached_entry):
return await run_in_threadpool(legacy_routes._refresh_city_market_cache, city, False)
return cached_entry.get("payload") or {}
return await _overlay_cached_wunderground(city, cached_entry.get("payload") or {})
return await run_in_threadpool(legacy_routes._refresh_city_market_cache, city, False)
return await run_in_threadpool(legacy_routes._analyze, city, force_refresh, False, detail_mode)
@@ -131,7 +139,7 @@ async def get_city_summary_payload(
if cached_entry:
if not legacy_routes._city_cache_is_fresh(cached_entry, legacy_routes.CITY_SUMMARY_CACHE_TTL_SEC):
return await run_in_threadpool(legacy_routes._refresh_city_summary_cache, city, False)
return cached_entry.get("payload") or {}
return await _overlay_cached_wunderground(city, cached_entry.get("payload") or {})
return await run_in_threadpool(legacy_routes._refresh_city_summary_cache, city, False)
@@ -154,7 +162,7 @@ async def get_city_detail_aggregate_payload(
if not legacy_routes._city_cache_is_fresh(cached_entry, legacy_routes.CITY_FULL_CACHE_TTL_SEC):
data = await run_in_threadpool(legacy_routes._refresh_city_full_cache, city, False)
else:
data = cached_entry.get("payload") or {}
data = await _overlay_cached_wunderground(city, cached_entry.get("payload") or {})
else:
data = await run_in_threadpool(legacy_routes._refresh_city_full_cache, city, False)
+4
View File
@@ -28,6 +28,7 @@ def build_city_summary_payload(data: Dict[str, Any]) -> Dict[str, Any]:
),
},
"deb": dict(deb) if deb else {"prediction": None},
"wunderground_current": data.get("wunderground_current") or {},
"deviation_monitor": data.get("deviation_monitor") or {},
"risk": {
"level": data.get("risk", {}).get("level"),
@@ -212,6 +213,7 @@ def build_city_detail_payload(
),
"airport_primary": data.get("airport_primary") or {},
"airport_primary_today_obs": data.get("airport_primary_today_obs") or [],
"wunderground_current": data.get("wunderground_current") or {},
"official_nearby": data.get("official_nearby") or [],
"official_network_source": data.get("official_network_source"),
"official_network_status": data.get("official_network_status") or {},
@@ -224,6 +226,7 @@ def build_city_detail_payload(
"metar_recent_obs": data.get("metar_recent_obs") or [],
"metar_today_obs": data.get("metar_today_obs") or [],
"settlement_today_obs": data.get("settlement_today_obs") or [],
"wunderground_today_obs": (data.get("wunderground_current") or {}).get("today_obs") or [],
"hourly": data.get("hourly") or {},
"mgm_hourly": (data.get("mgm") or {}).get("hourly", []),
"forecast_daily": (data.get("forecast") or {}).get("daily", []),
@@ -266,6 +269,7 @@ def build_city_detail_payload(
"center_station_candidate": data.get("center_station_candidate"),
"airport_vs_network_delta": data.get("airport_vs_network_delta"),
"airport_current": data.get("airport_current") or {},
"wunderground_current": data.get("wunderground_current") or {},
"amos": data.get("amos") or {},
"nearby_source": data.get("nearby_source")
or (
+48
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
import os
import time
from copy import deepcopy
from datetime import datetime
from typing import Optional
@@ -53,6 +54,7 @@ from web.core import (
_resolve_auth_points, # noqa: F401 - compatibility export for tests and transitional routers
_resolve_weekly_profile, # noqa: F401 - compatibility export for tests and transitional routers
_sf,
_weather,
)
router = APIRouter()
@@ -329,6 +331,52 @@ def _refresh_city_full_cache(city: str, force_refresh: bool = False) -> dict:
return payload
def _overlay_wunderground_current(payload: dict, wunderground: dict) -> dict:
if not isinstance(payload, dict) or not isinstance(wunderground, dict) or not wunderground:
return payload
next_payload = deepcopy(payload)
next_payload["wunderground_current"] = dict(wunderground)
official = next_payload.get("official")
if isinstance(official, dict):
official["wunderground_current"] = dict(wunderground)
timeseries = next_payload.get("timeseries")
if isinstance(timeseries, dict):
timeseries["wunderground_today_obs"] = list(wunderground.get("today_obs") or [])
return next_payload
def _overlay_latest_wunderground_current(city: str, payload: dict) -> dict:
if not isinstance(payload, dict):
return payload
city_key = str(city or payload.get("name") or payload.get("city") or "").strip().lower()
if city_key not in CITIES:
return deepcopy(payload)
try:
utc_offset = int(
payload.get("utc_offset_seconds")
or (payload.get("overview") or {}).get("utc_offset_seconds")
or get_city_utc_offset_seconds(city_key)
or 0
)
except Exception:
utc_offset = get_city_utc_offset_seconds(city_key)
try:
latest_wu = _weather.fetch_wunderground_historical(
city_key,
use_fahrenheit=bool(CITIES[city_key].get("f")),
utc_offset=utc_offset,
)
except Exception as exc:
logger.warning("latest WU overlay failed city={} error={}", city_key, exc)
return deepcopy(payload)
if not isinstance(latest_wu, dict) or not latest_wu:
return deepcopy(payload)
return _overlay_wunderground_current(payload, latest_wu)
def _schedule_cache_refresh(
background_tasks: BackgroundTasks,