#!/usr/bin/env python3 # -*- coding: utf-8 -*- import logging import aiosqlite import requests import random import asyncio from datetime import datetime, timezone, timedelta from apscheduler.schedulers.asyncio import AsyncIOScheduler from slixmpp.componentxmpp import ComponentXMPP # ============================= # НАСТРОЙКИ # ============================= XMPP_JID = "weather.xmpp-life.ru" XMPP_SECRET = "45622Qazwsx" XMPP_SERVER = "192.168.0.141" XMPP_PORT = 5275 OPENWEATHER_API = "3ae5ff5d3692fd119a0dfb62cd33a739" DB_PATH = "weather.db" logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s" ) # ============================= # КЭШ # ============================= weather_cache = {} forecast_cache = {} CACHE_TTL = 120 # ============================= # УТИЛИТЫ # ============================= def wind_dir(deg): dirs = ["С","СВ","В","ЮВ","Ю","ЮЗ","З","СЗ"] return dirs[int((deg + 22.5) / 45) % 8] def funny_phrase(): return random.choice([ "☕ Самое время для кофе!", "🧥 Не забудь куртку!", "🌂 Захвати зонт!", "😎 Отличный день для прогулки!", "🔥 Погода радует!" ]) def menu_text(): return ( "🌤 СНЕГУРБОТ робот-метеоролог\n" "❤️ Сделан в мастерской РЕМ-ЗОНА54.РФ\n\n" "/setcity Москва\n" "/weather\n" "/forecast3\n" "/forecast5\n" "/notify on\n" "/notify off" ) # ============================= # API # ============================= def api_request(url, params): try: r = requests.get(url, params=params, timeout=10) data = r.json() if str(data.get("cod")) == "200": return data except: return None return None async def fetch_current(city): now = datetime.utcnow().timestamp() if city in weather_cache: data, ts = weather_cache[city] if now - ts < CACHE_TTL: return data data = await asyncio.to_thread( api_request, "https://api.openweathermap.org/data/2.5/weather", {"q": city, "appid": OPENWEATHER_API, "units": "metric", "lang": "ru"} ) if data: weather_cache[city] = (data, now) return data async def fetch_forecast(city): now = datetime.utcnow().timestamp() if city in forecast_cache: data, ts = forecast_cache[city] if now - ts < CACHE_TTL: return data data = await asyncio.to_thread( api_request, "https://api.openweathermap.org/data/2.5/forecast", {"q": city, "appid": OPENWEATHER_API, "units": "metric", "lang": "ru"} ) if data: forecast_cache[city] = (data, now) return data # ============================= # ФОРМАТ # ============================= def get_city_timezone(data): offset = data.get("timezone", 0) return timezone(timedelta(seconds=offset)) def format_current(data): if not data: return "❌ Ошибка получения погоды" tz = get_city_timezone(data) sunrise = datetime.fromtimestamp(data["sys"]["sunrise"], tz).strftime("%H:%M") sunset = datetime.fromtimestamp(data["sys"]["sunset"], tz).strftime("%H:%M") pressure = round(data["main"]["pressure"] * 0.75006) return ( f"📍 {data['name']}\n" f"{data['weather'][0]['description'].capitalize()}\n" f"🌡 {data['main']['temp']}°C (ощущается {data['main']['feels_like']}°C)\n" f"💧 Влажность: {data['main']['humidity']}%\n" f"🧭 Ветер: {data['wind']['speed']} м/с {wind_dir(data['wind'].get('deg', 0))}\n" f"🌡 Давление: {pressure} мм\n" f"🌅 {sunrise} | 🌇 {sunset}\n\n" f"{funny_phrase()}" ) def format_forecast(data, days=3): if not data: return "❌ Ошибка получения прогноза" result = "📅 Прогноз:\n\n" tz_offset = data["city"].get("timezone", 0) grouped = {} for item in data["list"]: dt = datetime.utcfromtimestamp(item["dt"] + tz_offset) date_key = dt.date() grouped.setdefault(date_key, []).append(item) count = 0 for date_key in sorted(grouped.keys()): if count >= days: break day_data = grouped[date_key] temps = [x["main"]["temp"] for x in day_data] desc = day_data[0]["weather"][0]["description"] result += ( f"📆 {date_key.strftime('%d.%m')}\n" f"{desc.capitalize()}\n" f"🌡 {round(min(temps))}…{round(max(temps))}°C\n\n" ) count += 1 return result # ============================= # БД # ============================= async def init_db(): async with aiosqlite.connect(DB_PATH) as db: await db.execute(""" CREATE TABLE IF NOT EXISTS users ( jid TEXT PRIMARY KEY, city TEXT, notify TEXT, last_notify TEXT ) """) await db.commit() async def get_user(jid): async with aiosqlite.connect(DB_PATH) as db: async with db.execute( "SELECT city, notify FROM users WHERE jid=?", (jid,) ) as cur: row = await cur.fetchone() return row if row else (None, "") async def save_city(jid, city): async with aiosqlite.connect(DB_PATH) as db: await db.execute(""" INSERT INTO users (jid, city) VALUES (?, ?) ON CONFLICT(jid) DO UPDATE SET city=excluded.city """, (jid, city)) await db.commit() async def set_notify(jid, value): async with aiosqlite.connect(DB_PATH) as db: await db.execute( "UPDATE users SET notify=? WHERE jid=?", (value, jid) ) await db.commit() # ============================= # XMPP # ============================= class WeatherComponent(ComponentXMPP): def __init__(self): super().__init__(XMPP_JID, XMPP_SECRET, XMPP_SERVER, XMPP_PORT) self.whitespace_keepalive = True self.whitespace_keepalive_interval = 30 self.add_event_handler("session_start", self.start) self.add_event_handler("message", self.message) self.scheduler = AsyncIOScheduler(event_loop=self.loop) async def start(self, event): logging.info("Компонент подключён") await init_db() if not self.scheduler.running: self.scheduler.add_job(self.notify_users, "cron", minute="*") self.scheduler.start() async def safe_send(self, jid, text): self.send_message( mto=jid, mbody=text, mtype="chat", mfrom=self.boundjid.bare ) async def message(self, msg): if msg["type"] not in ("chat", "normal"): return if not msg["body"]: return text = msg["body"].strip() jid = str(msg["from"]).split("/")[0] city, _ = await get_user(jid) if text == "/start": await self.safe_send(jid, menu_text()) elif text.startswith("/setcity"): parts = text.split(" ", 1) if len(parts) < 2: await self.safe_send(jid, "Укажи город: /setcity Москва") return city = parts[1] if not await fetch_current(city): await self.safe_send(jid, "❌ Город не найден") return await save_city(jid, city) await self.safe_send(jid, "✅ Город сохранён") elif text == "/weather": if not city: await self.safe_send(jid, "Сначала /setcity") return await self.safe_send(jid, format_current(await fetch_current(city))) elif text == "/forecast3": if not city: await self.safe_send(jid, "Сначала /setcity") return await self.safe_send(jid, format_forecast(await fetch_forecast(city), 3)) elif text == "/forecast5": if not city: await self.safe_send(jid, "Сначала /setcity") return await self.safe_send(jid, format_forecast(await fetch_forecast(city), 5)) elif text == "/notify on": await set_notify(jid, "on") await self.safe_send(jid, "🔔 Уведомления включены") elif text == "/notify off": await set_notify(jid, "") await self.safe_send(jid, "🔕 Уведомления выключены") else: await self.safe_send(jid, menu_text()) async def notify_users(self): async with aiosqlite.connect(DB_PATH) as db: async with db.execute( "SELECT jid, city, last_notify FROM users WHERE notify='on'" ) as cur: rows = await cur.fetchall() for jid, city, last_notify in rows: data = await fetch_current(city) if not data: continue tz = get_city_timezone(data) local_now = datetime.now(tz) if local_now.hour not in (7, 21) or local_now.minute > 1: continue key = f"{local_now.date()}_{local_now.hour}" if last_notify == key: continue prefix = "🌅 Доброе утро!\n\n" if local_now.hour == 7 else "🌙 Добрый вечер!\n\n" await self.safe_send(jid, prefix + format_current(data)) async with aiosqlite.connect(DB_PATH) as db: await db.execute( "UPDATE users SET last_notify=? WHERE jid=?", (key, jid) ) await db.commit() if __name__ == "__main__": xmpp = WeatherComponent() xmpp.connect() xmpp.loop.run_forever()