diff --git a/web/services/city_api.py b/web/services/city_api.py index 0d6d24af..141d05a3 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_hko_observation, overlay_latest_jma_amedas_observation, overlay_latest_mgm_observation, parse_observation_epoch, @@ -687,6 +688,13 @@ async def _get_city_chart_data(city: str, *, force_refresh: bool) -> Dict[str, A fn=overlay_latest_mgm_observation, args=(legacy_routes._CACHE_DB, city, payload), ) + payload = await _run_optional_city_chart_overlay( + city=city, + overlay_name="hko_latest_raw", + payload=payload, + fn=overlay_latest_hko_observation, + args=(legacy_routes._CACHE_DB, city, payload), + ) return await _run_optional_city_chart_overlay( city=city, overlay_name="wunderground_current", @@ -737,6 +745,13 @@ async def _get_city_chart_data(city: str, *, force_refresh: bool) -> Dict[str, A fn=overlay_latest_mgm_observation, args=(legacy_routes._CACHE_DB, city, payload), ) + payload = await _run_optional_city_chart_overlay( + city=city, + overlay_name="hko_latest_raw", + payload=payload, + fn=overlay_latest_hko_observation, + args=(legacy_routes._CACHE_DB, 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 4c581fa3..e92faa5b 100644 --- a/web/services/latest_observation_overlay.py +++ b/web/services/latest_observation_overlay.py @@ -793,7 +793,79 @@ def overlay_latest_amos_observation(db, city, payload): # ═══════════════════════════════════════════════════════════════════════════════ -# MGM (Turkish State Meteorological Service — Ankara, Istanbul) +# HKO (Hong Kong Observatory — Hong Kong, Shenzhen) +# ═══════════════════════════════════════════════════════════════════════════════ + + +def _latest_hko_row(db, city): + getter = getattr(db, "get_latest_raw_observation", None) + if not callable(getter): + return None, None + try: + row = getter("hko_obs", city) + except Exception as exc: + logger.debug("latest HKO raw overlay read failed city={}: {}", city, exc) + return None, None + if not isinstance(row, dict): + return None, None + raw_payload = row.get("payload") + if isinstance(raw_payload, dict) and raw_payload: + temp = raw_payload.get("temp") + if temp is not None: + return row, raw_payload + return None, None + + +def _raw_hko_epoch(row, raw_payload): + payload_values = (raw_payload.get("obs_time"),) + parsed = [ + epoch + for epoch in (parse_observation_epoch(value) for value in payload_values) + if epoch is not None + ] + if parsed: + return max(parsed) + return parse_observation_epoch(row.get("observed_at")) + + +def overlay_latest_hko_observation(db, city, payload): + normalized_city = str(city or payload.get("name") or payload.get("city") or "").strip().lower() + if not normalized_city or not isinstance(payload, dict) or not payload: + return payload + row, raw_payload = _latest_hko_row(db, normalized_city) + if not isinstance(row, dict) or not isinstance(raw_payload, dict): + return payload + raw_epoch = _raw_hko_epoch(row, raw_payload) + if raw_epoch is None: + return payload + update = _raw_observation_update(normalized_city, row, raw_payload) + if not update: + return payload + + 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": row.get("fetched_at"), + "current": update, + }, + fetched_at=str(row.get("fetched_at") or ""), + ) + if canonical_payload: + next_payload["canonical_temperature"] = canonical_payload + changed = True + + return next_payload if changed else payload # ═══════════════════════════════════════════════════════════════════════════════