From 80b923176e261036fa441be57818d07efc842399 Mon Sep 17 00:00:00 2001 From: Remzona54 Date: Wed, 13 May 2026 01:17:37 +0000 Subject: [PATCH] =?UTF-8?q?=D0=97=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B=20=D0=B2=20=C2=AB?= =?UTF-8?q?/=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- weather_component.py | 368 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 368 insertions(+) create mode 100644 weather_component.py diff --git a/weather_component.py b/weather_component.py new file mode 100644 index 0000000..405c467 --- /dev/null +++ b/weather_component.py @@ -0,0 +1,368 @@ +#!/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()