Загрузить файлы в «/»

This commit is contained in:
Алексей 2026-05-13 01:17:37 +00:00
parent cae8694200
commit 80b923176e

368
weather_component.py Normal file
View file

@ -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()