import asyncio import logging import requests from requests.exceptions import ConnectionError, Timeout, SSLError from utils.rate_limiter import weather_limiter, open_meteo_limiter logger = logging.getLogger(__name__) API_URL_WEATHER = "https://wttr.in/Magnitogorsk?format=j1&lang=ru" _session = requests.Session() async def fetch_weather(api_url, timeout=10, max_retries=3): """Получить данные о погоде с retry.""" await weather_limiter.acquire() for attempt in range(max_retries): try: response = await asyncio.to_thread(_session.get, api_url, timeout=timeout) response.raise_for_status() return response.json() except (SSLError, ConnectionError, Timeout): if attempt < max_retries - 1: delay = 2 ** attempt logger.warning("Попытка %d не удалась. Повтор через %d сек...", attempt + 1, delay) await asyncio.sleep(delay) continue break except requests.RequestException as e: logger.error("Ошибка при получении данных: %s", e) break # Fallback: Open-Meteo API (без ключа, HTTPS) return await fetch_open_meteo() async def fetch_open_meteo(lat=53.4069, lon=58.9797, timeout=10, max_retries=3): """Fallback на Open-Meteo API.""" await open_meteo_limiter.acquire() url = ( f"https://api.open-meteo.com/v1/forecast?" f"latitude={lat}&longitude={lon}¤t=temperature," f"apparent_temperature,weather_code,wind_speed_10m," f"relative_humidity_2m,pressure_msl&timezone=Asia/Chelyabinsk" ) for attempt in range(max_retries): try: response = await asyncio.to_thread(_session.get, url, timeout=timeout) response.raise_for_status() data = response.json() current = data.get("current", {}) # weather_code WMO код -> перевод (https://open-meteo.com/en/docs) weather_code = current.get("weather_code", None) desc = wmo_to_russian(weather_code) return { "current_condition": [{ "temp_C": current.get("temperature", "—"), "FeelsLikeC": current.get("apparent_temperature", "—"), "weatherDesc": [{"value": desc}], "humidity": current.get("relative_humidity_2m", "—"), "windspeedKmph": current.get("wind_speed_10m", "—"), "pressure": current.get("pressure_msl", "—"), }] } except (SSLError, ConnectionError, Timeout): if attempt < max_retries - 1: delay = 2 ** attempt logger.warning("Попытка %d не удалась. Повтор через %d сек...", attempt + 1, delay) await asyncio.sleep(delay) continue break except requests.RequestException as e: logger.error("Ошибка при получении данных: %s", e) return None return None def wmo_to_russian(code): """Перевод WMO weather code в русский.""" mapping = { 0: "Ясно", 1: "Ясно", 2: "Переменная облачность", 3: "Пасмурно", 45: "Туман", 48: "Туман", 51: "Лёгкая морось", 53: "Морось", 55: "Сильная морось", 56: "Ледяная морось", 57: "Сильная ледяная морось", 61: "Небольшой дождь", 63: "Дождь", 65: "Сильный дождь", 66: "Ледяной дождь", 67: "Сильный ледяной дождь", 71: "Небольшой снег", 73: "Снег", 75: "Сильный снег", 77: "Снежная крупа", 80: "Небольшой ливень", 81: "Ливень", 82: "Сильный ливень", 85: "Снежный ливень", 86: "Сильный снежный ливень", 95: "Гроза", 96: "Гроза с градом", 99: "Сильная гроза с градом", } return mapping.get(code, "Неизвестно") def translate_weather(en): if not en: return "—" mapping = { "Moderate or heavy freezing rain in area": "Ледяной дождь", "Moderate or heavy sleet in area": "Слякоть", "Moderate or heavy snow in area": "Снег", "Moderate or heavy rain in area": "Дождь", "Thundery outbreaks in nearby": "Гроза вблизи", "Patchy rain nearby": "Местами дождь", "Patchy snow nearby": "Местами снег", "Patchy sleet nearby": "Местами слякоть", "Heavy freezing rain": "Сильный ледяной дождь", "Heavy snow": "Сильный снег", "Heavy rain": "Сильный дождь", "Moderate or heavy rain at times": "Дождь", "Moderate or heavy snow at times": "Снег", "Blowing snow": "Метель", "Patchy light drizzle": "Местами лёгкая морось", "Moderate or heavy freezing rain at a distance": "Ледяной дождь", "Moderate or heavy sleet at a distance": "Слякоть", "Light rain shower": "Небольшой дождь", "Heavy rain shower": "Сильный дождь", "Moderate rain": "Умеренный дождь", "Light rain": "Небольшой дождь", "Moderate rain at times": "Умеренный дождь", "Heavy rain at times": "Сильный дождь", "Light snow": "Небольшой снег", "Moderate snow": "Умеренный снег", "Patchy light snow": "Местами лёгкий снег", "Partly cloudy": "Переменная облачность", "Moderate or light sleet": "Слякоть", "Light freezing rain": "Лёгкий ледяной дождь", "Foggy": "Туманно", "Fog": "Туман", "Mist": "Туман", "Haze": "Дымка", "Overcast": "Пасмурно", "Cloudy": "Облачно", "Clear": "Ясно", "Sunny": "Ясно", } for key, value in mapping.items(): if key.lower() in en.lower(): return value return en def format_weather_data_for_console(data): """ Форматировать погодные данные для консольного вывода. :param data: Ответ от API (dict) :return: Строки с отформатированной погодой """ if data is None: return None current = data.get("current_condition", [{}])[0] if not current: return None temp = current.get("temp_C", "—") feels_like = current.get("FeelsLikeC", "—") description = translate_weather(current.get("weatherDesc", [{}])[0].get("value", "—")) humidity = current.get("humidity", "—") wind_kmh = current.get("windspeedKmph", "—") try: wind = round(int(wind_kmh) / 3.6, 1) if wind_kmh != "—" else "—" except (ValueError, TypeError): wind = "—" pressure_mb = current.get("pressure", "—") pressure_mm = pressure_to_mmhg(pressure_mb) return [ f"Температура: {temp}°C (ощущается как {feels_like}°C)", f"Описание: {description}", f"Влажность: {humidity}%", f"Ветер: {wind} м/с", f"Давление: {pressure_mm} мм рт. ст.", ] def format_weather_for_embed(data): """Форматировать погоду для Discord embed (с заголовком).""" if data is None: return None lines = format_weather_data_for_console(data) if not lines: return None return "**Погода в Магнитогорске:**\n" + "\n".join(lines) def pressure_to_mmhg(mb): if mb == "—" or not mb: return "—" try: return round(float(mb) * 0.750062, 1) except (ValueError, TypeError): return "—"