From d39534dff43fd6c6d35563c30b9260b354c3ff77 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Sun, 17 May 2026 18:04:05 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9C=BA=E5=9C=BA=E6=8E=A8=E9=80=81=E9=87=8D?= =?UTF-8?q?=E6=9E=84=EF=BC=9A=E8=A7=82=E6=B5=8B=E7=BC=93=E5=AD=98=E5=88=86?= =?UTF-8?q?=E7=A6=BB=20+=20=E5=85=A8=E5=9F=8E=E5=B8=82=E8=A6=86=E7=9B=96?= =?UTF-8?q?=20+=20=E5=9B=9B=E8=B7=AF=E5=B9=B6=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 force_refresh_observations_only 模式:机场推送仅刷新 METAR/AMOS 观测缓存,多模型预报缓存保留 15 分钟,杜绝 Open-Meteo 429 限流后 DEB 回退到实测温度的 bug - 砍掉夜间静默和温度状态机,每条新观测无条件推送 - HIGH_FREQ_AIRPORT_CITIES 从 19 城扩到 30 城(新增 11 个美国城市) - 美国城市走 airport_primary (MADIS) 优先取温 - 机场周期从串行改为 ThreadPoolExecutor(max_workers=4),周期时间从 ~120s 压缩到 ~33s Constraint: 单核 VPS 安全并发上限 Tested: docker compose up -d --build polyweather 重建后推送正常 --- src/data_collection/weather_sources.py | 43 ++- src/utils/telegram_push.py | 391 +++++++++++++------------ web/analysis_service.py | 16 +- 3 files changed, 242 insertions(+), 208 deletions(-) diff --git a/src/data_collection/weather_sources.py b/src/data_collection/weather_sources.py index a18e8196..b3d05152 100644 --- a/src/data_collection/weather_sources.py +++ b/src/data_collection/weather_sources.py @@ -727,24 +727,35 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour lat: Optional[float], lon: Optional[float], use_fahrenheit: bool, + *, + keep_model_caches: bool = False, ) -> None: - """Drop in-memory caches for one city before a force-refresh query.""" + """Drop in-memory caches for one city before a force-refresh query. + + When *keep_model_caches* is True (used by high-frequency observation + loops such as airport runway pushes), only observation-level caches + (METAR, AMOS, country networks, settlement) are evicted while the + longer-lived multi-model / ensemble / single-model forecast caches + are left intact so the DEB blending does not fall back to the + current observed temperature during an Open-Meteo rate-limit + cooldown. + """ if lat is not None and lon is not None: base = f"{round(float(lat), 4)}:{round(float(lon), 4)}" unit = "f" if use_fahrenheit else "c" - open_meteo_key = f"{base}:14:{unit}" - ensemble_key = f"{base}:{unit}" - cache_city = str(city or "").strip().lower() - multi_model_key = ( - f"{base}:{cache_city}:{unit}:{self.multi_model_cache_version}" - ) - - with self._open_meteo_cache_lock: - self._open_meteo_cache.pop(open_meteo_key, None) - with self._ensemble_cache_lock: - self._ensemble_cache.pop(ensemble_key, None) - with self._multi_model_cache_lock: - self._multi_model_cache.pop(multi_model_key, None) + if not keep_model_caches: + open_meteo_key = f"{base}:14:{unit}" + ensemble_key = f"{base}:{unit}" + cache_city = str(city or "").strip().lower() + multi_model_key = ( + f"{base}:{cache_city}:{unit}:{self.multi_model_cache_version}" + ) + with self._open_meteo_cache_lock: + self._open_meteo_cache.pop(open_meteo_key, None) + with self._ensemble_cache_lock: + self._ensemble_cache.pop(ensemble_key, None) + with self._multi_model_cache_lock: + self._multi_model_cache.pop(multi_model_key, None) icao = self.get_icao_code(city) if icao: @@ -1284,6 +1295,7 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour lon: float = None, country: str = None, force_refresh: bool = False, + force_refresh_observations_only: bool = False, include_taf: bool = True, include_nearby: bool = True, include_ensemble: bool = True, @@ -1298,12 +1310,13 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour use_fahrenheit = self._uses_fahrenheit(city_lower) supports_aviationweather = self._supports_aviationweather(city_lower) - if force_refresh: + if force_refresh or force_refresh_observations_only: self._evict_city_caches( city=city, lat=lat, lon=lon, use_fahrenheit=use_fahrenheit, + keep_model_caches=force_refresh_observations_only, ) self._log_temperature_unit(city, use_fahrenheit) self._attach_settlement_sources(results, city_lower) diff --git a/src/utils/telegram_push.py b/src/utils/telegram_push.py index a5c7ba41..4d75a87e 100644 --- a/src/utils/telegram_push.py +++ b/src/utils/telegram_push.py @@ -528,8 +528,24 @@ def _alert_signature(alert_payload: Dict[str, Any]) -> str: # ── high-freq airport push loop ── -HIGH_FREQ_AIRPORT_CITIES = {"seoul", "singapore", "busan", "tokyo", "ankara", "helsinki", "amsterdam", "istanbul", "paris", "hong kong", "lau fau shan", "taipei", "beijing", "shanghai", "guangzhou", "qingdao", "chengdu", "chongqing", "wuhan"} -HIGH_FREQ_AIRPORT_ICAO = {"seoul": "RKSI", "singapore": "WSSS", "busan": "RKPK", "tokyo": "44166", "ankara": "17128", "helsinki": "EFHK", "amsterdam": "EHAM", "istanbul": "17058", "paris": "LFPB", "hong kong": "HKO", "lau fau shan": "LFS", "taipei": "466920", "beijing": "ZBAA", "shanghai": "ZSPD", "guangzhou": "ZGGG", "qingdao": "ZSQD", "chengdu": "ZUUU", "chongqing": "ZUCK", "wuhan": "ZHHH"} +HIGH_FREQ_AIRPORT_CITIES = { + "seoul", "singapore", "busan", "tokyo", "ankara", "helsinki", "amsterdam", + "istanbul", "paris", "hong kong", "lau fau shan", "taipei", + "beijing", "shanghai", "guangzhou", "qingdao", "chengdu", "chongqing", "wuhan", + "new york", "los angeles", "chicago", "denver", "atlanta", + "miami", "san francisco", "houston", "dallas", "austin", "seattle", +} +HIGH_FREQ_AIRPORT_ICAO = { + "seoul": "RKSI", "singapore": "WSSS", "busan": "RKPK", "tokyo": "44166", + "ankara": "17128", "helsinki": "EFHK", "amsterdam": "EHAM", "istanbul": "17058", + "paris": "LFPB", "hong kong": "HKO", "lau fau shan": "LFS", "taipei": "466920", + "beijing": "ZBAA", "shanghai": "ZSPD", "guangzhou": "ZGGG", "qingdao": "ZSQD", + "chengdu": "ZUUU", "chongqing": "ZUCK", "wuhan": "ZHHH", + "new york": "KLGA", "los angeles": "KLAX", "chicago": "KORD", + "denver": "KBKF", "atlanta": "KATL", "miami": "KMIA", + "san francisco": "KSFO", "houston": "KHOU", "dallas": "KDAL", + "austin": "KAUS", "seattle": "KSEA", +} FOCUS_RUNWAY_PAIRS = { "chongqing": {("02L", "20R")}, "shanghai": {("17L", "35R")}, @@ -853,7 +869,11 @@ def _build_airport_status_message( "hong kong": "Observatory", "lau fau shan": "Lau Fau Shan", "taipei": "Songshan", "beijing": "Capital", "shanghai": "Pudong", "guangzhou": "Baiyun", "shenzhen": "Bao'an", "qingdao": "Jiaodong", - "chengdu": "Shuangliu", "chongqing": "Jiangbei", "wuhan": "Tianhe"} + "chengdu": "Shuangliu", "chongqing": "Jiangbei", "wuhan": "Tianhe", + "new york": "LaGuardia", "los angeles": "LAX", "chicago": "O'Hare", + "denver": "Buckley", "atlanta": "Hartsfield", "miami": "Intl", + "san francisco": "SFO", "houston": "Hobby", "dallas": "Love Field", + "austin": "Bergstrom", "seattle": "Sea-Tac"} en_name = city.title() ap_name = _AIRPORT_EN.get(city, "") time_suffix = f" · {local_time}" if local_time else "" @@ -984,25 +1004,14 @@ def _get_airport_daily_high(city_weather: Dict[str, Any]): # Per-city push interval — unified to 60s, obs_time dedup prevents spam _AIRPORT_PUSH_INTERVAL = { - "seoul": 60, - "busan": 60, - "tokyo": 60, - "ankara": 60, - "helsinki": 60, - "amsterdam": 60, - "istanbul": 60, - "paris": 60, - "hong kong": 60, - "lau fau shan": 60, - "singapore": 60, - "taipei": 60, - "beijing": 60, - "shanghai": 60, - "guangzhou": 60, - "qingdao": 60, - "chengdu": 60, - "chongqing": 60, - "wuhan": 60, + "seoul": 60, "busan": 60, "tokyo": 60, "ankara": 60, + "helsinki": 60, "amsterdam": 60, "istanbul": 60, "paris": 60, + "hong kong": 60, "lau fau shan": 60, "singapore": 60, "taipei": 60, + "beijing": 60, "shanghai": 60, "guangzhou": 60, "qingdao": 60, + "chengdu": 60, "chongqing": 60, "wuhan": 60, + "new york": 60, "los angeles": 60, "chicago": 60, "denver": 60, + "atlanta": 60, "miami": 60, "san francisco": 60, "houston": 60, + "dallas": 60, "austin": 60, "seattle": 60, } # Per-city temperature window threshold (°C below DEB predicted high) # Continental airports: wider window (temp rises steadily over land) @@ -1068,6 +1077,145 @@ def _check_rising_trend(icao: str) -> bool: return False +def _process_airport_city( + city: str, + now_ts: int, + last_city: dict, + chat_ids: List[str], + bot: Any, +) -> tuple | None: + """Process one airport city and return (city, new_state_entry) or None. + + This is the per-city unit used by the concurrent thread pool in + ``_run_high_freq_airport_cycle``. + """ + last_city_ts = int(last_city.get("ts") or 0) + last_obs_time = str(last_city.get("obs_time") or "") + city_interval = _AIRPORT_PUSH_INTERVAL.get(city, 600) + if now_ts - last_city_ts < city_interval: + return None + + from web.app import _analyze # lazy import — only the bot process needs it + + city_weather: Dict[str, Any] = {} + deb_pred: Optional[float] = None + try: + city_weather = _analyze(city, force_refresh_observations_only=True) + deb_raw = (city_weather.get("deb") or {}).get("prediction") + if deb_raw is not None: + deb_pred = float(deb_raw) + except Exception: + logger.exception("airport analyze failed for city={}", city) + return None + + # Extract airport-level temperature + amos = city_weather.get("amos") or {} + mgm_nearby = city_weather.get("mgm_nearby") or [] + airport_icao = HIGH_FREQ_AIRPORT_ICAO.get(city, "") + airport_row = None + for row in mgm_nearby: + if str(row.get("istNo") or "") == airport_icao or str(row.get("icao") or "") == airport_icao: + airport_row = row + break + if not airport_row: + airport_row = mgm_nearby[0] if mgm_nearby else {} + station_temp = airport_row.get("temp") if airport_row else None + current_obs_time = str(airport_row.get("obs_time") or "") + + runway_obs = (amos.get("runway_obs") or {}) + runway_pairs = runway_obs.get("runway_pairs") or [] + runway_temps = runway_obs.get("temperatures") or [] + runway_pairs, runway_temps, _point_temps = _select_focus_runway_obs( + city, runway_pairs, runway_temps, + runway_obs.get("point_temperatures") or [], + ) + if runway_temps: + valid_temps = [t for (t, _d) in runway_temps if t is not None] + if valid_temps: + station_temp = max(valid_temps) + amos_obs_time = amos.get("observation_time") or "" + if amos_obs_time: + current_obs_time = amos_obs_time + + current_temp = station_temp + if current_temp is None: + airport_primary = city_weather.get("airport_primary") or {} + current_temp = airport_primary.get("temp") or (city_weather.get("current") or {}).get("temp") + if not current_obs_time: + current_obs_time = str(airport_primary.get("obs_time") or "") + if city == "paris": + arome_temp = _fetch_arome_temp() + if arome_temp is not None: + current_temp = arome_temp + city_weather.setdefault("current", {})["temp"] = arome_temp + if not current_obs_time: + current_obs_time = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S") + if current_temp is None or deb_pred is None: + return None + + # Dedup: same observation → skip (with delayed retry for HK / LFS) + _CITIES_WITH_DELAYED_API = {"hong kong", "lau fau shan"} + if (current_obs_time and last_obs_time and current_obs_time == last_obs_time + and city in _CITIES_WITH_DELAYED_API + and now_ts - last_city_ts > 540): + time.sleep(4) + try: + city_weather = _analyze(city, force_refresh_observations_only=True) + deb_raw2 = (city_weather.get("deb") or {}).get("prediction") + if deb_raw2 is not None: + deb_pred = float(deb_raw2) + mgm_nearby2 = city_weather.get("mgm_nearby") or [] + row2 = None + for r in mgm_nearby2: + if str(r.get("istNo") or "") == airport_icao or str(r.get("icao") or "") == airport_icao: + row2 = r + break + if not row2 and mgm_nearby2: + row2 = mgm_nearby2[0] + retry_obs = str(row2.get("obs_time") or "") if row2 else "" + if retry_obs and retry_obs != last_obs_time: + current_obs_time = retry_obs + station_temp = row2.get("temp") if row2 else None + current_temp = station_temp or (city_weather.get("current") or {}).get("temp") + if current_temp is None or deb_pred is None: + return None + else: + return None + except Exception: + return None + elif current_obs_time and last_obs_time and current_obs_time == last_obs_time: + return None + + obs_local = ( + ((city_weather.get("amos") or {}).get("observation_time_local") or "")[11:16] + if len(str((city_weather.get("amos") or {}).get("observation_time_local") or "")) >= 16 + else (city_weather.get("airport_current") or {}).get("obs_time") + or city_weather.get("local_time") + or "" + ) + message = _build_airport_status_message(city, city_weather, deb_pred, obs_local, state="") + + # Send to all target chats + sent = False + for chat_id in chat_ids: + try: + kwargs = {} + thread_id = _resolve_thread_id(chat_id, city) + if thread_id: + kwargs["message_thread_id"] = thread_id + bot.send_message(chat_id, message, **kwargs) + sent = True + except Exception as exc: + logger.warning("airport push failed city={} chat_id={}: {}", city, chat_id, exc) + + if sent: + logger.info("airport status pushed city={} temp={} deb={} obs_time={}", + city, current_temp, deb_pred, current_obs_time) + return (city, {"ts": now_ts, "active": True, "obs_time": current_obs_time}) + + return None + + def _run_high_freq_airport_cycle( bot: Any, config: Dict[str, Any], @@ -1077,170 +1225,32 @@ def _run_high_freq_airport_cycle( state_dirty = False now_ts = int(time.time()) last_by_city = state.setdefault("last_by_city", {}) + logger.info("airport cycle tick cities={}", len(HIGH_FREQ_AIRPORT_CITIES)) - for city in sorted(HIGH_FREQ_AIRPORT_CITIES): - try: - last_city = last_by_city.get(city) or {} - last_city_ts = int(last_city.get("ts") or 0) - last_obs_time = str(last_city.get("obs_time") or "") - city_interval = _AIRPORT_PUSH_INTERVAL.get(city, 600) - if now_ts - last_city_ts < city_interval: - continue - - city_weather: Dict[str, Any] = {} - deb_pred: Optional[float] = None - try: - from web.app import _analyze - city_weather = _analyze(city, force_refresh=True) - deb_raw = (city_weather.get("deb") or {}).get("prediction") - if deb_raw is not None: - deb_pred = float(deb_raw) - except Exception: - pass - - # Extract airport-level temperature - amos = city_weather.get("amos") or {} - mgm_nearby = city_weather.get("mgm_nearby") or [] - airport_icao = HIGH_FREQ_AIRPORT_ICAO.get(city, "") - airport_row = None - for row in mgm_nearby: - if str(row.get("istNo") or "") == airport_icao or str(row.get("icao") or "") == airport_icao: - airport_row = row - break - if not airport_row: - airport_row = mgm_nearby[0] if mgm_nearby else {} - station_temp = airport_row.get("temp") if airport_row else None - current_obs_time = str(airport_row.get("obs_time") or "") - - runway_obs = (amos.get("runway_obs") or {}) - runway_pairs = runway_obs.get("runway_pairs") or [] - runway_temps = runway_obs.get("temperatures") or [] - runway_pairs, runway_temps, _point_temps = _select_focus_runway_obs( + cities = sorted(HIGH_FREQ_AIRPORT_CITIES) + with ThreadPoolExecutor(max_workers=4) as pool: + futures = { + pool.submit( + _process_airport_city, city, - runway_pairs, - runway_temps, - runway_obs.get("point_temperatures") or [], - ) - if runway_temps: - valid_temps = [t for (t, _d) in runway_temps if t is not None] - if valid_temps: - station_temp = max(valid_temps) - amos_obs_time = amos.get("observation_time") or "" - if amos_obs_time: - current_obs_time = amos_obs_time - - current_temp = station_temp - if current_temp is None: - current_temp = (city_weather.get("current") or {}).get("temp") - if city == "paris": - arome_temp = _fetch_arome_temp() - if arome_temp is not None: - current_temp = arome_temp - city_weather.setdefault("current", {})["temp"] = arome_temp - if not current_obs_time: - current_obs_time = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S") - if current_temp is None or deb_pred is None: - continue - - # 基于原始观测数据时间的去重:同一条观测不重复推送 - # HK/LFS 数据在 x7 分发布,API 可能有 3-5s 延迟, - # obs_time 未变但距上次推送已超 9min → 等 4s 重拉一次 - _CITIES_WITH_DELAYED_API = {"hong kong", "lau fau shan"} - if (current_obs_time and last_obs_time and current_obs_time == last_obs_time - and city in _CITIES_WITH_DELAYED_API - and now_ts - last_city_ts > 540): - time.sleep(4) - try: - city_weather = _analyze(city, force_refresh=True) - deb_raw2 = (city_weather.get("deb") or {}).get("prediction") - if deb_raw2 is not None: - deb_pred = float(deb_raw2) - mgm_nearby2 = city_weather.get("mgm_nearby") or [] - row2 = None - for r in mgm_nearby2: - if str(r.get("istNo") or "") == airport_icao or str(r.get("icao") or "") == airport_icao: - row2 = r - break - if not row2 and mgm_nearby2: - row2 = mgm_nearby2[0] - retry_obs = str(row2.get("obs_time") or "") if row2 else "" - if retry_obs and retry_obs != last_obs_time: - current_obs_time = retry_obs - station_temp = row2.get("temp") if row2 else None - current_temp = station_temp or (city_weather.get("current") or {}).get("temp") - if current_temp is None or deb_pred is None: - continue - else: - continue - except Exception: - continue - elif current_obs_time and last_obs_time and current_obs_time == last_obs_time: - continue - - # ── 热度状态机 ── - daily_high, max_time = _get_airport_daily_high(city_weather) - gap = (daily_high - current_temp) if daily_high is not None and current_temp is not None else None + now_ts, + last_by_city.get(city) or {}, + chat_ids, + bot, + ): city + for city in cities + } + for future in as_completed(futures): try: - h = int(str(city_weather.get("local_time") or "00")[:2]) - except ValueError: - h = 0 - rising = _check_rising_trend(airport_icao) if city != "paris" else True - - # 夜间 (20:00–06:00):🌙 无有效升温窗口,跳过 - if h >= 20 or h <= 5: + result = future.result() + except Exception: + logger.exception("airport city task crashed city={}", futures[future]) continue - - # 早晨 (06:00–09:00):"今日最高"可能只是凌晨刚形成的首个观测值 - # 只有日高已稳定存在一段时间,或当前已明显高于凌晨基线,才有意义 - high_formed = ( - max_time is not None - and daily_high is not None - and current_temp is not None - and (h >= 9 or current_temp > daily_high + 0.5) - ) - - # 判定状态 - if current_temp is not None and daily_high is not None and current_temp > daily_high + 0.3: - if deb_pred is not None and current_temp > deb_pred: - state = "\U0001f680 超预期" - else: - state = "\U0001f525 冲高中" - elif high_formed and gap is not None and gap <= 1.0: - state = "⚠️ 冲顶观察" - elif high_formed and gap is not None and gap <= 2.0 and rising: - state = "\U0001f525 升温中" - elif gap is not None and gap <= 3.0: - state = "❄️ 降温中" - else: + if result is None: continue - - # 用观测数据时间而非当前本地时间 - airport_cur = city_weather.get("airport_current") or {} - amos_obs = (city_weather.get("amos") or {}).get("observation_time_local") or "" - if amos_obs and len(str(amos_obs)) >= 16: - amos_obs = str(amos_obs)[11:16] # "2026-05-15 17:32:00" → "17:32" - obs_local = amos_obs or airport_cur.get("obs_time") or city_weather.get("local_time") or "" - message = _build_airport_status_message(city, city_weather, deb_pred, obs_local, state=state) - - sent = False - for chat_id in chat_ids: - try: - kwargs = {} - thread_id = _resolve_thread_id(chat_id, city) - if thread_id: - kwargs["message_thread_id"] = thread_id - bot.send_message(chat_id, message, **kwargs) - sent = True - except Exception as exc: - logger.warning("airport push failed city={} chat_id={}: {}", city, chat_id, exc) - - if sent: - last_by_city[city] = {"ts": now_ts, "active": True, "obs_time": current_obs_time} - state_dirty = True - logger.info("airport status pushed city={} temp={} deb={} obs_time={}", city, current_temp, deb_pred, current_obs_time) - - except Exception: - logger.exception("airport cycle failed for city={}", city) + city, entry = result + last_by_city[city] = entry + state_dirty = True return state_dirty @@ -1265,14 +1275,17 @@ def start_high_freq_airport_push_loop(bot: Any, config: Dict[str, Any]) -> Optio ) while True: cycle_started = time.time() - state = _load_airport_state() - if _run_high_freq_airport_cycle( - bot=bot, - config=config, - chat_ids=chat_ids, - state=state, - ): - _save_airport_state(state) + try: + state = _load_airport_state() + if _run_high_freq_airport_cycle( + bot=bot, + config=config, + chat_ids=chat_ids, + state=state, + ): + _save_airport_state(state) + except Exception: + logger.exception("airport push cycle crashed") elapsed = time.time() - cycle_started sleep_sec = max(5, interval_sec - int(elapsed)) diff --git a/web/analysis_service.py b/web/analysis_service.py index cbb79199..9621ca4d 100644 --- a/web/analysis_service.py +++ b/web/analysis_service.py @@ -1656,11 +1656,18 @@ def _archive_intraday_path_snapshot(city: str, result: Dict[str, Any]) -> None: def _analyze( city: str, force_refresh: bool = False, + force_refresh_observations_only: bool = False, include_llm_commentary: bool = False, detail_mode: str = "full", ) -> Dict[str, Any]: - """Fetch, analyse, and return structured weather data for one city.""" - # Check cache + """Fetch, analyse, and return structured weather data for one city. + + Set *force_refresh_observations_only* to True for high-frequency + observation loops that need fresh METAR/AMOS/runway data but should + keep the longer-lived multi-model forecast caches intact so the DEB + blending does not fall back to the current observed temperature. + """ + # Check cache – skip when explicitly refreshing observations ttl = _analysis_ttl_for_city(city) normalized_detail_mode_raw = str(detail_mode or "full").strip().lower() if normalized_detail_mode_raw == "panel": @@ -1672,8 +1679,8 @@ def _analyze( else: normalized_detail_mode = "full" cache_key = _analysis_cache_key(city, normalized_detail_mode) - - if not force_refresh: + + if not force_refresh and not force_refresh_observations_only: cached = _cache.get(cache_key) if cached and _time.time() - cached["t"] < ttl: if include_llm_commentary: @@ -1707,6 +1714,7 @@ def _analyze( lat=lat, lon=lon, force_refresh=force_refresh, + force_refresh_observations_only=force_refresh_observations_only, include_taf=not is_panel_mode and not is_nearby_mode and not is_market_mode, include_nearby=not is_panel_mode and not is_market_mode, include_ensemble=not is_panel_mode and not is_nearby_mode and not is_market_mode,