feat: add WeatherService and weather schedules to Domovoy Bot
This commit is contained in:
parent
a69b4cdc1e
commit
834dc1c7c6
4 changed files with 458 additions and 1 deletions
|
|
@ -12,7 +12,7 @@ from aiogram.fsm.state import State, StatesGroup
|
|||
from sqlalchemy import select, update
|
||||
from database.db import AsyncSessionLocal
|
||||
from database.models import User, InitiativeGroup
|
||||
from keyboards.inline import get_main_menu, get_phones_menu, get_profile_keyboard
|
||||
from keyboards.inline import get_main_menu, get_phones_menu, get_profile_keyboard, get_weather_keyboard
|
||||
from services.ai_service import ai_service
|
||||
import config
|
||||
|
||||
|
|
@ -472,3 +472,48 @@ async def process_help_reply(message: Message, state: FSMContext):
|
|||
await message.answer(f"❌ Не удалось отправить сообщение пользователю. Ошибка: {e}")
|
||||
|
||||
await state.clear()
|
||||
|
||||
|
||||
# ==========================================
|
||||
# ОБРАБОТЧИКИ МЕНЮ ПОГОДЫ
|
||||
# ==========================================
|
||||
|
||||
@router.callback_query(F.data == 'weather')
|
||||
async def cb_weather_menu(callback: CallbackQuery):
|
||||
"""Показ меню прогноза погоды"""
|
||||
text = (
|
||||
"🌦️ <b>Погодный информатор ЛКМ37</b>\n\n"
|
||||
"Выберите интересующий вас прогноз погоды для Ульяновска:"
|
||||
)
|
||||
await safe_edit_or_reply(callback, text, reply_markup=get_weather_keyboard())
|
||||
|
||||
@router.callback_query(F.data == 'weather_today')
|
||||
async def cb_weather_today(callback: CallbackQuery):
|
||||
"""Показ подробного прогноза погоды на сегодня"""
|
||||
from services.weather import WeatherService
|
||||
text = await WeatherService.get_today_forecast()
|
||||
|
||||
# Добавим кнопку возврата в меню погоды
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.row(InlineKeyboardButton(text='🔙 В меню погоды', callback_data='weather'))
|
||||
await safe_edit_or_reply(callback, text, reply_markup=builder.as_markup())
|
||||
|
||||
@router.callback_query(F.data == 'weather_3days')
|
||||
async def cb_weather_3days(callback: CallbackQuery):
|
||||
"""Показ прогноза погоды на 3 дня"""
|
||||
from services.weather import WeatherService
|
||||
text = await WeatherService.get_3day_forecast()
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.row(InlineKeyboardButton(text='🔙 В меню погоды', callback_data='weather'))
|
||||
await safe_edit_or_reply(callback, text, reply_markup=builder.as_markup())
|
||||
|
||||
@router.callback_query(F.data == 'weather_7days')
|
||||
async def cb_weather_7days(callback: CallbackQuery):
|
||||
"""Показ прогноза погоды на неделю"""
|
||||
from services.weather import WeatherService
|
||||
text = await WeatherService.get_7day_forecast()
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.row(InlineKeyboardButton(text='🔙 В меню погоды', callback_data='weather'))
|
||||
await safe_edit_or_reply(callback, text, reply_markup=builder.as_markup())
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ def get_main_menu(verified: bool = False, is_ig: bool = False, is_admin: bool =
|
|||
# СТРОКА 6: СЛУЖЕБНОЕ (НИЗ)
|
||||
builder.row(
|
||||
InlineKeyboardButton(text='❓ Помощь', callback_data='help'),
|
||||
InlineKeyboardButton(text='🌦️ Погода', callback_data='weather'),
|
||||
InlineKeyboardButton(text='👤 Профиль', callback_data='my_profile')
|
||||
)
|
||||
|
||||
|
|
@ -414,3 +415,19 @@ def get_profile_keyboard(meter_reminders_enabled: bool = True) -> InlineKeyboard
|
|||
builder.row(InlineKeyboardButton(text='🔙 Назад', callback_data='main_menu'))
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
def get_weather_keyboard() -> InlineKeyboardMarkup:
|
||||
"""Клавиатура меню погоды"""
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.row(
|
||||
InlineKeyboardButton(text='🌦️ Сегодня (Подробно)', callback_data='weather_today'),
|
||||
)
|
||||
builder.row(
|
||||
InlineKeyboardButton(text='📅 На 3 дня', callback_data='weather_3days'),
|
||||
InlineKeyboardButton(text='📅 На неделю', callback_data='weather_7days'),
|
||||
)
|
||||
builder.row(
|
||||
InlineKeyboardButton(text='🔙 Назад', callback_data='main_menu')
|
||||
)
|
||||
return builder.as_markup()
|
||||
|
||||
|
|
|
|||
|
|
@ -130,6 +130,36 @@ class Scheduler:
|
|||
replace_existing=True
|
||||
)
|
||||
logger.info('✅ Добавлена задача генерации дайджеста (воскресенье 19:00 Ульяновск)')
|
||||
|
||||
# Ежедневный прогноз погоды для админа (каждое утро в 08:00)
|
||||
self.scheduler.add_job(
|
||||
self.daily_admin_weather,
|
||||
CronTrigger.from_crontab('0 8 * * *'), # В 08:00 каждый день
|
||||
id='daily_admin_weather',
|
||||
name='Ежедневный прогноз погоды для админа',
|
||||
replace_existing=True
|
||||
)
|
||||
logger.info('✅ Добавлена задача ежедневного прогноза погоды для админа')
|
||||
|
||||
# Еженедельный прогноз в чат по понедельникам (в 09:00)
|
||||
self.scheduler.add_job(
|
||||
self.monday_chat_weather,
|
||||
CronTrigger.from_crontab('0 9 * * 1'), # Пн в 09:00
|
||||
id='monday_chat_weather',
|
||||
name='Прогноз погоды в чат (Понедельник)',
|
||||
replace_existing=True
|
||||
)
|
||||
logger.info('✅ Добавлена задача прогноза погоды в чат по понедельникам')
|
||||
|
||||
# Еженедельный прогноз в чат по четвергам (в 09:00)
|
||||
self.scheduler.add_job(
|
||||
self.thursday_chat_weather,
|
||||
CronTrigger.from_crontab('0 9 * * 4'), # Чт в 09:00
|
||||
id='thursday_chat_weather',
|
||||
name='Прогноз погоды в чат (Четверг)',
|
||||
replace_existing=True
|
||||
)
|
||||
logger.info('✅ Добавлена задача прогноза погоды в чат по четвергам')
|
||||
|
||||
def stop(self):
|
||||
"""Остановка планировщика"""
|
||||
|
|
@ -481,3 +511,57 @@ class Scheduler:
|
|||
logger.info('✅ Еженедельный дайджест сгенерирован')
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка генерации дайджеста: {e}')
|
||||
|
||||
async def daily_admin_weather(self):
|
||||
"""Ежедневный прогноз погоды для админа (каждое утро в 08:00)"""
|
||||
try:
|
||||
from services.weather import WeatherService
|
||||
today_forecast = await WeatherService.get_today_forecast()
|
||||
await self.bot.send_message(
|
||||
ADMIN_USER_ID,
|
||||
today_forecast,
|
||||
parse_mode='HTML'
|
||||
)
|
||||
|
||||
# Если сегодня четверг или пятница — шлем прогноз на выходные для велика
|
||||
current_day = datetime.now().weekday()
|
||||
if current_day in [3, 4]: # Четверг (3) или Пятница (4)
|
||||
weekend_forecast = await WeatherService.get_weekend_forecast_cycling()
|
||||
await self.bot.send_message(
|
||||
ADMIN_USER_ID,
|
||||
weekend_forecast,
|
||||
parse_mode='HTML'
|
||||
)
|
||||
logger.info("✅ Daily weather forecast sent to admin")
|
||||
except Exception as e:
|
||||
logger.error(f"Error in daily_admin_weather: {e}")
|
||||
|
||||
async def monday_chat_weather(self):
|
||||
"""Прогноз в чат по понедельникам (на 3 дня)"""
|
||||
try:
|
||||
from services.weather import WeatherService
|
||||
from config import ADMIN_CHAT_ID
|
||||
forecast = await WeatherService.get_3day_forecast()
|
||||
await self.bot.send_message(
|
||||
ADMIN_CHAT_ID,
|
||||
forecast,
|
||||
parse_mode='HTML'
|
||||
)
|
||||
logger.info("✅ Monday weather forecast broadcasted to chat")
|
||||
except Exception as e:
|
||||
logger.error(f"Error in monday_chat_weather: {e}")
|
||||
|
||||
async def thursday_chat_weather(self):
|
||||
"""Прогноз в чат по четвергам (на остаток недели и выходные)"""
|
||||
try:
|
||||
from services.weather import WeatherService
|
||||
from config import ADMIN_CHAT_ID
|
||||
forecast = await WeatherService.get_thursday_weekend_broadcast()
|
||||
await self.bot.send_message(
|
||||
ADMIN_CHAT_ID,
|
||||
forecast,
|
||||
parse_mode='HTML'
|
||||
)
|
||||
logger.info("✅ Thursday weather forecast broadcasted to chat")
|
||||
except Exception as e:
|
||||
logger.error(f"Error in thursday_chat_weather: {e}")
|
||||
|
|
|
|||
311
services/weather.py
Normal file
311
services/weather.py
Normal file
|
|
@ -0,0 +1,311 @@
|
|||
import logging
|
||||
import aiohttp
|
||||
import random
|
||||
from datetime import datetime, timedelta
|
||||
from config import get_proxy_url
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Координаты Ульяновска
|
||||
LAT = 54.3188
|
||||
LON = 48.3978
|
||||
TIMEZONE = "Europe/Ulyanovsk"
|
||||
|
||||
# WMO Weather Codes to Emoji & Description
|
||||
WEATHER_CODES = {
|
||||
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: ("⛈️", "Сильная гроза с градом"),
|
||||
}
|
||||
|
||||
CYCLING_QUOTES = [
|
||||
"«Жизнь — как вождение велосипеда. Чтобы сохранить равновесие, ты должен двигаться». — Альберт Эйнштейн 🚲",
|
||||
"«Ничто не сравнится с простым удовольствием от поездки на велосипеде». — Джон Ф. Кеннеди 🚴",
|
||||
"«Когда мне тяжело, я просто сажусь на велосипед и кручу педали». — Эдди Меркс 🏁",
|
||||
"Велосипед — это самый быстрый способ добраться из унылой рутины в прекрасное приключение! 🌲🚲",
|
||||
"Крути педали, пока молод и пока светит солнце (или хотя бы нет ливня)! ☀️🚴",
|
||||
"Ветер в лицо — это не препятствие, это признак того, что ты летишь! 💨🚴",
|
||||
"«Велосипед спасет мир». — Эмилио Эмилиани 🌍🚲",
|
||||
"Хорошая покатушка очищает разум лучше любого психолога. Только ты, дорога и педали! 🚴✨",
|
||||
"«Велосипед — это часть меня». — Марко Пантани 🇮🇹",
|
||||
"Каждый раз, когда я вижу взрослого на велосипеде, я перестаю отчаиваться за будущее человечества. — Герберт Уэллс 🚲",
|
||||
]
|
||||
|
||||
class WeatherService:
|
||||
@staticmethod
|
||||
def get_emoji(code):
|
||||
return WEATHER_CODES.get(code, ("🌦️", "Переменная погода"))[0]
|
||||
|
||||
@staticmethod
|
||||
def get_desc(code):
|
||||
return WEATHER_CODES.get(code, ("🌦️", "Переменная погода"))[1]
|
||||
|
||||
@classmethod
|
||||
async def _fetch_raw(cls, url):
|
||||
proxy = get_proxy_url()
|
||||
async with aiohttp.ClientSession() as session:
|
||||
try:
|
||||
async with session.get(url, proxy=proxy, timeout=10) as resp:
|
||||
if resp.status == 200:
|
||||
return await resp.json()
|
||||
else:
|
||||
logger.error(f"Weather API status: {resp.status}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Weather API connection error: {e}")
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
async def get_today_forecast(cls) -> str:
|
||||
"""Подробный прогноз на сегодня (по часам) + вероятность дождя сегодня/завтра"""
|
||||
url = (
|
||||
f"https://api.open-meteo.com/v1/forecast?"
|
||||
f"latitude={LAT}&longitude={LON}&"
|
||||
f"hourly=temperature_2m,precipitation_probability,weathercode,windspeed_10m&"
|
||||
f"daily=weathercode,temperature_2m_max,temperature_2m_min,precipitation_probability_max&"
|
||||
f"timezone={TIMEZONE}&forecast_days=2"
|
||||
)
|
||||
data = await cls._fetch_raw(url)
|
||||
if not data:
|
||||
return "❌ Не удалось получить прогноз погоды. Попробуйте позже."
|
||||
|
||||
temp_max_today = data["daily"]["temperature_2m_max"][0]
|
||||
temp_min_today = data["daily"]["temperature_2m_min"][0]
|
||||
rain_prob_today = data["daily"]["precipitation_probability_max"][0]
|
||||
rain_prob_tomorrow = data["daily"]["precipitation_probability_max"][1]
|
||||
|
||||
hourly = data["hourly"]
|
||||
times = hourly["time"]
|
||||
|
||||
idx_morning = next((i for i, t in enumerate(times) if t.endswith("T09:00")), 9)
|
||||
idx_afternoon = next((i for i, t in enumerate(times) if t.endswith("T15:00")), 15)
|
||||
idx_evening = next((i for i, t in enumerate(times) if t.endswith("T21:00")), 21)
|
||||
idx_night = next((i for i, t in enumerate(times) if t.endswith("T03:00") and i > 20), 27)
|
||||
|
||||
def fmt_hour(idx, label):
|
||||
temp = hourly["temperature_2m"][idx]
|
||||
prob = hourly["precipitation_probability"][idx]
|
||||
w_code = hourly["weathercode"][idx]
|
||||
emoji = cls.get_emoji(w_code)
|
||||
return f"▪️ <b>{label}:</b> {emoji} <code>{temp:+.1f}°C</code> (дождь: <code>{prob}%</code>)"
|
||||
|
||||
weather_code_today = data["daily"]["weathercode"][0]
|
||||
general_emoji = cls.get_emoji(weather_code_today)
|
||||
general_desc = cls.get_desc(weather_code_today)
|
||||
|
||||
msg = [
|
||||
f"🌦️ <b>Прогноз погоды в Ульяновске на сегодня:</b>",
|
||||
f"Погода: {general_emoji} <b>{general_desc}</b>",
|
||||
f"Температура: от <code>{temp_min_today:+.1f}°C</code> до <code>{temp_max_today:+.1f}°C</code>",
|
||||
"",
|
||||
"🕒 <b>Детали по часам на сегодня:</b>",
|
||||
fmt_hour(idx_morning, "Утро (09:00)"),
|
||||
fmt_hour(idx_afternoon, "День (15:00)"),
|
||||
fmt_hour(idx_evening, "Вечер (21:00)"),
|
||||
fmt_hour(idx_night, "Ночь (03:00)"),
|
||||
"",
|
||||
f"☔ <b>Макс. вероятность осадков:</b>",
|
||||
f"• Сегодня: <code>{rain_prob_today}%</code>",
|
||||
f"• Завтра: <code>{rain_prob_tomorrow}%</code>"
|
||||
]
|
||||
return "\n".join(msg)
|
||||
|
||||
@classmethod
|
||||
async def get_3day_forecast(cls) -> str:
|
||||
"""Прогноз на 3 дня (для понедельника или по запросу)"""
|
||||
url = (
|
||||
f"https://api.open-meteo.com/v1/forecast?"
|
||||
f"latitude={LAT}&longitude={LON}&"
|
||||
f"daily=weathercode,temperature_2m_max,temperature_2m_min,precipitation_probability_max&"
|
||||
f"timezone={TIMEZONE}&forecast_days=3"
|
||||
)
|
||||
data = await cls._fetch_raw(url)
|
||||
if not data:
|
||||
return "❌ Не удалось получить прогноз погоды. Попробуйте позже."
|
||||
|
||||
daily = data["daily"]
|
||||
msg = ["🌦️ <b>Прогноз погоды на ближайшие 3 дня:</b>"]
|
||||
|
||||
for i in range(3):
|
||||
date_str = daily["time"][i]
|
||||
date_obj = datetime.strptime(date_str, "%Y-%m-%d")
|
||||
ru_days = ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"]
|
||||
day_name = ru_days[date_obj.weekday()]
|
||||
formatted_date = f"{date_obj.strftime('%d.%m')} ({day_name})"
|
||||
|
||||
w_code = daily["weathercode"][i]
|
||||
emoji = cls.get_emoji(w_code)
|
||||
desc = cls.get_desc(w_code)
|
||||
t_max = daily["temperature_2m_max"][i]
|
||||
t_min = daily["temperature_2m_min"][i]
|
||||
prob = daily["precipitation_probability_max"][i]
|
||||
|
||||
msg.append(
|
||||
f"📅 <b>{formatted_date}:</b> {emoji} {desc}\n"
|
||||
f" 🌡️ от <code>{t_min:+.1f}°C</code> до <code>{t_max:+.1f}°C</code> | ☔ Дождь: <code>{prob}%</code>"
|
||||
)
|
||||
|
||||
return "\n\n".join(msg)
|
||||
|
||||
@classmethod
|
||||
async def get_7day_forecast(cls) -> str:
|
||||
"""Прогноз на неделю"""
|
||||
url = (
|
||||
f"https://api.open-meteo.com/v1/forecast?"
|
||||
f"latitude={LAT}&longitude={LON}&"
|
||||
f"daily=weathercode,temperature_2m_max,temperature_2m_min,precipitation_probability_max&"
|
||||
f"timezone={TIMEZONE}&forecast_days=7"
|
||||
)
|
||||
data = await cls._fetch_raw(url)
|
||||
if not data:
|
||||
return "❌ Не удалось получить прогноз погоды. Попробуйте позже."
|
||||
|
||||
daily = data["daily"]
|
||||
msg = ["📅 <b>Прогноз погоды на неделю:</b>"]
|
||||
|
||||
for i in range(7):
|
||||
date_str = daily["time"][i]
|
||||
date_obj = datetime.strptime(date_str, "%Y-%m-%d")
|
||||
ru_days = ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"]
|
||||
day_name = ru_days[date_obj.weekday()]
|
||||
formatted_date = f"{date_obj.strftime('%d.%m')} ({day_name})"
|
||||
|
||||
w_code = daily["weathercode"][i]
|
||||
emoji = cls.get_emoji(w_code)
|
||||
t_max = daily["temperature_2m_max"][i]
|
||||
t_min = daily["temperature_2m_min"][i]
|
||||
prob = daily["precipitation_probability_max"][i]
|
||||
|
||||
msg.append(
|
||||
f"• <b>{formatted_date}:</b> {emoji} <code>{t_min:+.0f}°</code>..<code>{t_max:+.0f}°C</code> | ☔ <code>{prob}%</code>"
|
||||
)
|
||||
|
||||
return "\n".join(msg)
|
||||
|
||||
@classmethod
|
||||
async def get_weekend_forecast_cycling(cls) -> str:
|
||||
"""Прогноз на выходные (Сб-Вс) с фокусом на велосипедные поездки"""
|
||||
url = (
|
||||
f"https://api.open-meteo.com/v1/forecast?"
|
||||
f"latitude={LAT}&longitude={LON}&"
|
||||
f"daily=weathercode,temperature_2m_max,temperature_2m_min,precipitation_probability_max,windspeed_10m_max&"
|
||||
f"timezone={TIMEZONE}&forecast_days=7"
|
||||
)
|
||||
data = await cls._fetch_raw(url)
|
||||
if not data:
|
||||
return "❌ Не удалось получить прогноз погоды для велоквеста."
|
||||
|
||||
daily = data["daily"]
|
||||
times = daily["time"]
|
||||
|
||||
idx_sat = None
|
||||
idx_sun = None
|
||||
for i, t_str in enumerate(times):
|
||||
dt = datetime.strptime(t_str, "%Y-%m-%d")
|
||||
if dt.weekday() == 5: # Суббота
|
||||
idx_sat = i
|
||||
elif dt.weekday() == 6: # Воскресенье
|
||||
idx_sun = i
|
||||
|
||||
if idx_sat is None or idx_sun is None:
|
||||
return "❌ Не удалось определить выходные дни в прогнозе."
|
||||
|
||||
quote = random.choice(CYCLING_QUOTES)
|
||||
|
||||
def fmt_day(idx, label):
|
||||
w_code = daily["weathercode"][idx]
|
||||
emoji = cls.get_emoji(w_code)
|
||||
desc = cls.get_desc(w_code)
|
||||
t_max = daily["temperature_2m_max"][idx]
|
||||
t_min = daily["temperature_2m_min"][idx]
|
||||
prob = daily["precipitation_probability_max"][idx]
|
||||
wind = daily["windspeed_10m_max"][idx]
|
||||
|
||||
if prob > 50 or w_code in [65, 82, 95]:
|
||||
cycling_verdict = "⚠️ Неблагоприятно (высокий риск ливня)"
|
||||
elif wind > 25:
|
||||
cycling_verdict = "💨 Сильный ветер! Будет тяжело крутить"
|
||||
elif t_max < 10:
|
||||
cycling_verdict = "🥶 Холодновато, одевайтесь теплее"
|
||||
else:
|
||||
cycling_verdict = "✅ Отличная погода для катания!"
|
||||
|
||||
return (
|
||||
f"📅 <b>{label}:</b> {emoji} {desc}\n"
|
||||
f" 🌡️ Температура: <code>{t_min:+.1f}°C</code>..<code>{t_max:+.1f}°C</code>\n"
|
||||
f" ☔ Вероятность осадков: <code>{prob}%</code> | 💨 Ветер до: <code>{wind:.1f} км/ч</code>\n"
|
||||
f" 👉 <b>Вердикт:</b> {cycling_verdict}"
|
||||
)
|
||||
|
||||
msg = [
|
||||
"🚴 <b>Прогноз на выходные (для покатушек на велике):</b>",
|
||||
fmt_day(idx_sat, f"Суббота ({datetime.strptime(times[idx_sat], '%Y-%m-%d').strftime('%d.%m')})"),
|
||||
"",
|
||||
fmt_day(idx_sun, f"Воскресенье ({datetime.strptime(times[idx_sun], '%Y-%m-%d').strftime('%d.%m')})"),
|
||||
"",
|
||||
"📣 <b>Мотивация дня:</b>",
|
||||
f"<i>{quote}</i>"
|
||||
]
|
||||
return "\n".join(msg)
|
||||
|
||||
@classmethod
|
||||
async def get_thursday_weekend_broadcast(cls) -> str:
|
||||
"""Прогноз на четверг, пятницу и выходные (для еженедельной рассылки по четвергам)"""
|
||||
url = (
|
||||
f"https://api.open-meteo.com/v1/forecast?"
|
||||
f"latitude={LAT}&longitude={LON}&"
|
||||
f"daily=weathercode,temperature_2m_max,temperature_2m_min,precipitation_probability_max&"
|
||||
f"timezone={TIMEZONE}&forecast_days=4" # Чт, Пт, Сб, Вс
|
||||
)
|
||||
data = await cls._fetch_raw(url)
|
||||
if not data:
|
||||
return "❌ Не удалось получить прогноз погоды на выходные."
|
||||
|
||||
daily = data["daily"]
|
||||
msg = ["🌦️ <b>Прогноз погоды на остаток недели и выходные (Чт - Вс):</b>"]
|
||||
|
||||
for i in range(4):
|
||||
date_str = daily["time"][i]
|
||||
date_obj = datetime.strptime(date_str, "%Y-%m-%d")
|
||||
ru_days = ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"]
|
||||
day_name = ru_days[date_obj.weekday()]
|
||||
formatted_date = f"{date_obj.strftime('%d.%m')} ({day_name})"
|
||||
|
||||
w_code = daily["weathercode"][i]
|
||||
emoji = cls.get_emoji(w_code)
|
||||
desc = cls.get_desc(w_code)
|
||||
t_max = daily["temperature_2m_max"][i]
|
||||
t_min = daily["temperature_2m_min"][i]
|
||||
prob = daily["precipitation_probability_max"][i]
|
||||
|
||||
msg.append(
|
||||
f"📅 <b>{formatted_date}:</b> {emoji} {desc}\n"
|
||||
f" 🌡️ от <code>{t_min:+.1f}°C</code> до <code>{t_max:+.1f}°C</code> | ☔ Дождь: <code>{prob}%</code>"
|
||||
)
|
||||
|
||||
return "\n\n".join(msg)
|
||||
Loading…
Reference in a new issue