Files
2026-06-08 06:05:31 -05:00

122 lines
3.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Open-Meteo 实时天气(免费、无需 API Keyhttps://open-meteo.com/"""
from __future__ import annotations
import asyncio
import time
from typing import Any
import httpx
OPEN_METEO_FORECAST_URL = "https://api.open-meteo.com/v1/forecast"
CACHE_TTL_SECONDS = 30 * 60
_FETCH_CONCURRENCY = 12
_cache: dict[str, tuple[float, dict[str, Any]]] = {}
_cache_lock = asyncio.Lock()
def _cache_key(lat: float, lng: float) -> str:
return f"{round(lat, 2)}:{round(lng, 2)}"
def _parse_current(payload: dict[str, Any]) -> dict[str, Any] | None:
current = payload.get("current")
if not isinstance(current, dict):
return None
temperature = current.get("temperature_2m")
if temperature is None:
return None
try:
temp = round(float(temperature))
except (TypeError, ValueError):
return None
result: dict[str, Any] = {
"temperature": temp,
"observedAt": current.get("time"),
}
humidity = current.get("relative_humidity_2m")
if humidity is not None:
try:
result["humidity"] = int(round(float(humidity)))
except (TypeError, ValueError):
pass
apparent = current.get("apparent_temperature")
if apparent is not None:
try:
result["apparentTemperature"] = round(float(apparent))
except (TypeError, ValueError):
pass
weather_code = current.get("weather_code")
if weather_code is not None:
try:
result["weatherCode"] = int(weather_code)
except (TypeError, ValueError):
pass
return result
async def _fetch_point_weather(client: httpx.AsyncClient, lat: float, lng: float) -> dict[str, Any] | None:
key = _cache_key(lat, lng)
now = time.time()
cached = _cache.get(key)
if cached and now - cached[0] < CACHE_TTL_SECONDS:
return cached[1]
params = {
"latitude": lat,
"longitude": lng,
"current": "temperature_2m,relative_humidity_2m,apparent_temperature,weather_code",
"timezone": "auto",
"forecast_days": 1,
}
try:
response = await client.get(OPEN_METEO_FORECAST_URL, params=params, timeout=12.0)
response.raise_for_status()
parsed = _parse_current(response.json())
except (httpx.HTTPError, ValueError):
return None
if parsed:
async with _cache_lock:
_cache[key] = (now, parsed)
return parsed
async def fetch_weather_for_cities(cities: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
"""按城市 slug 返回实时天气;无坐标或请求失败则跳过。"""
targets: list[tuple[str, float, float]] = []
seen: set[str] = set()
for city in cities:
slug = str(city.get("slug") or "").strip()
if not slug or slug in seen:
continue
lat = city.get("lat")
lng = city.get("lng")
if lat is None or lng is None:
continue
try:
lat_f = float(lat)
lng_f = float(lng)
except (TypeError, ValueError):
continue
seen.add(slug)
targets.append((slug, lat_f, lng_f))
if not targets:
return {}
semaphore = asyncio.Semaphore(_FETCH_CONCURRENCY)
results: dict[str, dict[str, Any]] = {}
async with httpx.AsyncClient() as client:
async def load_one(slug: str, lat: float, lng: float) -> None:
async with semaphore:
weather = await _fetch_point_weather(client, lat, lng)
if weather:
results[slug] = weather
await asyncio.gather(*(load_one(slug, lat, lng) for slug, lat, lng in targets))
return results