diff --git a/web/services/city_api.py b/web/services/city_api.py index 97bdbb60..144937a4 100644 --- a/web/services/city_api.py +++ b/web/services/city_api.py @@ -20,6 +20,7 @@ from web.services.canonical_temperature import build_city_weather_from_canonical from web.services.latest_observation_overlay import ( overlay_latest_amos_observation, overlay_latest_amsc_observation, + overlay_latest_cwa_observation, overlay_latest_hko_observation, overlay_latest_jma_amedas_observation, overlay_latest_mgm_observation, @@ -695,6 +696,13 @@ async def _get_city_chart_data(city: str, *, force_refresh: bool) -> Dict[str, A fn=overlay_latest_hko_observation, args=(legacy_routes._CACHE_DB, city, payload), ) + payload = await _run_optional_city_chart_overlay( + city=city, + overlay_name="cwa_taipei", + payload=payload, + fn=overlay_latest_cwa_observation, + args=(legacy_routes._weather, city, payload), + ) return await _run_optional_city_chart_overlay( city=city, overlay_name="wunderground_current", @@ -752,6 +760,13 @@ async def _get_city_chart_data(city: str, *, force_refresh: bool) -> Dict[str, A fn=overlay_latest_hko_observation, args=(legacy_routes._CACHE_DB, city, payload), ) + payload = await _run_optional_city_chart_overlay( + city=city, + overlay_name="cwa_taipei", + payload=payload, + fn=overlay_latest_cwa_observation, + args=(legacy_routes._weather, city, payload), + ) return await _run_optional_city_chart_overlay( city=city, overlay_name="wunderground_current", diff --git a/web/services/latest_observation_overlay.py b/web/services/latest_observation_overlay.py index e92faa5b..118c1c95 100644 --- a/web/services/latest_observation_overlay.py +++ b/web/services/latest_observation_overlay.py @@ -711,7 +711,82 @@ def overlay_latest_amsc_observation( # ═══════════════════════════════════════════════════════════════════════════════ -# AMOS (Korean runway sensors — Seoul, Busan) +# CWA (Central Weather Administration — Taipei) +# ═══════════════════════════════════════════════════════════════════════════════ + + +def overlay_latest_cwa_observation(weather, city, payload): + normalized_city = str(city or payload.get("name") or payload.get("city") or "").strip().lower() + if normalized_city != "taipei" or not isinstance(payload, dict) or not payload: + return payload + + fetcher = getattr(weather, "fetch_cwa_taipei_settlement_current", None) + if not callable(fetcher): + return payload + try: + cwa_data = fetcher() + except Exception as exc: + logger.debug("latest CWA overlay fetch failed: {}", exc) + return payload + if not isinstance(cwa_data, dict): + return payload + + temp = _to_float((cwa_data.get("current") or {}).get("temp")) + obs_time = str(cwa_data.get("observation_time") or "").strip() + if temp is None or not obs_time: + return payload + + raw_epoch = parse_observation_epoch(obs_time) + if raw_epoch is None: + return payload + existing_epochs = [ + epoch + for epoch in ( + _block_epoch(payload.get("current")), + _block_epoch(payload.get("airport_primary")), + _block_epoch(payload.get("airport_current")), + ) + if epoch is not None + ] + if existing_epochs and max(existing_epochs) >= raw_epoch: + return payload + + update = { + "temp": round(float(temp), 1), + "source_code": "cwa", + "source_label": str(cwa_data.get("source_label") or "CWA").strip(), + "station_code": str(cwa_data.get("station_code") or "466920").strip(), + "station_name": str(cwa_data.get("station_name") or "\u81fa\u5317").strip(), + "observed_at": obs_time, + "obs_time": obs_time, + "observation_status": "live", + "city": normalized_city, + } + + next_payload = deepcopy(payload) + changed = False + + for key in ("current", "airport_primary", "airport_current"): + changed = _merge_observation_block(next_payload, key, update, raw_epoch) or changed + + canonical = next_payload.get("canonical_temperature") + canonical_epoch = _block_epoch(canonical) + if canonical_epoch is None or raw_epoch > canonical_epoch: + canonical_payload = build_canonical_temperature( + normalized_city, + { + "name": normalized_city, + "temp_symbol": next_payload.get("temp_symbol") or "\u00b0C", + "updated_at": obs_time, + "current": update, + }, + fetched_at=obs_time, + ) + if canonical_payload: + next_payload["canonical_temperature"] = canonical_payload + changed = True + + return next_payload if changed else payload # ═══════════════════════════════════════════════════════════════════════════════