接入新加坡 MSS 1分钟实时温度及 MGM/JMA/FMI/KNMI 高频源到 airport_primary
- 新建 singapore_mss_sources.py:拉取 data.gov.sg 1分钟干球温度(S24 樟宜站) - country_networks.py:_airport_primary_from_raw 新增 MGM/JMA/FMI/KNMI/SG_MSS 分支 - weather_sources.py:注入 jma_current/fmi_current/knmi_current/singapore_mss_current - 前端 MonitorPanel:resolveSourceLabel 根据 airport_primary.source_code 显示数据源标签 - 文档:更新 AIRPORT_REALTIME_SOURCES.md 新增新加坡 Tested: ruff check . ✓ npx tsc --noEmit ✓
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
| 赫尔辛基 | Vantaa | EFHK | FMI (`opendata.fmi.fi`) | 10 分钟 | 机场站点实时温度 | 免费 |
|
||||
| 阿姆斯特丹 | Schiphol | EHAM | KNMI (`dataplatform.knmi.nl`) | 10 分钟 | 机场站点实时温度 | 免费(需注册) |
|
||||
| 巴黎 | Le Bourget | LFPB | AROME HD (`api.open-meteo.com`) | 15 分钟 | 模型预报(非实测) | 免费 |
|
||||
| 新加坡 | Changi | WSSS | Singapore MSS (`api.data.gov.sg`) | 1 分钟 | 机场站点实时温度 (S24 站) | 免费 |
|
||||
| 纽约 | LaGuardia | KLGA | NOAA MADIS HFMETAR | 5 分钟 | 机场站点实时温度 | 免费 |
|
||||
| 洛杉矶 | LAX | KLAX | NOAA MADIS HFMETAR | 5 分钟 | 机场站点实时温度 | 免费 |
|
||||
| 芝加哥 | O'Hare | KORD | NOAA MADIS HFMETAR | 5 分钟 | 机场站点实时温度 | 免费 |
|
||||
@@ -24,6 +25,11 @@
|
||||
| 奥斯汀 | Bergstrom | KAUS | NOAA MADIS HFMETAR | 5 分钟 | 机场站点实时温度 | 免费 |
|
||||
| 西雅图 | SeaTac | KSEA | NOAA MADIS HFMETAR | 5 分钟 | 机场站点实时温度 | 免费 |
|
||||
|
||||
> **Singapore MSS**: 新加坡气象局(MSS)通过 data.gov.sg 开放数据平台提供全国 15 个站点
|
||||
> 的干球温度(1 分钟均值),更新频率 ~1 分钟。选取 S24 Upper Changi Road North 站
|
||||
> 作为樟宜机场 (WSSS) 的实时温度锚点。数据公开免费,无需 API 密钥。
|
||||
> 后端通过 `singapore_mss_sources.py` 拉取并注入 `airport_primary`。
|
||||
|
||||
> **NOAA MADIS HFMETAR**: 美国 11 个城市的机场高频实时数据通过 NOAA MADIS 公共档案获取。
|
||||
> 数据源为 NetCDF 格式(`madis-data.ncep.noaa.gov/madisPublic1/data/LDAD/hfmetar/`),
|
||||
> 每 5 分钟全量更新一次,温度保留一位小数。匿名公开访问,无需 API 密钥。
|
||||
|
||||
@@ -167,13 +167,35 @@ function airportLabel(key: string, isEn: boolean) {
|
||||
*/
|
||||
const HKO_OBS_CITIES = new Set<MonitorKey>(["hong kong", "lau fau shan"]);
|
||||
|
||||
function obsSourceLabel(
|
||||
const SOURCE_LABELS: Record<string, { en: string; zh: string }> = {
|
||||
amos_runway_median: { en: "AMOS Runway", zh: "AMOS 跑道温度" },
|
||||
amos_runway: { en: "AMOS Runway", zh: "AMOS 跑道温度" },
|
||||
amos: { en: "AMOS", zh: "AMOS" },
|
||||
madis_hfmetar: { en: "NOAA MADIS", zh: "NOAA MADIS" },
|
||||
mgm: { en: "MGM", zh: "MGM" },
|
||||
jma_amedas: { en: "JMA AMeDAS", zh: "JMA AMeDAS" },
|
||||
fmi: { en: "FMI", zh: "FMI" },
|
||||
knmi: { en: "KNMI", zh: "KNMI" },
|
||||
sg_mss: { en: "Singapore MSS", zh: "新加坡 MSS" },
|
||||
airport_current: { en: "Airport METAR", zh: "机场报文" },
|
||||
current: { en: "Obs", zh: "观测" },
|
||||
};
|
||||
|
||||
function resolveSourceLabel(
|
||||
detail: CityDetail | undefined,
|
||||
key: MonitorKey,
|
||||
isEn: boolean,
|
||||
freshness?: ObservationFreshness | null,
|
||||
): string {
|
||||
const sourceLabel = String(freshness?.source_label || "").trim();
|
||||
if (sourceLabel) return sourceLabel;
|
||||
// Use airport_primary source label from high-freq pipeline
|
||||
const apSource = detail?.airport_primary?.source_code;
|
||||
if (apSource && SOURCE_LABELS[apSource]) {
|
||||
return isEn ? SOURCE_LABELS[apSource].en : SOURCE_LABELS[apSource].zh;
|
||||
}
|
||||
// Use temperature resolution source
|
||||
const { source } = resolveMonitorTemperature(detail);
|
||||
if (source && SOURCE_LABELS[source]) {
|
||||
return isEn ? SOURCE_LABELS[source].en : SOURCE_LABELS[source].zh;
|
||||
}
|
||||
if (HKO_OBS_CITIES.has(key)) return isEn ? "HKO Obs" : "天文台观测";
|
||||
return isEn ? "Airport METAR" : "机场报文";
|
||||
}
|
||||
@@ -570,7 +592,7 @@ export default function MonitorPanel({
|
||||
</div>
|
||||
<div className="monitor-obs-row">
|
||||
<span className="monitor-stat-label">
|
||||
{obsSourceLabel(key, isEn, freshnessInfo)}
|
||||
{resolveSourceLabel(detail, key, isEn)}
|
||||
</span>
|
||||
<span className={`monitor-obs-age ${freshness}`}>
|
||||
{age != null ? (
|
||||
|
||||
@@ -273,6 +273,152 @@ def _airport_primary_from_raw(city: str, raw: Dict[str, Any]) -> Dict[str, Any]:
|
||||
},
|
||||
)
|
||||
|
||||
mgm = raw.get("mgm") or {}
|
||||
mgm_current = mgm.get("current") or {}
|
||||
if mgm_current.get("temp") is not None:
|
||||
return _normalize_station_row(
|
||||
station_code=meta.get("icao") or str(mgm.get("istNo") or ""),
|
||||
station_label=meta.get("airport_name") or mgm.get("station_label") or meta.get("icao"),
|
||||
temp=_safe_float(mgm_current["temp"]),
|
||||
obs_time=mgm.get("obs_time") or metar.get("observation_time"),
|
||||
source_code="mgm",
|
||||
source_label="MGM",
|
||||
is_official=True,
|
||||
is_airport_station=True,
|
||||
is_settlement_anchor=False,
|
||||
extra={
|
||||
"max_so_far": _safe_float(current.get("max_temp_so_far")),
|
||||
"max_temp_time": current.get("max_temp_time"),
|
||||
"obs_age_min": None,
|
||||
"report_time": metar.get("report_time"),
|
||||
"receipt_time": metar.get("receipt_time"),
|
||||
"obs_time_epoch": metar.get("obs_time_epoch"),
|
||||
"obs_time_utc_offset_seconds": 0,
|
||||
"wind_speed_kt": _safe_float(current.get("wind_speed_kt")),
|
||||
"wind_dir": _safe_float(current.get("wind_dir")),
|
||||
"humidity": _safe_float(current.get("humidity")),
|
||||
"visibility_mi": _safe_float(current.get("visibility_mi")),
|
||||
"wx_desc": current.get("wx_desc"),
|
||||
"raw_metar": current.get("raw_metar"),
|
||||
},
|
||||
)
|
||||
|
||||
jma = raw.get("jma_current") or {}
|
||||
if jma.get("temp") is not None:
|
||||
return _normalize_station_row(
|
||||
station_code=meta.get("icao") or str(jma.get("icao") or "RJTT"),
|
||||
station_label=meta.get("airport_name") or jma.get("station_label") or meta.get("icao"),
|
||||
temp=_safe_float(jma["temp"]),
|
||||
obs_time=str(jma.get("obs_time") or metar.get("observation_time") or ""),
|
||||
source_code="jma_amedas",
|
||||
source_label="JMA AMeDAS",
|
||||
is_official=True,
|
||||
is_airport_station=True,
|
||||
is_settlement_anchor=False,
|
||||
extra={
|
||||
"max_so_far": _safe_float(current.get("max_temp_so_far")),
|
||||
"max_temp_time": current.get("max_temp_time"),
|
||||
"obs_age_min": None,
|
||||
"report_time": metar.get("report_time"),
|
||||
"receipt_time": metar.get("receipt_time"),
|
||||
"obs_time_epoch": metar.get("obs_time_epoch"),
|
||||
"obs_time_utc_offset_seconds": 0,
|
||||
"wind_speed_kt": _safe_float(current.get("wind_speed_kt")),
|
||||
"wind_dir": _safe_float(current.get("wind_dir")),
|
||||
"humidity": _safe_float(current.get("humidity")),
|
||||
"visibility_mi": _safe_float(current.get("visibility_mi")),
|
||||
"wx_desc": current.get("wx_desc"),
|
||||
"raw_metar": current.get("raw_metar"),
|
||||
},
|
||||
)
|
||||
|
||||
fmi = raw.get("fmi_current") or {}
|
||||
if fmi.get("temp") is not None:
|
||||
return _normalize_station_row(
|
||||
station_code=meta.get("icao") or str(fmi.get("icao") or "EFHK"),
|
||||
station_label=meta.get("airport_name") or fmi.get("station_label") or meta.get("icao"),
|
||||
temp=_safe_float(fmi["temp"]),
|
||||
obs_time=str(fmi.get("obs_time") or metar.get("observation_time") or ""),
|
||||
source_code="fmi",
|
||||
source_label="FMI",
|
||||
is_official=True,
|
||||
is_airport_station=True,
|
||||
is_settlement_anchor=False,
|
||||
extra={
|
||||
"max_so_far": _safe_float(current.get("max_temp_so_far")),
|
||||
"max_temp_time": current.get("max_temp_time"),
|
||||
"obs_age_min": None,
|
||||
"report_time": metar.get("report_time"),
|
||||
"receipt_time": metar.get("receipt_time"),
|
||||
"obs_time_epoch": metar.get("obs_time_epoch"),
|
||||
"obs_time_utc_offset_seconds": 0,
|
||||
"wind_speed_kt": _safe_float(current.get("wind_speed_kt")),
|
||||
"wind_dir": _safe_float(current.get("wind_dir")),
|
||||
"humidity": _safe_float(current.get("humidity")),
|
||||
"visibility_mi": _safe_float(current.get("visibility_mi")),
|
||||
"wx_desc": current.get("wx_desc"),
|
||||
"raw_metar": current.get("raw_metar"),
|
||||
},
|
||||
)
|
||||
|
||||
knmi = raw.get("knmi_current") or {}
|
||||
if knmi.get("temp") is not None:
|
||||
return _normalize_station_row(
|
||||
station_code=meta.get("icao") or str(knmi.get("icao") or "EHAM"),
|
||||
station_label=meta.get("airport_name") or knmi.get("station_label") or meta.get("icao"),
|
||||
temp=_safe_float(knmi["temp"]),
|
||||
obs_time=str(knmi.get("obs_time") or metar.get("observation_time") or ""),
|
||||
source_code="knmi",
|
||||
source_label="KNMI",
|
||||
is_official=True,
|
||||
is_airport_station=True,
|
||||
is_settlement_anchor=False,
|
||||
extra={
|
||||
"max_so_far": _safe_float(current.get("max_temp_so_far")),
|
||||
"max_temp_time": current.get("max_temp_time"),
|
||||
"obs_age_min": None,
|
||||
"report_time": metar.get("report_time"),
|
||||
"receipt_time": metar.get("receipt_time"),
|
||||
"obs_time_epoch": metar.get("obs_time_epoch"),
|
||||
"obs_time_utc_offset_seconds": 0,
|
||||
"wind_speed_kt": _safe_float(current.get("wind_speed_kt")),
|
||||
"wind_dir": _safe_float(current.get("wind_dir")),
|
||||
"humidity": _safe_float(current.get("humidity")),
|
||||
"visibility_mi": _safe_float(current.get("visibility_mi")),
|
||||
"wx_desc": current.get("wx_desc"),
|
||||
"raw_metar": current.get("raw_metar"),
|
||||
},
|
||||
)
|
||||
|
||||
sg_mss = raw.get("singapore_mss_current") or {}
|
||||
if sg_mss.get("temp_c") is not None:
|
||||
return _normalize_station_row(
|
||||
station_code=meta.get("icao") or "WSSS",
|
||||
station_label=meta.get("airport_name") or "Changi Airport",
|
||||
temp=sg_mss["temp_c"],
|
||||
obs_time=sg_mss.get("obs_time") or metar.get("observation_time"),
|
||||
source_code="sg_mss",
|
||||
source_label="Singapore MSS",
|
||||
is_official=True,
|
||||
is_airport_station=True,
|
||||
is_settlement_anchor=False,
|
||||
extra={
|
||||
"max_so_far": _safe_float(current.get("max_temp_so_far")),
|
||||
"max_temp_time": current.get("max_temp_time"),
|
||||
"obs_age_min": None,
|
||||
"report_time": metar.get("report_time"),
|
||||
"receipt_time": metar.get("receipt_time"),
|
||||
"obs_time_epoch": metar.get("obs_time_epoch"),
|
||||
"obs_time_utc_offset_seconds": 0,
|
||||
"wind_speed_kt": _safe_float(current.get("wind_speed_kt")),
|
||||
"wind_dir": _safe_float(current.get("wind_dir")),
|
||||
"humidity": _safe_float(current.get("humidity")),
|
||||
"visibility_mi": _safe_float(current.get("visibility_mi")),
|
||||
"wx_desc": current.get("wx_desc"),
|
||||
"raw_metar": current.get("raw_metar"),
|
||||
},
|
||||
)
|
||||
|
||||
return _normalize_station_row(
|
||||
station_code=meta.get("icao") or metar.get("icao"),
|
||||
station_label=meta.get("airport_name") or metar.get("station_name") or metar.get("icao"),
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
"""Singapore MSS 1-minute real-time temperature data source.
|
||||
|
||||
Fetches air temperature from data.gov.sg public API.
|
||||
The API provides dry-bulb temperature (1-min mean) at ~1 min interval
|
||||
from 15 stations across Singapore. Station S24 (Upper Changi Road North)
|
||||
is the closest to Changi Airport (WSSS).
|
||||
|
||||
URL: https://api.data.gov.sg/v1/environment/air-temperature
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from src.utils.metrics import record_source_call
|
||||
|
||||
SINGAPORE_MSS_TEMP_URL = (
|
||||
"https://api.data.gov.sg/v1/environment/air-temperature"
|
||||
)
|
||||
|
||||
# Preferred station: closest to Changi Airport (WSSS)
|
||||
PREFERRED_STATION_ID = "S24"
|
||||
PREFERRED_STATION_NAME = "Upper Changi Road North"
|
||||
|
||||
|
||||
def _parse_mss_reading(raw: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
"""Extract the preferred station's temperature from an API response."""
|
||||
items = raw.get("items") or []
|
||||
if not items:
|
||||
return None
|
||||
|
||||
metadata = raw.get("metadata") or {}
|
||||
stations = metadata.get("stations") or []
|
||||
|
||||
# Resolve station name
|
||||
station_name = ""
|
||||
for s in stations:
|
||||
if isinstance(s, dict) and s.get("id") == PREFERRED_STATION_ID:
|
||||
station_name = str(s.get("name") or PREFERRED_STATION_NAME)
|
||||
break
|
||||
if not station_name:
|
||||
station_name = PREFERRED_STATION_NAME
|
||||
|
||||
# Take the most recent reading
|
||||
latest = items[-1]
|
||||
readings = latest.get("readings") or []
|
||||
ts = str(latest.get("timestamp") or "")
|
||||
|
||||
for r in readings:
|
||||
if r.get("station_id") == PREFERRED_STATION_ID:
|
||||
temp_c = r.get("value")
|
||||
if temp_c is not None and -20 < temp_c < 55:
|
||||
# Observed at: use the API timestamp, which is UTC+8
|
||||
obs_time = ts
|
||||
if obs_time:
|
||||
try:
|
||||
dt = datetime.fromisoformat(obs_time)
|
||||
obs_time = dt.astimezone(timezone.utc).isoformat()
|
||||
except (ValueError, OverflowError):
|
||||
pass
|
||||
|
||||
return {
|
||||
"station_id": PREFERRED_STATION_ID,
|
||||
"station_name": station_name,
|
||||
"temp_c": round(float(temp_c), 1),
|
||||
"obs_time": obs_time,
|
||||
"source": "sg_mss",
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class SingaporeMssSourceMixin:
|
||||
"""Mixin that adds Singapore MSS 1-min data to WeatherDataCollector."""
|
||||
|
||||
def _sg_mss_http_get(self, url: str) -> Optional[Dict[str, Any]]:
|
||||
getter = getattr(self, "_http_get", None)
|
||||
if callable(getter):
|
||||
resp = getter(url)
|
||||
if hasattr(resp, "json"):
|
||||
return resp.json()
|
||||
return resp
|
||||
try:
|
||||
resp = self.session.get(url, timeout=self.timeout)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def fetch_singapore_mss_current(self) -> Optional[Dict[str, Any]]:
|
||||
"""Fetch latest Singapore MSS temperature for Changi station."""
|
||||
started = time.perf_counter()
|
||||
|
||||
try:
|
||||
raw = self._sg_mss_http_get(SINGAPORE_MSS_TEMP_URL)
|
||||
if not raw:
|
||||
record_source_call(
|
||||
"sg_mss", "current", "empty",
|
||||
(time.perf_counter() - started) * 1000.0,
|
||||
)
|
||||
return None
|
||||
|
||||
result = _parse_mss_reading(raw)
|
||||
if result:
|
||||
logger.info(
|
||||
"Singapore MSS: station={} temp={}°C obs_time={}",
|
||||
result["station_id"],
|
||||
result["temp_c"],
|
||||
result.get("obs_time", "?"),
|
||||
)
|
||||
record_source_call(
|
||||
"sg_mss", "current", "success",
|
||||
(time.perf_counter() - started) * 1000.0,
|
||||
)
|
||||
else:
|
||||
record_source_call(
|
||||
"sg_mss", "current", "no_station",
|
||||
(time.perf_counter() - started) * 1000.0,
|
||||
)
|
||||
return result
|
||||
|
||||
except Exception as exc:
|
||||
logger.warning("Singapore MSS fetch failed: {}", exc)
|
||||
record_source_call(
|
||||
"sg_mss", "current", "error",
|
||||
(time.perf_counter() - started) * 1000.0,
|
||||
)
|
||||
return None
|
||||
@@ -19,10 +19,11 @@ from src.data_collection.fmi_sources import FmiSourceMixin
|
||||
from src.data_collection.knmi_sources import KnmiSourceMixin
|
||||
from src.data_collection.hko_obs_sources import HkoObsSourceMixin
|
||||
from src.data_collection.madis_sources import MadisSourceMixin
|
||||
from src.data_collection.singapore_mss_sources import SingaporeMssSourceMixin
|
||||
from src.database.db_manager import DBManager
|
||||
|
||||
|
||||
class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSourceMixin, MgmSourceMixin, JmaAmedasSourceMixin, RussiaStationSourceMixin, NmcSourceMixin, NwsOpenMeteoSourceMixin, AmosStationSourceMixin, FmiSourceMixin, KnmiSourceMixin, HkoObsSourceMixin, MadisSourceMixin):
|
||||
class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSourceMixin, MgmSourceMixin, JmaAmedasSourceMixin, RussiaStationSourceMixin, NmcSourceMixin, NwsOpenMeteoSourceMixin, AmosStationSourceMixin, FmiSourceMixin, KnmiSourceMixin, HkoObsSourceMixin, MadisSourceMixin, SingaporeMssSourceMixin):
|
||||
"""
|
||||
Multi-source weather data collector
|
||||
|
||||
@@ -923,6 +924,7 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour
|
||||
if not official_rows:
|
||||
return
|
||||
results["jma_official_nearby"] = official_rows
|
||||
results["jma_current"] = official_rows[0] # primary station for airport_primary
|
||||
if "mgm_nearby" not in results:
|
||||
results["mgm_nearby"] = official_rows
|
||||
results["nearby_source"] = "jma"
|
||||
@@ -949,6 +951,7 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour
|
||||
if not rows:
|
||||
return
|
||||
results["knmi_official_nearby"] = rows
|
||||
results["knmi_current"] = rows[0] # primary station for airport_primary
|
||||
if "mgm_nearby" not in results:
|
||||
results["mgm_nearby"] = rows
|
||||
results["nearby_source"] = "knmi"
|
||||
@@ -974,6 +977,7 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour
|
||||
if not rows:
|
||||
return
|
||||
results["fmi_official_nearby"] = rows
|
||||
results["fmi_current"] = rows[0] # primary station for airport_primary
|
||||
if "mgm_nearby" not in results:
|
||||
results["mgm_nearby"] = rows
|
||||
results["nearby_source"] = "fmi"
|
||||
@@ -1101,6 +1105,30 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour
|
||||
)
|
||||
break
|
||||
|
||||
def _attach_singapore_mss_data(
|
||||
self, results: Dict, city_lower: str
|
||||
) -> None:
|
||||
"""Fetch Singapore MSS 1-min temperature and inject into results."""
|
||||
if city_lower != "singapore":
|
||||
return
|
||||
|
||||
obs = self.fetch_singapore_mss_current()
|
||||
if not obs:
|
||||
return
|
||||
|
||||
results["singapore_mss_current"] = obs
|
||||
try:
|
||||
DBManager().append_airport_obs(
|
||||
icao="WSSS",
|
||||
city=city_lower,
|
||||
temp_c=obs["temp_c"],
|
||||
obs_time=obs["obs_time"] or datetime.now().isoformat(),
|
||||
)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"airport_obs_log append failed for singapore_mss city={}", city_lower
|
||||
)
|
||||
|
||||
def _attach_russia_official_nearby(
|
||||
self, results: Dict, city_lower: str, use_fahrenheit: bool
|
||||
) -> None:
|
||||
@@ -1227,6 +1255,7 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour
|
||||
)
|
||||
self._attach_korean_amos_data(results, city_lower, use_fahrenheit)
|
||||
self._attach_madis_hfmetar_data(results, city_lower)
|
||||
self._attach_singapore_mss_data(results, city_lower)
|
||||
if include_nearby:
|
||||
self._attach_china_official_nearby(results, city_lower, use_fahrenheit)
|
||||
self._attach_japan_official_nearby(results, city_lower, use_fahrenheit)
|
||||
@@ -1273,6 +1302,7 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour
|
||||
)
|
||||
self._attach_korean_amos_data(results, city_lower, use_fahrenheit)
|
||||
self._attach_madis_hfmetar_data(results, city_lower)
|
||||
self._attach_singapore_mss_data(results, city_lower)
|
||||
if include_nearby:
|
||||
self._attach_china_official_nearby(results, city_lower, use_fahrenheit)
|
||||
self._attach_japan_official_nearby(results, city_lower, use_fahrenheit)
|
||||
|
||||
Reference in New Issue
Block a user