From e6ea72c4583cf12e6a7e387c466d0f9e926fb12c Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Mon, 15 Jun 2026 00:09:12 +0800 Subject: [PATCH] Prefer latest AMSC data for Telegram runway pushes --- src/utils/telegram_push.py | 79 ++++++++++++++++++++++++++++++--- tests/test_telegram_hashtags.py | 78 ++++++++++++++++++++++++++++++++ 2 files changed, 150 insertions(+), 7 deletions(-) diff --git a/src/utils/telegram_push.py b/src/utils/telegram_push.py index 22fb5eb8..b7ffe09c 100644 --- a/src/utils/telegram_push.py +++ b/src/utils/telegram_push.py @@ -1577,12 +1577,36 @@ def _read_cached_airport_city_weather(city: str, max_age_sec: Optional[int] = No return None -def _read_canonical_airport_city_weather(city: str) -> Optional[Dict[str, Any]]: +def _attach_latest_raw_observation_payload( + db: Any, + city: str, + payload: Dict[str, Any], +) -> Dict[str, Any]: + canonical = payload.get("canonical_temperature") or {} + source = str(canonical.get("source") or "").strip().lower() + if source != "amsc_awos": + return payload + getter = getattr(db, "get_latest_raw_observation", None) + if not callable(getter): + return payload + try: + row = getter("amsc_awos", city) + except Exception as exc: + logger.debug("airport push latest raw observation read failed city={}: {}", city, exc) + return payload + raw_payload = row.get("payload") if isinstance(row, dict) else None + if isinstance(raw_payload, dict) and raw_payload: + payload = dict(payload) + payload["amos"] = dict(raw_payload) + return payload + + +def _read_canonical_airport_city_weather(city: str, db: Optional[Any] = None) -> Optional[Dict[str, Any]]: normalized_city = (city or "").strip().lower() if not normalized_city: return None try: - db = DBManager() + db = db or DBManager() getter = getattr(db, "get_canonical_temperature", None) if not callable(getter): return None @@ -1595,17 +1619,58 @@ def _read_canonical_airport_city_weather(city: str) -> Optional[Dict[str, Any]]: canonical = row.get("payload") or row if not isinstance(canonical, dict): return None - return build_city_weather_from_canonical(normalized_city, canonical) + payload = build_city_weather_from_canonical(normalized_city, canonical) + if not isinstance(payload, dict): + return None + return _attach_latest_raw_observation_payload(db, normalized_city, payload) + + +def _merge_airport_push_context( + latest_payload: Dict[str, Any], + cached_payload: Optional[Dict[str, Any]], +) -> Dict[str, Any]: + if not isinstance(cached_payload, dict) or not cached_payload: + return latest_payload + merged = dict(latest_payload) + + def has_useful_context(value: Any) -> bool: + if isinstance(value, dict): + return any(item not in (None, "", [], {}) for item in value.values()) + if isinstance(value, list): + return bool(value) + return value is not None + + for key in ( + "deb", + "multi_model", + "multi_model_daily", + "forecast", + "probabilities", + "peak", + "local_time", + "local_date", + "temp_symbol", + "risk", + ): + value = cached_payload.get(key) + if not has_useful_context(value): + continue + if key not in merged or not has_useful_context(merged.get(key)): + merged[key] = value + return merged def _load_airport_city_weather_for_push(city: str) -> Dict[str, Any]: cached = _read_cached_airport_city_weather(city) - if cached is not None: - return cached - canonical = _read_canonical_airport_city_weather(city) if canonical is not None: - return canonical + cached_ts = _cached_payload_observation_epoch(cached or {}) + canonical_ts = _cached_payload_observation_epoch(canonical) + if cached is None or (canonical_ts or 0) > (cached_ts or 0): + return _merge_airport_push_context(canonical, cached) + + if cached is not None: + return cached raise RuntimeError(f"no cached city weather for airport push city={city}") diff --git a/tests/test_telegram_hashtags.py b/tests/test_telegram_hashtags.py index d38cd4f7..138f6e30 100644 --- a/tests/test_telegram_hashtags.py +++ b/tests/test_telegram_hashtags.py @@ -335,6 +335,84 @@ def test_airport_push_without_cache_uses_canonical_latest_not_analysis(monkeypat assert city_weather["current"]["freshness"]["freshness_status"] == "fresh" +def test_airport_push_prefers_newer_amsc_canonical_over_stale_cache(monkeypatch): + import src.utils.telegram_push as telegram_push + + class FakeDB: + def get_city_cache(self, kind, city): + if kind != "panel": + return None + assert city == "qingdao" + return { + "updated_at_ts": 1000.0, + "payload": { + "local_time": "12:00", + "current": { + "temp": 29.0, + "source_code": "metar", + "observed_at": "2026-06-14T04:00:00+00:00", + }, + "airport_primary": { + "temp": 29.0, + "source_code": "metar", + "obs_time": "2026-06-14T04:00:00+00:00", + }, + "deb": {"prediction": 31.5}, + }, + } + + def get_canonical_temperature(self, city): + assert city == "qingdao" + return { + "payload": { + "city": "qingdao", + "value": 30.8, + "source": "amsc_awos", + "source_label": "AMSC AWOS runway-point air temperature", + "source_role": "settlement_proxy", + "observed_at": "2026-06-14T10:44:00+00:00", + "observed_at_local": "18:44", + "freshness_sec": 30, + "freshness_status": "fresh", + "fetched_at": "2026-06-14T10:44:30+00:00", + "confidence": 0.92, + }, + } + + def get_latest_raw_observation(self, source, city): + assert (source, city) == ("amsc_awos", "qingdao") + return { + "payload": { + "source": "amsc_awos", + "temp_c": 30.8, + "observation_time": "2026-06-14T18:44:00+08:00", + "runway_obs": { + "runway_pairs": [("16", "34")], + "temperatures": [(30.8, None)], + "point_temperatures": [ + { + "runway": "16/34", + "tdz_temp": 30.2, + "mid_temp": 30.8, + "end_temp": 30.6, + "target_runway_max": 30.6, + } + ], + }, + } + } + + monkeypatch.setattr(telegram_push, "DBManager", lambda: FakeDB()) + + city_weather = telegram_push._load_airport_city_weather_for_push("qingdao") + + assert city_weather["current"]["source_code"] == "amsc_awos" + assert city_weather["current"]["temp"] == 30.8 + assert city_weather["deb"]["prediction"] == 31.5 + assert city_weather["amos"]["source"] == "amsc_awos" + assert city_weather["amos"]["runway_obs"]["point_temperatures"][0]["runway"] == "16/34" + + def test_airport_push_without_cache_or_canonical_skips_analysis(monkeypatch): import src.utils.telegram_push as telegram_push import web.app as web_app