148 lines
5.7 KiB
Python
148 lines
5.7 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
import time
|
|
from datetime import datetime
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
from loguru import logger
|
|
|
|
from src.utils.metrics import record_source_call
|
|
|
|
_JMA_BASE = os.getenv("JMA_AMEDAS_BASE_URL", "").strip() or "https://www.jma.go.jp"
|
|
|
|
|
|
JMA_AMEDAS_STATIONS: Dict[str, Dict[str, Any]] = {
|
|
"tokyo": {
|
|
"station_code": "44166",
|
|
"station_label": "羽田 10分实况 (JMA)",
|
|
"lat": 35.5533,
|
|
"lon": 139.78,
|
|
},
|
|
}
|
|
|
|
|
|
class JmaAmedasSourceMixin:
|
|
def _jma_http_get_text(self, url: str) -> str:
|
|
getter = getattr(self, "_http_get", None)
|
|
if callable(getter):
|
|
response = getter(url)
|
|
else:
|
|
response = self.session.get(url, timeout=self.timeout)
|
|
response.raise_for_status()
|
|
return response.text
|
|
|
|
@staticmethod
|
|
def _jma_safe_float(value: Any) -> Optional[float]:
|
|
try:
|
|
if value in (None, "", "///"):
|
|
return None
|
|
return float(value)
|
|
except Exception:
|
|
return None
|
|
|
|
def fetch_jma_amedas_current(
|
|
self,
|
|
city: str,
|
|
use_fahrenheit: bool = False,
|
|
) -> Optional[Dict[str, Any]]:
|
|
started = time.perf_counter()
|
|
city_key = str(city or "").strip().lower()
|
|
meta = JMA_AMEDAS_STATIONS.get(city_key) or {}
|
|
if not meta:
|
|
record_source_call("jma_amedas", "current", "unsupported_city", (time.perf_counter() - started) * 1000.0)
|
|
return None
|
|
|
|
cache_key = f"{city_key}:{use_fahrenheit}"
|
|
now_ts = time.time()
|
|
with self._jma_cache_lock:
|
|
cached = self._jma_cache.get(cache_key)
|
|
if cached and now_ts - cached["t"] < self.jma_cache_ttl_sec:
|
|
record_source_call("jma_amedas", "current", "cache_hit", (time.perf_counter() - started) * 1000.0)
|
|
return cached["d"]
|
|
|
|
try:
|
|
latest_time_text = self._jma_http_get_text(
|
|
f"{_JMA_BASE}/bosai/amedas/data/latest_time.txt"
|
|
).strip()
|
|
latest_dt = datetime.fromisoformat(latest_time_text)
|
|
bucket_hour = (latest_dt.hour // 3) * 3
|
|
bucket_key = f"{latest_dt.strftime('%Y%m%d')}_{bucket_hour:02d}"
|
|
station_code = str(meta.get("station_code") or "").strip()
|
|
url = f"{_JMA_BASE}/bosai/amedas/data/point/{station_code}/{bucket_key}.json"
|
|
|
|
getter = getattr(self, "_http_get_json", None)
|
|
if callable(getter):
|
|
payload = getter(url)
|
|
else:
|
|
response = self.session.get(url, timeout=self.timeout)
|
|
response.raise_for_status()
|
|
payload = response.json()
|
|
|
|
if not isinstance(payload, dict) or not payload:
|
|
record_source_call("jma_amedas", "current", "empty", (time.perf_counter() - started) * 1000.0)
|
|
return None
|
|
|
|
latest_key = sorted(payload.keys())[-1]
|
|
row = payload.get(latest_key) or {}
|
|
temp_pair = row.get("temp") or []
|
|
temp_c = self._jma_safe_float(temp_pair[0] if isinstance(temp_pair, list) and temp_pair else None)
|
|
if temp_c is None:
|
|
record_source_call("jma_amedas", "current", "no_temperature", (time.perf_counter() - started) * 1000.0)
|
|
return None
|
|
|
|
temp = round(temp_c * 9 / 5 + 32, 1) if use_fahrenheit else round(temp_c, 1)
|
|
obs_time = None
|
|
try:
|
|
obs_time = datetime.strptime(str(latest_key), "%Y%m%d%H%M%S").isoformat()
|
|
except Exception:
|
|
obs_time = str(latest_key)
|
|
|
|
result = {
|
|
"source": "jma_amedas",
|
|
"timestamp": datetime.utcnow().isoformat(),
|
|
"station_code": station_code,
|
|
"station_name": meta.get("station_label") or "羽田 10分实况 (JMA)",
|
|
"obs_time": obs_time,
|
|
"current": {
|
|
"temp": temp,
|
|
},
|
|
}
|
|
with self._jma_cache_lock:
|
|
self._jma_cache[cache_key] = {"d": result, "t": now_ts}
|
|
record_source_call("jma_amedas", "current", "success", (time.perf_counter() - started) * 1000.0)
|
|
return result
|
|
except Exception as exc:
|
|
logger.warning("JMA AMeDAS current fetch failed city={} error={}", city_key, exc)
|
|
with self._jma_cache_lock:
|
|
stale = self._jma_cache.get(cache_key)
|
|
if stale:
|
|
record_source_call("jma_amedas", "current", "stale_cache", (time.perf_counter() - started) * 1000.0)
|
|
return stale["d"]
|
|
record_source_call("jma_amedas", "current", "error", (time.perf_counter() - started) * 1000.0)
|
|
return None
|
|
|
|
def fetch_jma_amedas_official_nearby(
|
|
self,
|
|
city: str,
|
|
use_fahrenheit: bool = False,
|
|
) -> List[Dict[str, Any]]:
|
|
current = self.fetch_jma_amedas_current(city, use_fahrenheit=use_fahrenheit)
|
|
if not current:
|
|
return []
|
|
meta = JMA_AMEDAS_STATIONS.get(str(city or "").strip().lower()) or {}
|
|
return [
|
|
{
|
|
"name": meta.get("station_label") or "羽田 10分实况 (JMA)",
|
|
"station_label": meta.get("station_label") or "羽田 10分实况 (JMA)",
|
|
"lat": meta.get("lat"),
|
|
"lon": meta.get("lon"),
|
|
"temp": (current.get("current") or {}).get("temp"),
|
|
"icao": current.get("station_code"),
|
|
"istNo": current.get("station_code"),
|
|
"source": "jma",
|
|
"source_label": "JMA",
|
|
"obs_time": current.get("obs_time"),
|
|
}
|
|
]
|