154 lines
6.0 KiB
Python
154 lines
6.0 KiB
Python
import time
|
||
from typing import Any
|
||
|
||
import httpx
|
||
|
||
from app.config import get_settings
|
||
|
||
WEATHER_CODES: dict[int, str] = {
|
||
0: "ясно",
|
||
1: "преимущественно ясно",
|
||
2: "переменная облачность",
|
||
3: "пасмурно",
|
||
45: "туман",
|
||
48: "изморозь",
|
||
51: "морось",
|
||
53: "морось",
|
||
55: "морось",
|
||
61: "дождь",
|
||
63: "дождь",
|
||
65: "сильный дождь",
|
||
71: "снег",
|
||
73: "снег",
|
||
75: "сильный снег",
|
||
80: "ливень",
|
||
81: "ливень",
|
||
82: "сильный ливень",
|
||
95: "гроза",
|
||
96: "гроза с градом",
|
||
99: "гроза с градом",
|
||
}
|
||
|
||
_cache: dict[str, Any] = {"data": None, "expires_at": 0.0}
|
||
|
||
|
||
class OpenMeteoClient:
|
||
def __init__(self) -> None:
|
||
settings = get_settings()
|
||
self.base_url = settings.openmeteo_base_url.rstrip("/")
|
||
self.lat = settings.weather_lat
|
||
self.lon = settings.weather_lon
|
||
self.location_name = settings.weather_location_name
|
||
self.cache_ttl = settings.weather_cache_sec
|
||
|
||
def _fetch_raw(self) -> dict[str, Any]:
|
||
now = time.time()
|
||
if _cache["data"] and now < _cache["expires_at"]:
|
||
return _cache["data"]
|
||
|
||
params = {
|
||
"latitude": self.lat,
|
||
"longitude": self.lon,
|
||
"current": (
|
||
"temperature_2m,apparent_temperature,relative_humidity_2m,"
|
||
"precipitation,weather_code,wind_speed_10m"
|
||
),
|
||
"hourly": "temperature_2m,precipitation_probability,precipitation,weather_code",
|
||
"timezone": "auto",
|
||
"forecast_days": 2,
|
||
}
|
||
with httpx.Client(timeout=15.0) as client:
|
||
response = client.get(f"{self.base_url}/v1/forecast", params=params)
|
||
response.raise_for_status()
|
||
data = response.json()
|
||
|
||
_cache["data"] = data
|
||
_cache["expires_at"] = now + self.cache_ttl
|
||
return data
|
||
|
||
def fetch_current_and_hourly(self, hours_ahead: int = 12) -> dict[str, Any]:
|
||
try:
|
||
raw = self._fetch_raw()
|
||
except Exception as exc:
|
||
return {"ok": False, "error": str(exc), "location": self.location_name}
|
||
|
||
current = raw.get("current") or {}
|
||
hourly = raw.get("hourly") or {}
|
||
times = hourly.get("time") or []
|
||
limit = min(hours_ahead, len(times))
|
||
hourly_slice = []
|
||
for i in range(limit):
|
||
hourly_slice.append({
|
||
"time": times[i],
|
||
"temperature_c": hourly.get("temperature_2m", [None])[i],
|
||
"precipitation_mm": hourly.get("precipitation", [None])[i],
|
||
"precipitation_probability": hourly.get("precipitation_probability", [None])[i],
|
||
"weather_code": hourly.get("weather_code", [None])[i],
|
||
})
|
||
|
||
code = current.get("weather_code")
|
||
return {
|
||
"ok": True,
|
||
"location": self.location_name,
|
||
"current": {
|
||
"time": current.get("time"),
|
||
"temperature_c": current.get("temperature_2m"),
|
||
"apparent_temperature_c": current.get("apparent_temperature"),
|
||
"humidity_pct": current.get("relative_humidity_2m"),
|
||
"precipitation_mm": current.get("precipitation"),
|
||
"wind_speed_kmh": current.get("wind_speed_10m"),
|
||
"weather_code": code,
|
||
"conditions": WEATHER_CODES.get(code, "неизвестно") if code is not None else "неизвестно",
|
||
},
|
||
"hourly": hourly_slice,
|
||
}
|
||
|
||
def rain_summary(self, hours_ahead: int = 12) -> str:
|
||
data = self.fetch_current_and_hourly(hours_ahead=hours_ahead)
|
||
if not data.get("ok"):
|
||
return f"Погода недоступна: {data.get('error', 'ошибка')}"
|
||
|
||
rainy_hours = []
|
||
for hour in data.get("hourly") or []:
|
||
prob = hour.get("precipitation_probability")
|
||
precip = hour.get("precipitation_mm") or 0
|
||
if (prob is not None and prob >= 40) or precip > 0:
|
||
time_str = (hour.get("time") or "")[11:16]
|
||
rainy_hours.append(f"{time_str} ({prob}% вероятность, {precip} мм)")
|
||
|
||
if rainy_hours:
|
||
return "Ожидаются осадки: " + ", ".join(rainy_hours[:6])
|
||
return "Существенных осадков в ближайшие часы не ожидается."
|
||
|
||
|
||
def format_weather_snapshot(data: dict[str, Any] | None = None) -> str:
|
||
client = OpenMeteoClient()
|
||
snapshot = data if data is not None else client.fetch_current_and_hourly(hours_ahead=6)
|
||
|
||
lines = ["[Погода]"]
|
||
if not snapshot.get("ok"):
|
||
lines.append(f"Данные недоступны ({snapshot.get('error', 'ошибка')}).")
|
||
lines.append("Для точного ответа вызови get_weather.")
|
||
return "\n".join(lines)
|
||
|
||
cur = snapshot.get("current") or {}
|
||
lines.append(
|
||
f"{snapshot.get('location')}: {cur.get('temperature_c')}°C "
|
||
f"(ощущается {cur.get('apparent_temperature_c')}°C), "
|
||
f"{cur.get('conditions')}, ветер {cur.get('wind_speed_kmh')} км/ч."
|
||
)
|
||
hourly = snapshot.get("hourly") or []
|
||
rainy_hours = []
|
||
for hour in hourly:
|
||
prob = hour.get("precipitation_probability")
|
||
precip = hour.get("precipitation_mm") or 0
|
||
if (prob is not None and prob >= 40) or precip > 0:
|
||
time_str = (hour.get("time") or "")[11:16]
|
||
rainy_hours.append(f"{time_str} ({prob}% вероятность, {precip} мм)")
|
||
if rainy_hours:
|
||
lines.append("Ожидаются осадки: " + ", ".join(rainy_hours[:6]))
|
||
else:
|
||
lines.append("Существенных осадков в ближайшие часы не ожидается.")
|
||
lines.append("Вопросы «что на улице» / «будет ли дождь» — get_weather.")
|
||
return "\n".join(lines)
|