新增 CoWIN 6087 数据源:香港保良局陳守仁小學 1 分钟参考站

- cowin_sources.py:从 cowin.hku.hk API 抓取站 6087 的逐分钟温度
- weather_sources.py:CowinSourceMixin 接入采集链,优先级高于 HKO
- country_networks.py:_airport_primary_from_raw 加入 COWIN 优先检查
  airport_primary.source_label 显示为 CoWIN 6087,前端图表默认可见
This commit is contained in:
2569718930@qq.com
2026-05-26 07:52:49 +08:00
parent 00e3007949
commit 20d53e1ae2
3 changed files with 208 additions and 1 deletions
+142
View File
@@ -0,0 +1,142 @@
"""CoWIN (Community Weather Information Network) 1-minute real-time data source.
Fetches 1-minute temperature from HKU CoWIN API for Hong Kong.
Station 6087 (保良局陳守仁小學) provides true 1-minute observations.
No API key required.
"""
from __future__ import annotations
import os
import time
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, Optional
import requests
from loguru import logger
from src.utils.metrics import record_source_call
COWIN_BASE_URL = os.getenv("COWIN_BASE_URL", "").strip() or "https://cowin.hku.hk"
COWIN_SERIES_URL = f"{COWIN_BASE_URL}/API/data/CoWIN/series"
COWIN_STATION_ID = int(os.getenv("COWIN_HK_STATION_ID", "6087"))
COWIN_STATION_LABEL = os.getenv("COWIN_HK_STATION_LABEL", "").strip() or "保良局陳守仁小學 1min (CoWIN)"
class CowinSourceMixin:
def _cowin_http_get(self, url: str) -> requests.Response:
getter = getattr(self, "_http_get", None)
if callable(getter):
resp = getter(url)
return resp
resp = self.session.get(url, timeout=self.timeout)
resp.raise_for_status()
return resp
def fetch_cowin_obs_current(
self,
city: str,
use_fahrenheit: bool = False,
) -> Optional[Dict[str, Any]]:
started = time.perf_counter()
city_key = str(city or "").strip().lower()
if city_key != "hong kong":
return None
cache_key = f"cowin_obs:{city_key}:{use_fahrenheit}"
now_ts = time.time()
with self._cowin_obs_cache_lock:
cached = self._cowin_obs_cache.get(cache_key)
if cached and now_ts - cached["t"] < self.cowin_obs_cache_ttl_sec:
record_source_call("cowin_obs", "current", "cache_hit",
(time.perf_counter() - started) * 1000.0)
return cached["d"]
try:
# Fetch last 10 minutes to get the latest reading
now = datetime.now(timezone.utc)
end_dt = now.strftime("%Y-%m-%dT%H:%M:%S")
start_dt = (now - timedelta(minutes=10)).strftime("%Y-%m-%dT%H:%M:%S")
params = {
"station_id": COWIN_STATION_ID,
"element_id": "temp",
"start_dt": start_dt,
"end_dt": end_dt,
}
resp = self._cowin_http_get(COWIN_SERIES_URL + "?" + "&".join(
f"{k}={v}" for k, v in params.items()
))
payload = resp.json() if resp.content else {}
except Exception as exc:
logger.warning("CoWIN obs fetch failed city={} error={}", city_key, exc)
with self._cowin_obs_cache_lock:
stale = self._cowin_obs_cache.get(cache_key)
if stale:
record_source_call("cowin_obs", "current", "stale_cache",
(time.perf_counter() - started) * 1000.0)
return stale["d"]
record_source_call("cowin_obs", "current", "error",
(time.perf_counter() - started) * 1000.0)
return None
minutely = payload.get("minutely") if isinstance(payload, dict) else None
if not minutely or not isinstance(minutely, list) or not minutely:
record_source_call("cowin_obs", "current", "no_data",
(time.perf_counter() - started) * 1000.0)
return None
latest = minutely[-1]
try:
temp_c = float(latest["value1"])
except (KeyError, ValueError, TypeError):
record_source_call("cowin_obs", "current", "no_temperature",
(time.perf_counter() - started) * 1000.0)
return None
obs_time = str(latest.get("obstime") or "").strip()
temp = round(temp_c * 9 / 5 + 32, 1) if use_fahrenheit else round(temp_c, 1)
result = {
"source": "cowin_obs",
"timestamp": datetime.now(timezone.utc).isoformat(),
"station_code": str(COWIN_STATION_ID),
"station_name": COWIN_STATION_LABEL,
"icao": f"COWIN{COWIN_STATION_ID}",
"obs_time": obs_time or datetime.now(timezone.utc).isoformat(),
"current": {
"temp": temp,
},
"temp_c": temp_c,
}
with self._cowin_obs_cache_lock:
self._cowin_obs_cache[cache_key] = {"d": result, "t": now_ts}
record_source_call("cowin_obs", "current", "success",
(time.perf_counter() - started) * 1000.0)
return result
def fetch_cowin_obs_official_nearby(
self,
city: str,
use_fahrenheit: bool = False,
) -> list[Dict[str, Any]]:
current = self.fetch_cowin_obs_current(city, use_fahrenheit=use_fahrenheit)
if not current:
return []
return [
{
"name": COWIN_STATION_LABEL,
"station_label": COWIN_STATION_LABEL,
"lat": 22.3050,
"lon": 114.1670,
"temp": (current.get("current") or {}).get("temp"),
"icao": f"COWIN{COWIN_STATION_ID}",
"istNo": str(COWIN_STATION_ID),
"source": "cowin_obs",
"source_label": "CoWIN 6087",
"obs_time": current.get("obs_time"),
}
]