Refactor Dockerfile and main.py for improved environment variable handling and logging; update requirements.txt for package specifications
This commit is contained in:
@@ -8,6 +8,6 @@ RUN pip install --no-cache-dir -r requirements.txt
|
|||||||
|
|
||||||
# Копирование кода
|
# Копирование кода
|
||||||
COPY main.py .
|
COPY main.py .
|
||||||
|
COPY .env .
|
||||||
# Запуск бота
|
# Запуск бота
|
||||||
CMD ["python", "-u", "main.py"]
|
CMD ["python", "-u", "main.py"]
|
||||||
+433
-248
@@ -5,26 +5,84 @@ from bs4 import BeautifulSoup
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from aiogram import Bot, Dispatcher, types
|
from aiogram import Bot, Dispatcher, types
|
||||||
from aiogram.filters import Command
|
from aiogram.filters import Command
|
||||||
from aiogram.types import Message, ReplyKeyboardMarkup, KeyboardButton
|
from aiogram.types import Message, KeyboardButton
|
||||||
from aiogram.utils.keyboard import ReplyKeyboardBuilder
|
from aiogram.utils.keyboard import ReplyKeyboardBuilder
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from typing import Optional, List, Dict
|
||||||
|
|
||||||
|
# Загрузка переменных окружения
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
# Настройки
|
# Настройки
|
||||||
TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN", "YOUR_TOKEN_HERE")
|
TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN", "YOUR_TOKEN_HERE")
|
||||||
ALLOWED_CHAT_IDS = list(map(int, os.getenv("ALLOWED_CHAT_IDS", "").split(","))) if os.getenv("ALLOWED_CHAT_IDS") else []
|
ALLOWED_CHAT_IDS = list(map(int, os.getenv("ALLOWED_CHAT_IDS", "").split(","))) if os.getenv("ALLOWED_CHAT_IDS") else []
|
||||||
CHECK_INTERVAL = 120 # 2 минуты
|
CHECK_INTERVAL = int(os.getenv("CHECK_INTERVAL", "120"))
|
||||||
|
|
||||||
|
# Параметры для запроса
|
||||||
|
VOE_CITY_ID = int(os.getenv("VOE_CITY_ID", "VOE_CITY_ID"))
|
||||||
|
VOE_STREET_ID = int(os.getenv("VOE_STREET_ID", "VOE_STREET_ID"))
|
||||||
|
VOE_HOUSE_ID = int(os.getenv("VOE_HOUSE_ID", "VOE_HOUSE_ID"))
|
||||||
|
|
||||||
# Настройка логирования
|
# Настройка логирования
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Глобальные переменные
|
# Глобальные переменные
|
||||||
bot = Bot(token=TELEGRAM_TOKEN)
|
bot = Bot(token=TELEGRAM_TOKEN)
|
||||||
dp = Dispatcher()
|
dp = Dispatcher()
|
||||||
last_schedule = None
|
last_schedule: Optional[List[Dict]] = None
|
||||||
|
last_notification_time: Dict[str, datetime] = {}
|
||||||
|
|
||||||
|
|
||||||
def parse_html(html: str):
|
# ============================================================================
|
||||||
|
# УТИЛИТЫ
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def format_time_duration(minutes: int) -> str:
|
||||||
|
"""Форматирует время из минут в часы и минуты"""
|
||||||
|
hours = minutes // 60
|
||||||
|
mins = minutes % 60
|
||||||
|
|
||||||
|
if hours == 0:
|
||||||
|
return f"{mins}м"
|
||||||
|
elif mins == 0:
|
||||||
|
return f"{hours}ч"
|
||||||
|
return f"{hours}ч {mins}м"
|
||||||
|
|
||||||
|
|
||||||
|
def get_day_statistics(day_blocks: List[Dict]) -> Dict[str, int]:
|
||||||
|
"""Получает статистику по дню"""
|
||||||
|
total_minutes = 0
|
||||||
|
confirmed_minutes = 0
|
||||||
|
possible_minutes = 0
|
||||||
|
|
||||||
|
for block in day_blocks:
|
||||||
|
for half in [block["first_half"], block["second_half"]]:
|
||||||
|
if half["status"] == "off":
|
||||||
|
total_minutes += 30
|
||||||
|
if half["confirmed"]:
|
||||||
|
confirmed_minutes += 30
|
||||||
|
else:
|
||||||
|
possible_minutes += 30
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total": total_minutes,
|
||||||
|
"confirmed": confirmed_minutes,
|
||||||
|
"possible": possible_minutes
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# ПАРСИНГ ДАННЫХ
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def parse_html(html: str) -> List[Dict]:
|
||||||
|
"""Парсит HTML с графиком отключений"""
|
||||||
soup = BeautifulSoup(html, "html.parser")
|
soup = BeautifulSoup(html, "html.parser")
|
||||||
cells = soup.select(".disconnection-detailed-table-cell.cell")
|
cells = soup.select(".disconnection-detailed-table-cell.cell")
|
||||||
|
|
||||||
@@ -46,28 +104,29 @@ def parse_html(html: str):
|
|||||||
left = hour_block.select_one(".half.left")
|
left = hour_block.select_one(".half.left")
|
||||||
right = hour_block.select_one(".half.right")
|
right = hour_block.select_one(".half.right")
|
||||||
|
|
||||||
def parse_half(half, force_off=False):
|
def parse_half(half, force_off: bool = False) -> Dict:
|
||||||
if not half:
|
if not half:
|
||||||
return {"status": "unknown", "queue": None, "confirmed": None}
|
return {"status": "unknown", "queue": None, "confirmed": None}
|
||||||
|
|
||||||
if force_off:
|
status = "off" if force_off else "unknown"
|
||||||
status = "off"
|
if not force_off:
|
||||||
else:
|
|
||||||
classes = half.get("class", [])
|
classes = half.get("class", [])
|
||||||
if "has_disconnection" in classes:
|
if "has_disconnection" in classes:
|
||||||
status = "off"
|
status = "off"
|
||||||
elif "no_disconnection" in classes:
|
elif "no_disconnection" in classes:
|
||||||
status = "on"
|
status = "on"
|
||||||
else:
|
|
||||||
status = "unknown"
|
|
||||||
|
|
||||||
queue = None
|
queue = None
|
||||||
confirmed = None
|
confirmed = None
|
||||||
disconnection_div = half.select_one(".disconnection")
|
disconnection_div = half.select_one(".disconnection")
|
||||||
|
|
||||||
if disconnection_div and disconnection_div.has_attr("title"):
|
if disconnection_div and disconnection_div.has_attr("title"):
|
||||||
title = disconnection_div["title"]
|
title = disconnection_div["title"]
|
||||||
if "Номер черги" in title:
|
if "Номер черги" in title:
|
||||||
queue = title.split(":")[-1].strip()
|
try:
|
||||||
|
queue = title.split(":")[-1].strip()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
disconnection_classes = disconnection_div.get("class", [])
|
disconnection_classes = disconnection_div.get("class", [])
|
||||||
if "disconnection_confirm_1" in disconnection_classes:
|
if "disconnection_confirm_1" in disconnection_classes:
|
||||||
@@ -75,7 +134,7 @@ def parse_html(html: str):
|
|||||||
elif "disconnection_confirm_0" in disconnection_classes:
|
elif "disconnection_confirm_0" in disconnection_classes:
|
||||||
confirmed = False
|
confirmed = False
|
||||||
|
|
||||||
if force_off and not confirmed and not queue:
|
if force_off and confirmed is None:
|
||||||
if 'confirm_1' in cell_classes:
|
if 'confirm_1' in cell_classes:
|
||||||
confirmed = True
|
confirmed = True
|
||||||
elif 'confirm_0' in cell_classes:
|
elif 'confirm_0' in cell_classes:
|
||||||
@@ -101,12 +160,13 @@ def parse_html(html: str):
|
|||||||
return schedule
|
return schedule
|
||||||
|
|
||||||
|
|
||||||
def get_voe_html(city_id, street_id, house_id):
|
def get_voe_html(city_id: int, street_id: int, house_id: int) -> str:
|
||||||
|
"""Получает HTML с сайта VOE"""
|
||||||
url = "https://www.voe.com.ua/disconnection/detailed?ajax_form=1&_wrapper_format=drupal_ajax"
|
url = "https://www.voe.com.ua/disconnection/detailed?ajax_form=1&_wrapper_format=drupal_ajax"
|
||||||
headers = {
|
headers = {
|
||||||
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||||
"X-Requested-With": "XMLHttpRequest",
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
"User-Agent": "Mozilla/5.0 (compatible; PowerBot/1.0)"
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
||||||
}
|
}
|
||||||
data = {
|
data = {
|
||||||
"search_type": 0,
|
"search_type": 0,
|
||||||
@@ -119,184 +179,208 @@ def get_voe_html(city_id, street_id, house_id):
|
|||||||
"_triggering_element_value": "Показати",
|
"_triggering_element_value": "Показати",
|
||||||
"_drupal_ajax": 1,
|
"_drupal_ajax": 1,
|
||||||
}
|
}
|
||||||
r = requests.post(url, headers=headers, data=data)
|
|
||||||
r.raise_for_status()
|
try:
|
||||||
resp_json = json.loads(r.text)
|
response = requests.post(url, headers=headers, data=data, timeout=10)
|
||||||
insert_html = next((i["data"] for i in resp_json if i.get("command") == "insert"), None)
|
response.raise_for_status()
|
||||||
if not insert_html:
|
resp_json = response.json()
|
||||||
raise ValueError("HTML не найден в ответе")
|
|
||||||
return insert_html
|
insert_html = next(
|
||||||
|
(item["data"] for item in resp_json if item.get("command") == "insert"),
|
||||||
|
None
|
||||||
|
)
|
||||||
|
|
||||||
|
if not insert_html:
|
||||||
|
raise ValueError("HTML не найден в ответе")
|
||||||
|
|
||||||
|
return insert_html
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logger.error(f"Ошибка запроса VOE: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# ФОРМАТИРОВАНИЕ СООБЩЕНИЙ
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
def get_main_keyboard():
|
def get_main_keyboard():
|
||||||
"""Создает главную клавиатуру с кнопками"""
|
"""Создает главную клавиатуру"""
|
||||||
builder = ReplyKeyboardBuilder()
|
builder = ReplyKeyboardBuilder()
|
||||||
builder.row(
|
builder.row(
|
||||||
KeyboardButton(text="📊 Графік"),
|
KeyboardButton(text="📊 Графік"),
|
||||||
KeyboardButton(text="🔄 Перевірити")
|
KeyboardButton(text="🔄 Оновити")
|
||||||
)
|
)
|
||||||
builder.row(
|
builder.row(
|
||||||
KeyboardButton(text="📅 Сьогодні"),
|
KeyboardButton(text="📅 Сьогодні"),
|
||||||
KeyboardButton(text="📅 Завтра")
|
KeyboardButton(text="📅 Завтра")
|
||||||
)
|
)
|
||||||
|
builder.row(KeyboardButton(text="ℹ️ Про бота"))
|
||||||
return builder.as_markup(resize_keyboard=True)
|
return builder.as_markup(resize_keyboard=True)
|
||||||
|
|
||||||
|
|
||||||
def format_schedule_message(schedule, days_to_show=2):
|
def format_schedule_message(schedule: List[Dict], days_to_show: int = 2) -> str:
|
||||||
"""Форматирует расписание для отправки в Telegram"""
|
"""Форматирует полный график на несколько дней"""
|
||||||
message = f"⚡️ <b>Графік відключень світла</b>\n"
|
lines = [
|
||||||
message += f"🕐 Оновлено: {datetime.now().strftime('%d.%m.%Y %H:%M')}\n\n"
|
"⚡️ <b>Графік відключень світла</b>",
|
||||||
|
f"🕐 Оновлено: {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}",
|
||||||
|
"─" * 30,
|
||||||
|
""
|
||||||
|
]
|
||||||
|
|
||||||
start_date = datetime.now()
|
start_date = datetime.now()
|
||||||
|
|
||||||
for day in range(days_to_show):
|
for day in range(min(days_to_show, 2)):
|
||||||
day_blocks = [b for b in schedule if b["day"] == day]
|
day_blocks = [b for b in schedule if b["day"] == day]
|
||||||
if not day_blocks:
|
if not day_blocks:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
date_str = (start_date + timedelta(days=day)).strftime('%d.%m')
|
date_str = (start_date + timedelta(days=day)).strftime('%d.%m.%Y')
|
||||||
day_name = "🌅 Сьогодні" if day == 0 else "🌄 Завтра"
|
day_name = "🌅 <b>Сьогодні</b>" if day == 0 else "🌄 <b>Завтра</b>"
|
||||||
|
|
||||||
message += f"{'─' * 32}\n"
|
lines.append(f"{day_name} ({date_str})")
|
||||||
message += f"📅 <b>{day_name} ({date_str})</b>\n"
|
|
||||||
message += f"{'─' * 32}\n\n"
|
|
||||||
|
|
||||||
# Группируем последовательные отключения
|
# Статистика
|
||||||
current_time = None
|
stats = get_day_statistics(day_blocks)
|
||||||
|
if stats["total"] > 0:
|
||||||
|
lines.append(f"⏱ Всього: <code>{format_time_duration(stats['total'])}</code>")
|
||||||
|
if stats["confirmed"] > 0:
|
||||||
|
lines.append(f"🔴 Підтверджено: <code>{format_time_duration(stats['confirmed'])}</code>")
|
||||||
|
if stats["possible"] > 0:
|
||||||
|
lines.append(f"🟠 Можливо: <code>{format_time_duration(stats['possible'])}</code>")
|
||||||
|
else:
|
||||||
|
lines.append("🟢 <b>Відключень немає!</b>")
|
||||||
|
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
# Детальный список отключений
|
||||||
|
disconnections = []
|
||||||
current_status = None
|
current_status = None
|
||||||
|
start_time = None
|
||||||
current_confirmed = None
|
current_confirmed = None
|
||||||
current_queue = None
|
current_queue = None
|
||||||
start_time = None
|
|
||||||
|
|
||||||
disconnection_periods = []
|
for block in day_blocks:
|
||||||
|
|
||||||
for i, block in enumerate(day_blocks):
|
|
||||||
hour = block["hour"]
|
hour = block["hour"]
|
||||||
|
|
||||||
# Проверяем обе половины часа
|
|
||||||
for half_idx, half in enumerate([block["first_half"], block["second_half"]]):
|
for half_idx, half in enumerate([block["first_half"], block["second_half"]]):
|
||||||
time_str = f"{hour:02d}:00" if half_idx == 0 else f"{hour:02d}:30"
|
time_str = f"{hour:02d}:00" if half_idx == 0 else f"{hour:02d}:30"
|
||||||
|
|
||||||
if half["status"] == "off":
|
if half["status"] == "off":
|
||||||
if current_status != "off":
|
if current_status != "off":
|
||||||
# Начало нового периода отключения
|
|
||||||
start_time = time_str
|
start_time = time_str
|
||||||
current_confirmed = half["confirmed"]
|
current_confirmed = half["confirmed"]
|
||||||
current_queue = half["queue"]
|
current_queue = half["queue"]
|
||||||
current_status = "off"
|
current_status = "off"
|
||||||
else:
|
else:
|
||||||
if current_status == "off":
|
if current_status == "off":
|
||||||
# Конец периода отключения
|
icon = "🔴" if current_confirmed else "🟠"
|
||||||
icon = "🔴" if current_confirmed is True else "🟠"
|
queue_text = f" (Ч{current_queue})" if current_queue else ""
|
||||||
queue_text = f" • Черга {current_queue}" if current_queue else ""
|
disconnections.append(f"{icon} <code>{start_time} - {time_str}</code>{queue_text}")
|
||||||
disconnection_periods.append(f"{icon} <code>{start_time} - {time_str}</code>{queue_text}")
|
|
||||||
current_status = half["status"]
|
current_status = half["status"]
|
||||||
|
|
||||||
# Если день закончился на отключении
|
# Если день закончился на отключении
|
||||||
if current_status == "off":
|
if current_status == "off":
|
||||||
icon = "🔴" if current_confirmed is True else "🟠"
|
icon = "🔴" if current_confirmed else "🟠"
|
||||||
queue_text = f" • Черга {current_queue}" if current_queue else ""
|
queue_text = f" (Ч{current_queue})" if current_queue else ""
|
||||||
next_hour = (day_blocks[-1]["hour"] + 1) % 24
|
next_hour = (day_blocks[-1]["hour"] + 1) % 24
|
||||||
end_time = f"{next_hour:02d}:00"
|
end_time = f"{next_hour:02d}:00"
|
||||||
disconnection_periods.append(f"{icon} <code>{start_time} - {end_time}</code>{queue_text}")
|
disconnections.append(f"{icon} <code>{start_time} - {end_time}</code>{queue_text}")
|
||||||
|
|
||||||
# Выводим результат
|
if disconnections:
|
||||||
if disconnection_periods:
|
for idx, disc in enumerate(disconnections, 1):
|
||||||
for period in disconnection_periods:
|
lines.append(f"{idx}. {disc}")
|
||||||
message += period + "\n"
|
|
||||||
else:
|
|
||||||
message += "🟢 <b>Відключень немає!</b>\n"
|
|
||||||
|
|
||||||
message += "\n"
|
lines.append("")
|
||||||
|
|
||||||
message += f"{'─' * 32}\n"
|
lines.append("<i>🔴 = підтверджено • 🟠 = можливо • 🟢 = світло</i>")
|
||||||
message += "💡 <i>Легенда:</i>\n"
|
|
||||||
message += "🔴 — Підтверджено\n"
|
|
||||||
message += "🟠 — Можливо\n"
|
|
||||||
message += "🟢 — Світло є\n"
|
|
||||||
|
|
||||||
return message
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
def format_single_day_schedule(schedule, day_num):
|
def format_single_day_schedule(schedule: List[Dict], day: int) -> str:
|
||||||
"""Форматирует расписание для одного дня"""
|
"""Форматирует график на один день"""
|
||||||
start_date = datetime.now()
|
day_blocks = [b for b in schedule if b["day"] == day]
|
||||||
date_str = (start_date + timedelta(days=day_num)).strftime('%d.%m')
|
|
||||||
day_name = "🌅 Сьогодні" if day_num == 0 else "🌄 Завтра"
|
|
||||||
|
|
||||||
message = f"{'─' * 32}\n"
|
|
||||||
message += f"📅 <b>{day_name} ({date_str})</b>\n"
|
|
||||||
message += f"{'─' * 32}\n\n"
|
|
||||||
|
|
||||||
day_blocks = [b for b in schedule if b["day"] == day_num]
|
|
||||||
if not day_blocks:
|
if not day_blocks:
|
||||||
return message + "❓ <i>Немає даних</i>"
|
return "❌ Немає даних для цього дня"
|
||||||
|
|
||||||
# Создаем визуальную шкалу времени
|
start_date = datetime.now()
|
||||||
message += "⏰ <b>Часова шкала:</b>\n\n"
|
date_str = (start_date + timedelta(days=day)).strftime('%d.%m.%Y')
|
||||||
|
day_name = "🟠 <b>Сьогодні</b>" if day == 0 else "🔶 <b>Завтра</b>"
|
||||||
|
|
||||||
# Выводим по 4 часа в строке для читаемости
|
lines = [
|
||||||
for start_hour in range(0, 24, 4):
|
f"{day_name} • {date_str}",
|
||||||
line = ""
|
""
|
||||||
for h in range(start_hour, min(start_hour + 4, 24)):
|
]
|
||||||
block = day_blocks[h] if h < len(day_blocks) else None
|
|
||||||
if not block:
|
# Статистика
|
||||||
line += f"{h:02d}|❓❓ "
|
lines.append("<b>📊 Статистика</b>")
|
||||||
continue
|
stats = get_day_statistics(day_blocks)
|
||||||
|
|
||||||
first = block["first_half"]
|
if stats["total"] == 0:
|
||||||
second = block["second_half"]
|
lines.append("└ 🟢 <b>Відключень немає!</b>")
|
||||||
|
else:
|
||||||
def get_icon(half_data):
|
total_time = format_time_duration(stats["total"])
|
||||||
if half_data["status"] == "off":
|
lines.append(f"├ ⏱ Всього: <code>{total_time}</code>")
|
||||||
return "🔴" if half_data["confirmed"] is True else "🟠"
|
|
||||||
elif half_data["status"] == "on":
|
|
||||||
return "🟢"
|
|
||||||
return "❓"
|
|
||||||
|
|
||||||
first_icon = "●" if first["status"] == "off" else "·"
|
|
||||||
second_icon = "●" if second["status"] == "off" else "·"
|
|
||||||
|
|
||||||
line += f"<code>{h:02d}|{first_icon}{second_icon}</code> "
|
|
||||||
|
|
||||||
message += line + "\n"
|
if stats["confirmed"] > 0:
|
||||||
|
confirmed_time = format_time_duration(stats["confirmed"])
|
||||||
|
lines.append(f"├ 🔴 Підтверджено: <code>{confirmed_time}</code>")
|
||||||
|
|
||||||
|
if stats["possible"] > 0:
|
||||||
|
possible_time = format_time_duration(stats["possible"])
|
||||||
|
lines.append(f"└ 🟠 Можливо: <code>{possible_time}</code>")
|
||||||
|
else:
|
||||||
|
lines.append(f"└ 🟢 Решта часу світло")
|
||||||
|
|
||||||
message += "\n<i>● = відключено, · = світло</i>\n\n"
|
lines.append("")
|
||||||
|
|
||||||
# Детальний список відключень
|
# Детальный список отключений
|
||||||
message += "📋 <b>Детально:</b>\n\n"
|
disconnections = []
|
||||||
|
current_status = None
|
||||||
|
start_time = None
|
||||||
|
current_confirmed = None
|
||||||
|
current_queue = None
|
||||||
|
|
||||||
has_disconnections = False
|
|
||||||
for block in day_blocks:
|
for block in day_blocks:
|
||||||
hour = block["hour"]
|
hour = block["hour"]
|
||||||
first = block["first_half"]
|
|
||||||
second = block["second_half"]
|
|
||||||
|
|
||||||
if first["status"] == "off":
|
for half_idx, half in enumerate([block["first_half"], block["second_half"]]):
|
||||||
has_disconnections = True
|
time_str = f"{hour:02d}:00" if half_idx == 0 else f"{hour:02d}:30"
|
||||||
icon = "🔴" if first["confirmed"] is True else "🟠"
|
|
||||||
status = "підтв." if first["confirmed"] is True else "можл."
|
if half["status"] == "off":
|
||||||
queue = f"Ч{first['queue']}" if first['queue'] else "—"
|
if current_status != "off":
|
||||||
message += f"{icon} <code>{hour:02d}:00-{hour:02d}:30</code> • {status} • {queue}\n"
|
start_time = time_str
|
||||||
|
current_confirmed = half["confirmed"]
|
||||||
if second["status"] == "off":
|
current_queue = half["queue"]
|
||||||
has_disconnections = True
|
current_status = "off"
|
||||||
icon = "🔴" if second["confirmed"] is True else "🟠"
|
else:
|
||||||
status = "підтв." if second["confirmed"] is True else "можл."
|
if current_status == "off":
|
||||||
queue = f"Ч{second['queue']}" if second['queue'] else "—"
|
icon = "🔴" if current_confirmed else "🟠"
|
||||||
message += f"{icon} <code>{hour:02d}:30-{hour+1:02d}:00</code> • {status} • {queue}\n"
|
queue_text = f" (Ч.{current_queue})" if current_queue else ""
|
||||||
|
disconnections.append(f"{icon} <code>{start_time} - {time_str}</code>{queue_text}")
|
||||||
|
current_status = half["status"]
|
||||||
|
|
||||||
if not has_disconnections:
|
# Если день закончился на отключении
|
||||||
message += "🟢 <b>Відключень немає!</b>\n"
|
if current_status == "off":
|
||||||
|
icon = "🔴" if current_confirmed else "🟠"
|
||||||
|
queue_text = f" (Ч.{current_queue})" if current_queue else ""
|
||||||
|
next_hour = (day_blocks[-1]["hour"] + 1) % 24
|
||||||
|
end_time = f"{next_hour:02d}:00"
|
||||||
|
disconnections.append(f"{icon} <code>{start_time} - {end_time}</code>{queue_text}")
|
||||||
|
|
||||||
message += f"\n{'─' * 32}\n"
|
if disconnections:
|
||||||
message += "<i>🔴 підтв. • 🟠 можл. • 🟢 світло</i>"
|
lines.append("<b>⚡️ Розклад відключень</b>")
|
||||||
|
for idx, disc in enumerate(disconnections, 1):
|
||||||
|
lines.append(f"{idx}. {disc}")
|
||||||
|
|
||||||
return message
|
lines.append("")
|
||||||
|
lines.append("<i>🔴 підтверджено • 🟠 можливо • 🟢 світло</i>")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
def schedules_differ(old_schedule, new_schedule):
|
def schedules_differ(old_schedule: Optional[List[Dict]], new_schedule: Optional[List[Dict]]) -> bool:
|
||||||
"""Проверяет, отличаются ли графики"""
|
"""Проверяет отличия между графиками"""
|
||||||
if old_schedule is None or new_schedule is None:
|
if old_schedule is None or new_schedule is None:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -304,7 +388,6 @@ def schedules_differ(old_schedule, new_schedule):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
for old, new in zip(old_schedule, new_schedule):
|
for old, new in zip(old_schedule, new_schedule):
|
||||||
# Сравниваем только первые 2 дня
|
|
||||||
if old["day"] >= 2:
|
if old["day"] >= 2:
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -315,256 +398,358 @@ def schedules_differ(old_schedule, new_schedule):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
async def send_to_all_users(message_text: str):
|
# ============================================================================
|
||||||
|
# УВЕДОМЛЕНИЯ
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
async def send_to_all_users(message_text: str, parse_mode: str = "HTML"):
|
||||||
"""Отправляет сообщение всем пользователям"""
|
"""Отправляет сообщение всем пользователям"""
|
||||||
|
if not ALLOWED_CHAT_IDS:
|
||||||
|
logger.warning("Нет допущенных ID чатов для отправки уведомлений")
|
||||||
|
return
|
||||||
|
|
||||||
for chat_id in ALLOWED_CHAT_IDS:
|
for chat_id in ALLOWED_CHAT_IDS:
|
||||||
try:
|
try:
|
||||||
await bot.send_message(chat_id, message_text, parse_mode="HTML")
|
await bot.send_message(chat_id, message_text, parse_mode=parse_mode)
|
||||||
logger.info(f"Сообщение отправлено пользователю {chat_id}")
|
logger.info(f"✅ Сообщение отправлено пользователю {chat_id}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка отправки пользователю {chat_id}: {e}")
|
logger.error(f"❌ Ошибка отправки пользователю {chat_id}: {e}")
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
|
|
||||||
async def check_schedule():
|
async def check_schedule():
|
||||||
"""Проверяет график и отправляет уведомления при изменениях"""
|
"""Проверяет график и отправляет уведомления"""
|
||||||
global last_schedule
|
global last_schedule
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logger.info("Проверка графика...")
|
logger.info("🔍 Проверка графика...")
|
||||||
html = get_voe_html(VOE_CITY_ID, VOE_STREET_ID, VOE_HOUSE_ID)
|
html = get_voe_html(VOE_CITY_ID, VOE_STREET_ID, VOE_HOUSE_ID)
|
||||||
new_schedule = parse_html(html)
|
new_schedule = parse_html(html)
|
||||||
|
|
||||||
if schedules_differ(last_schedule, new_schedule):
|
if schedules_differ(last_schedule, new_schedule):
|
||||||
logger.info("Обнаружены изменения в графике!")
|
logger.info("✨ Обнаружены изменения!")
|
||||||
message = format_schedule_message(new_schedule, days_to_show=2)
|
message = format_schedule_message(new_schedule, days_to_show=2)
|
||||||
|
|
||||||
if last_schedule is None:
|
if last_schedule is not None:
|
||||||
pass # Ничего не делать, если изменений нет
|
|
||||||
else:
|
|
||||||
# Изменения в графике
|
|
||||||
await send_to_all_users(f"🔄 <b>Графік оновлено!</b>\n\n{message}")
|
await send_to_all_users(f"🔄 <b>Графік оновлено!</b>\n\n{message}")
|
||||||
|
|
||||||
last_schedule = new_schedule
|
last_schedule = new_schedule
|
||||||
else:
|
else:
|
||||||
logger.info("Изменений нет")
|
logger.info("✓ Графік без змін")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка при проверке графика: {e}")
|
logger.error(f"❌ Ошибка при проверке графика: {e}")
|
||||||
await send_to_all_users(f"❌ <b>Помилка!</b>\n\nНе вдалося отримати графік: {str(e)}")
|
|
||||||
|
|
||||||
|
|
||||||
async def check_upcoming_disconnections():
|
async def check_upcoming_disconnections():
|
||||||
"""Проверяет предстоящие отключения и включения, отправляет предупреждения"""
|
"""Проверяет предстоящие события и отправляет предупреждения за 5 минут"""
|
||||||
global last_schedule
|
global last_notification_time
|
||||||
|
|
||||||
if last_schedule is None:
|
if last_schedule is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
current_time = now.time()
|
|
||||||
|
|
||||||
# Проверяем только текущий день
|
|
||||||
today_blocks = [b for b in last_schedule if b["day"] == 0]
|
today_blocks = [b for b in last_schedule if b["day"] == 0]
|
||||||
|
|
||||||
|
# Создаем список всех переходов (off -> on или on -> off)
|
||||||
|
transitions = []
|
||||||
|
prev_status = None
|
||||||
|
|
||||||
for block in today_blocks:
|
for block in today_blocks:
|
||||||
hour = block["hour"]
|
hour = block["hour"]
|
||||||
|
|
||||||
# Проверяем первую половину часа (XX:00 - XX:30)
|
for half_idx, half in enumerate([block["first_half"], block["second_half"]]):
|
||||||
first_half_start = now.replace(hour=hour, minute=0, second=0, microsecond=0)
|
minute = 0 if half_idx == 0 else 30
|
||||||
time_until_first = (first_half_start - now).total_seconds() / 60
|
time_str = f"{hour:02d}:{minute:02d}"
|
||||||
|
|
||||||
# Если до события осталось 5 минут (с погрешностью ±1 минута)
|
|
||||||
if 4 <= time_until_first <= 6:
|
|
||||||
first = block["first_half"]
|
|
||||||
|
|
||||||
# ОТКЛЮЧЕНИЕ
|
current_status = half["status"]
|
||||||
if first["status"] == "off":
|
|
||||||
icon = "🔴" if first["confirmed"] is True else "🟠"
|
# Если статус изменился - это переход
|
||||||
status_text = "підтверджено" if first["confirmed"] is True else "можливе"
|
if prev_status is not None and prev_status != current_status:
|
||||||
queue_info = f" (Черга {first['queue']})" if first["queue"] else ""
|
transitions.append({
|
||||||
|
"hour": hour,
|
||||||
|
"minute": minute,
|
||||||
|
"time_str": time_str,
|
||||||
|
"from_status": prev_status,
|
||||||
|
"to_status": current_status,
|
||||||
|
"confirmed": half.get("confirmed"),
|
||||||
|
"queue": half.get("queue")
|
||||||
|
})
|
||||||
|
|
||||||
|
prev_status = current_status
|
||||||
|
|
||||||
|
# Проверяем переходы
|
||||||
|
for transition in transitions:
|
||||||
|
event_time = now.replace(
|
||||||
|
hour=transition["hour"],
|
||||||
|
minute=transition["minute"],
|
||||||
|
second=0,
|
||||||
|
microsecond=0
|
||||||
|
)
|
||||||
|
|
||||||
|
time_until = (event_time - now).total_seconds() / 60
|
||||||
|
notification_key = f"{transition['hour']}:{transition['minute']}_{transition['to_status']}"
|
||||||
|
|
||||||
|
# Если за 5 минут до события (±1 минута) и еще не отправляли
|
||||||
|
if 4 <= time_until <= 6:
|
||||||
|
# Проверяем, не отправляли ли уже уведомление сегодня
|
||||||
|
if notification_key in last_notification_time:
|
||||||
|
last_notif_time = last_notification_time[notification_key]
|
||||||
|
if last_notif_time.date() == now.date():
|
||||||
|
continue # Уже отправляли сегодня
|
||||||
|
|
||||||
|
# Переход на ОТКЛЮЧЕНИЕ (on -> off)
|
||||||
|
if transition["from_status"] == "on" and transition["to_status"] == "off":
|
||||||
|
icon = "🔴" if transition["confirmed"] else "🟠"
|
||||||
|
status = "підтверджено" if transition["confirmed"] else "можливе"
|
||||||
|
queue_info = f" (Черга {transition['queue']})" if transition['queue'] else ""
|
||||||
|
|
||||||
warning = (
|
warning = (
|
||||||
f"⚠️ <b>УВАГА! ВІДКЛЮЧЕННЯ</b>\n\n"
|
f"⚠️ <b>УВАГА! ВІДКЛЮЧЕННЯ</b>\n\n"
|
||||||
f"Через 5 хвилин відключення {icon}\n"
|
f"Через ~5 хвилин\n"
|
||||||
f"Час: {hour:02d}:00 - {hour:02d}:30\n"
|
f"Час: <code>{transition['time_str']}</code>\n"
|
||||||
f"Статус: {status_text}{queue_info}"
|
f"Статус: {icon} {status}{queue_info}"
|
||||||
)
|
)
|
||||||
|
|
||||||
await send_to_all_users(warning)
|
await send_to_all_users(warning)
|
||||||
logger.info(f"Отправлено предупреждение о отключении в {hour:02d}:00")
|
last_notification_time[notification_key] = now
|
||||||
|
logger.info(f"📢 Відправлено попередження про ВІДКЛЮЧЕННЯ в {transition['time_str']}")
|
||||||
|
|
||||||
# ВКЛЮЧЕНИЕ
|
# Переход на ВКЛЮЧЕНИЕ (off -> on)
|
||||||
elif first["status"] == "on":
|
elif transition["from_status"] == "off" and transition["to_status"] == "on":
|
||||||
# Проверяем, было ли отключение до этого
|
|
||||||
prev_block = today_blocks[today_blocks.index(block) - 1] if today_blocks.index(block) > 0 else None
|
|
||||||
was_off = False
|
|
||||||
|
|
||||||
if prev_block:
|
|
||||||
was_off = prev_block["second_half"]["status"] == "off"
|
|
||||||
|
|
||||||
if was_off:
|
|
||||||
warning = (
|
|
||||||
f"✅ <b>УВАГА! ВКЛЮЧЕННЯ</b>\n\n"
|
|
||||||
f"Через 5 хвилин світло має з'явитися 🟢\n"
|
|
||||||
f"Час: {hour:02d}:00"
|
|
||||||
)
|
|
||||||
await send_to_all_users(warning)
|
|
||||||
logger.info(f"Отправлено предупреждение о включении в {hour:02d}:00")
|
|
||||||
|
|
||||||
# Проверяем вторую половину часа (XX:30 - XX+1:00)
|
|
||||||
second_half_start = now.replace(hour=hour, minute=30, second=0, microsecond=0)
|
|
||||||
time_until_second = (second_half_start - now).total_seconds() / 60
|
|
||||||
|
|
||||||
if 4 <= time_until_second <= 6:
|
|
||||||
second = block["second_half"]
|
|
||||||
|
|
||||||
# ОТКЛЮЧЕНИЕ
|
|
||||||
if second["status"] == "off":
|
|
||||||
icon = "🔴" if second["confirmed"] is True else "🟠"
|
|
||||||
status_text = "підтверджено" if second["confirmed"] is True else "можливе"
|
|
||||||
queue_info = f" (Черга {second['queue']})" if second["queue"] else ""
|
|
||||||
|
|
||||||
warning = (
|
warning = (
|
||||||
f"⚠️ <b>УВАГА! ВІДКЛЮЧЕННЯ</b>\n\n"
|
f"✅ <b>УВАГА! ВКЛЮЧЕННЯ</b>\n\n"
|
||||||
f"Через 5 хвилин відключення {icon}\n"
|
f"Через ~5 хвилин буде світло\n"
|
||||||
f"Час: {hour:02d}:30 - {hour+1:02d}:00\n"
|
f"Час: <code>{transition['time_str']}</code>"
|
||||||
f"Статус: {status_text}{queue_info}"
|
|
||||||
)
|
)
|
||||||
await send_to_all_users(warning)
|
|
||||||
logger.info(f"Отправлено предупреждение о отключении в {hour:02d}:30")
|
|
||||||
|
|
||||||
# ВКЛЮЧЕНИЕ
|
|
||||||
elif second["status"] == "on":
|
|
||||||
# Проверяем, было ли отключение в первой половине
|
|
||||||
was_off = block["first_half"]["status"] == "off"
|
|
||||||
|
|
||||||
if was_off:
|
await send_to_all_users(warning)
|
||||||
warning = (
|
last_notification_time[notification_key] = now
|
||||||
f"✅ <b>УВАГА! ВКЛЮЧЕННЯ</b>\n\n"
|
logger.info(f"📢 Відправлено попередження про ВКЛЮЧЕННЯ в {transition['time_str']}")
|
||||||
f"Через 5 хвилин світло має з'явитися 🟢\n"
|
|
||||||
f"Час: {hour:02d}:30"
|
|
||||||
)
|
|
||||||
await send_to_all_users(warning)
|
|
||||||
logger.info(f"Отправлено предупреждение о включении в {hour:02d}:30")
|
|
||||||
|
|
||||||
|
|
||||||
async def monitoring_loop():
|
async def monitoring_loop():
|
||||||
"""Основной цикл мониторинга"""
|
"""Основной цикл мониторинга"""
|
||||||
# Первая проверка сразу при запуске
|
|
||||||
await check_schedule()
|
await check_schedule()
|
||||||
|
|
||||||
# Затем проверяем каждые 2 минуты
|
|
||||||
while True:
|
while True:
|
||||||
await asyncio.sleep(CHECK_INTERVAL)
|
try:
|
||||||
await check_schedule()
|
await asyncio.sleep(CHECK_INTERVAL)
|
||||||
await check_upcoming_disconnections()
|
await check_schedule()
|
||||||
|
await check_upcoming_disconnections()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка в цикле мониторинга: {e}")
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# ОБРАБОТЧИКИ КОМАНД
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
@dp.message(Command("start"))
|
@dp.message(Command("start"))
|
||||||
async def cmd_start(message: Message):
|
async def cmd_start(message: Message):
|
||||||
"""Обработчик команды /start"""
|
"""Обработчик /start"""
|
||||||
if message.chat.id not in ALLOWED_CHAT_IDS:
|
if message.chat.id not in ALLOWED_CHAT_IDS:
|
||||||
await message.answer("❌ У вас немає доступу до цього бота.")
|
await message.answer("❌ У вас немає доступу до цього бота.")
|
||||||
return
|
return
|
||||||
|
|
||||||
await message.answer(
|
await message.answer(
|
||||||
"👋 <b>Вітаю!</b>\n\n"
|
"👋 <b>Ласкаво просимо!</b>\n\n"
|
||||||
"Я відстежую зміни в графіку відключень світла та попереджаю за 5 хвилин до відключення.\n\n"
|
"🤖 <b>Бот для моніторингу графіку відключень світла</b>\n\n"
|
||||||
|
"✨ <b>Можливості:</b>\n"
|
||||||
|
"• 📊 Перегляд графіку на сьогодні і завтра\n"
|
||||||
|
"• 🔔 Автоматичні сповіщення за 5 хвилин до подій\n"
|
||||||
|
"• 🔄 Моніторинг змін графіку\n\n"
|
||||||
"Використовуйте кнопки нижче 👇",
|
"Використовуйте кнопки нижче 👇",
|
||||||
parse_mode="HTML",
|
parse_mode="HTML",
|
||||||
reply_markup=get_main_keyboard()
|
reply_markup=get_main_keyboard()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@dp.message(lambda message: message.text == "📊 Графік")
|
@dp.message(lambda msg: msg.text == "ℹ️ Про бота")
|
||||||
|
async def cmd_info(message: Message):
|
||||||
|
"""Показывает информацию о боте"""
|
||||||
|
if message.chat.id not in ALLOWED_CHAT_IDS:
|
||||||
|
return
|
||||||
|
|
||||||
|
await message.answer(
|
||||||
|
"<b>ℹ️ Про бота</b>\n\n"
|
||||||
|
"🚀 <b>Версія:</b> 2.1 (Оптимізована)\n"
|
||||||
|
"⚡ <b>Функціональність:</b>\n"
|
||||||
|
"├ Моніторинг графіку 24/7\n"
|
||||||
|
"├ Сповіщення за 5 хвилин\n"
|
||||||
|
"├ Детальна статистика дня\n"
|
||||||
|
"└ Красива візуалізація\n\n"
|
||||||
|
"🔐 <b>Безпека:</b> Використовуються .env файли\n"
|
||||||
|
"💾 <b>Джерело:</b> voe.com.ua",
|
||||||
|
parse_mode="HTML",
|
||||||
|
reply_markup=get_main_keyboard()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dp.message(Command("schedule"))
|
@dp.message(Command("schedule"))
|
||||||
async def cmd_schedule(message: Message):
|
async def cmd_schedule(message: Message):
|
||||||
"""Показывает текущий график (2 дня)"""
|
"""Показывает полный график"""
|
||||||
if message.chat.id not in ALLOWED_CHAT_IDS:
|
if message.chat.id not in ALLOWED_CHAT_IDS:
|
||||||
await message.answer("❌ У вас немає доступу до цього бота.")
|
await message.answer("❌ У вас немає доступу.")
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
await message.answer("⏳ Завантаження графіку...")
|
||||||
html = get_voe_html(VOE_CITY_ID, VOE_STREET_ID, VOE_HOUSE_ID)
|
html = get_voe_html(VOE_CITY_ID, VOE_STREET_ID, VOE_HOUSE_ID)
|
||||||
schedule = parse_html(html)
|
schedule = parse_html(html)
|
||||||
text = format_schedule_message(schedule, days_to_show=2)
|
text = format_schedule_message(schedule, days_to_show=2)
|
||||||
await message.answer(text, parse_mode="HTML", reply_markup=get_main_keyboard())
|
await message.answer(text, parse_mode="HTML", reply_markup=get_main_keyboard())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await message.answer(f"❌ Помилка: {str(e)}", reply_markup=get_main_keyboard())
|
await message.answer(
|
||||||
|
f"❌ <b>Помилка:</b> {str(e)}",
|
||||||
|
parse_mode="HTML",
|
||||||
|
reply_markup=get_main_keyboard()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dp.message(lambda message: message.text == "📅 Сьогодні")
|
@dp.message(Command("today"))
|
||||||
async def cmd_today(message: Message):
|
async def cmd_today(message: Message):
|
||||||
"""Показывает график на сегодня"""
|
"""Показывает график на сегодня"""
|
||||||
if message.chat.id not in ALLOWED_CHAT_IDS:
|
if message.chat.id not in ALLOWED_CHAT_IDS:
|
||||||
await message.answer("❌ У вас немає доступу до цього бота.")
|
await message.answer("❌ У вас немає доступу.")
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
await message.answer("⏳ Завантаження графіку сьогодні...")
|
||||||
html = get_voe_html(VOE_CITY_ID, VOE_STREET_ID, VOE_HOUSE_ID)
|
html = get_voe_html(VOE_CITY_ID, VOE_STREET_ID, VOE_HOUSE_ID)
|
||||||
schedule = parse_html(html)
|
schedule = parse_html(html)
|
||||||
text = format_single_day_schedule(schedule, 0)
|
text = format_single_day_schedule(schedule, 0)
|
||||||
await message.answer(text, parse_mode="HTML", reply_markup=get_main_keyboard())
|
await message.answer(text, parse_mode="HTML", reply_markup=get_main_keyboard())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await message.answer(f"❌ Помилка: {str(e)}", reply_markup=get_main_keyboard())
|
await message.answer(
|
||||||
|
f"❌ <b>Помилка:</b> {str(e)}",
|
||||||
|
parse_mode="HTML",
|
||||||
|
reply_markup=get_main_keyboard()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dp.message(lambda message: message.text == "📅 Завтра")
|
@dp.message(Command("tomorrow"))
|
||||||
async def cmd_tomorrow(message: Message):
|
async def cmd_tomorrow(message: Message):
|
||||||
"""Показывает график на завтра"""
|
"""Показывает график на завтра"""
|
||||||
if message.chat.id not in ALLOWED_CHAT_IDS:
|
if message.chat.id not in ALLOWED_CHAT_IDS:
|
||||||
await message.answer("❌ У вас немає доступу до цього бота.")
|
await message.answer("❌ У вас немає доступу.")
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
await message.answer("⏳ Завантаження графіку завтра...")
|
||||||
html = get_voe_html(VOE_CITY_ID, VOE_STREET_ID, VOE_HOUSE_ID)
|
html = get_voe_html(VOE_CITY_ID, VOE_STREET_ID, VOE_HOUSE_ID)
|
||||||
schedule = parse_html(html)
|
schedule = parse_html(html)
|
||||||
text = format_single_day_schedule(schedule, 1)
|
text = format_single_day_schedule(schedule, 1)
|
||||||
await message.answer(text, parse_mode="HTML", reply_markup=get_main_keyboard())
|
await message.answer(text, parse_mode="HTML", reply_markup=get_main_keyboard())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await message.answer(f"❌ Помилка: {str(e)}", reply_markup=get_main_keyboard())
|
await message.answer(
|
||||||
|
f"❌ <b>Помилка:</b> {str(e)}",
|
||||||
|
parse_mode="HTML",
|
||||||
|
reply_markup=get_main_keyboard()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dp.message(lambda message: message.text == "🔄 Перевірити")
|
|
||||||
@dp.message(Command("check"))
|
@dp.message(Command("check"))
|
||||||
async def cmd_check(message: Message):
|
async def cmd_check(message: Message):
|
||||||
"""Принудительная проверка графика"""
|
"""Принудительная проверка графика"""
|
||||||
if message.chat.id not in ALLOWED_CHAT_IDS:
|
if message.chat.id not in ALLOWED_CHAT_IDS:
|
||||||
await message.answer("❌ У вас немає доступу до цього бота.")
|
await message.answer("❌ У вас немає доступу.")
|
||||||
return
|
return
|
||||||
|
|
||||||
await message.answer("🔄 Перевіряю графік...", reply_markup=get_main_keyboard())
|
|
||||||
|
|
||||||
# Выполняем проверку и отправляем результат
|
|
||||||
try:
|
try:
|
||||||
|
await message.answer("🔄 <b>Перевіряю графік...</b>", parse_mode="HTML")
|
||||||
html = get_voe_html(VOE_CITY_ID, VOE_STREET_ID, VOE_HOUSE_ID)
|
html = get_voe_html(VOE_CITY_ID, VOE_STREET_ID, VOE_HOUSE_ID)
|
||||||
new_schedule = parse_html(html)
|
new_schedule = parse_html(html)
|
||||||
|
|
||||||
if schedules_differ(last_schedule, new_schedule):
|
prefix = "✅ <b>Знайдено зміни!</b>\n\n" if schedules_differ(last_schedule, new_schedule) else "✓ <b>Графік без змін</b>\n\n"
|
||||||
result_message = "✅ <b>Знайдено зміни в графіку!</b>\n\n"
|
result = prefix + format_schedule_message(new_schedule, days_to_show=2)
|
||||||
result_message += format_schedule_message(new_schedule, days_to_show=2)
|
|
||||||
await message.answer(result_message, parse_mode="HTML", reply_markup=get_main_keyboard())
|
await message.answer(result, parse_mode="HTML", reply_markup=get_main_keyboard())
|
||||||
else:
|
|
||||||
result_message = "✅ <b>Графік без змін</b>\n\n"
|
|
||||||
result_message += format_schedule_message(new_schedule, days_to_show=2)
|
|
||||||
await message.answer(result_message, parse_mode="HTML", reply_markup=get_main_keyboard())
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await message.answer(f"❌ Помилка при перевірці: {str(e)}", reply_markup=get_main_keyboard())
|
await message.answer(
|
||||||
|
f"❌ <b>Помилка:</b> {str(e)}",
|
||||||
|
parse_mode="HTML",
|
||||||
|
reply_markup=get_main_keyboard()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dp.message()
|
||||||
|
async def handle_text(message: Message):
|
||||||
|
"""Обработчик текстовых сообщений и кнопок"""
|
||||||
|
if message.chat.id not in ALLOWED_CHAT_IDS:
|
||||||
|
return
|
||||||
|
|
||||||
|
text = message.text
|
||||||
|
|
||||||
|
# Кнопка "Графік"
|
||||||
|
if text == "📊 Графік":
|
||||||
|
await cmd_schedule(message)
|
||||||
|
|
||||||
|
# Кнопка "Сьогодні"
|
||||||
|
elif text == "📅 Сьогодні":
|
||||||
|
await cmd_today(message)
|
||||||
|
|
||||||
|
# Кнопка "Завтра"
|
||||||
|
elif text == "📅 Завтра":
|
||||||
|
await cmd_tomorrow(message)
|
||||||
|
|
||||||
|
# Кнопка "Оновити"
|
||||||
|
elif text == "🔄 Оновити":
|
||||||
|
await cmd_check(message)
|
||||||
|
|
||||||
|
# Кнопка "Про бота"
|
||||||
|
elif text == "ℹ️ Про бота":
|
||||||
|
await cmd_info(message)
|
||||||
|
|
||||||
|
# Неизвестная команда
|
||||||
|
else:
|
||||||
|
await message.answer(
|
||||||
|
"❓ <b>Команда не розпізнана</b>\n\n"
|
||||||
|
"Використовуйте кнопки на клавіатурі або команди:\n"
|
||||||
|
"/start • /today • /tomorrow • /schedule • /check",
|
||||||
|
parse_mode="HTML",
|
||||||
|
reply_markup=get_main_keyboard()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# ГЛАВНАЯ ФУНКЦИЯ
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
"""Главная функция"""
|
"""Главная функция"""
|
||||||
logger.info("Запуск бота...")
|
logger.info("=" * 50)
|
||||||
|
logger.info("🚀 ЗАПУСК БОТА V2.1 (ОПТИМІЗОВАНА ВЕРСІЯ)")
|
||||||
|
logger.info("=" * 50)
|
||||||
|
|
||||||
# Запускаем мониторинг в фоне
|
if not TELEGRAM_TOKEN or TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN", "YOUR_TOKEN_HERE")
|
||||||
|
logger.error("❌ TELEGRAM_TOKEN не конфігурований! Напишіть токен в .env файл")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not ALLOWED_CHAT_IDS:
|
||||||
|
logger.error("❌ ALLOWED_CHAT_IDS не конфігуровані! Напишіть ID в .env файл")
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info(f"🔌 Дозволені чати: {ALLOWED_CHAT_IDS}")
|
||||||
|
logger.info(f"⏱ Інтервал перевірки: {CHECK_INTERVAL} сек")
|
||||||
|
logger.info("=" * 50)
|
||||||
|
|
||||||
|
# Запускаем мониторинг
|
||||||
monitoring_task = asyncio.create_task(monitoring_loop())
|
monitoring_task = asyncio.create_task(monitoring_loop())
|
||||||
|
|
||||||
# Запускаем бота
|
|
||||||
try:
|
try:
|
||||||
await dp.start_polling(bot)
|
await dp.start_polling(bot)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logger.info("⏹ Бот зупинений користувачем")
|
||||||
finally:
|
finally:
|
||||||
monitoring_task.cancel()
|
monitoring_task.cancel()
|
||||||
await bot.session.close()
|
await bot.session.close()
|
||||||
|
logger.info("✓ Підключення закрито")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
asyncio.run(main())
|
try:
|
||||||
|
asyncio.run(main())
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logger.info("⏹ Завершено")
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
bs4
|
requests>=2.31.0
|
||||||
requests
|
beautifulsoup4>=4.12.0
|
||||||
aiogram
|
aiogram>=3.3.0
|
||||||
|
python-dotenv>=1.0.0
|
||||||
|
aiohttp>=3.9.0
|
||||||
|
|||||||
Reference in New Issue
Block a user