diff --git a/main.py b/main.py
index 1102f0b..8c06d49 100644
--- a/main.py
+++ b/main.py
@@ -5,7 +5,8 @@ from bs4 import BeautifulSoup
from datetime import datetime, timedelta
from aiogram import Bot, Dispatcher, types
from aiogram.filters import Command
-from aiogram.types import Message
+from aiogram.types import Message, ReplyKeyboardMarkup, KeyboardButton
+from aiogram.utils.keyboard import ReplyKeyboardBuilder
import logging
# Настройки
@@ -127,10 +128,24 @@ def get_voe_html(city_id, street_id, house_id):
return insert_html
+def get_main_keyboard():
+ """Создает главную клавиатуру с кнопками"""
+ builder = ReplyKeyboardBuilder()
+ builder.row(
+ KeyboardButton(text="📊 Графік"),
+ KeyboardButton(text="🔄 Перевірити")
+ )
+ builder.row(
+ KeyboardButton(text="📅 Сьогодні"),
+ KeyboardButton(text="📅 Завтра")
+ )
+ return builder.as_markup(resize_keyboard=True)
+
+
def format_schedule_message(schedule, days_to_show=2):
- """Форматирует расписание для отправки в Telegram"""
- message = f"⚡️ Графік відключень світла\n"
- message += f"🕐 Оновлено: {datetime.now().strftime('%d.%m.%Y %H:%M')}\n\n"
+ """Форматирует расписание для отправки в Telegram (компактная версия)"""
+ message = f"⚡️ Графік відключень\n"
+ message += f"🕐 {datetime.now().strftime('%d.%m.%Y %H:%M')}\n\n"
start_date = datetime.now()
@@ -143,45 +158,135 @@ def format_schedule_message(schedule, days_to_show=2):
day_name = "Сьогодні" if day == 0 else "Завтра"
message += f"📅 {day_name} ({date_str})\n"
- message += "━━━━━━━━━━━━━━━━━━━━\n"
- for block in day_blocks:
+ # Группируем последовательные отключения
+ disconnections = []
+ i = 0
+ while i < len(day_blocks):
+ block = day_blocks[i]
first = block["first_half"]
second = block["second_half"]
- def get_icon(half_data):
- if half_data["status"] == "off":
- if half_data["confirmed"] is True:
- return "🔴" # Точно відключено
- elif half_data["confirmed"] is False:
- return "🟠" # Можливо відключено
- return "🔴"
- elif half_data["status"] == "on":
- return "🟢"
- return "⚪️"
+ # Проверяем статус отключения
+ first_off = first["status"] == "off"
+ second_off = second["status"] == "off"
- first_icon = get_icon(first)
- second_icon = get_icon(second)
-
- # Форматируем время
- hour_start = f"{block['hour']:02d}:00"
- hour_mid = f"{block['hour']:02d}:30"
- hour_end = f"{block['hour']+1:02d}:00"
-
- # Информация о черге
- queue_info = ""
- if first["queue"] or second["queue"]:
+ if first_off or second_off:
+ start_time = f"{block['hour']:02d}:00" if first_off else f"{block['hour']:02d}:30"
+
+ # Ищем конец периода отключения
+ j = i
+ while j < len(day_blocks):
+ b = day_blocks[j]
+ f = b["first_half"]
+ s = b["second_half"]
+
+ # Если обе половины не отключены, прерываем
+ if f["status"] != "off" and s["status"] != "off":
+ break
+
+ j += 1
+
+ # Определяем конечное время
+ if j > 0:
+ prev_block = day_blocks[j-1]
+ if prev_block["second_half"]["status"] == "off":
+ end_time = f"{prev_block['hour']+1:02d}:00"
+ else:
+ end_time = f"{prev_block['hour']:02d}:30"
+ else:
+ end_time = f"{block['hour']+1:02d}:00"
+
+ # Получаем информацию о черге и подтверждении
queue = first["queue"] or second["queue"]
- queue_info = f" | Черга {queue}"
-
- message += f"{hour_start}-{hour_mid} {first_icon} {hour_mid}-{hour_end} {second_icon}{queue_info}\n"
+ confirmed = first["confirmed"] if first_off else second["confirmed"]
+
+ icon = "🔴" if confirmed is True else "🟠" if confirmed is False else "🔴"
+
+ disconnections.append({
+ "time": f"{start_time}-{end_time}",
+ "icon": icon,
+ "queue": queue
+ })
+
+ i = j if j > i else i + 1
+ else:
+ i += 1
+
+ # Выводим сгруппированные отключения
+ if disconnections:
+ for d in disconnections:
+ queue_info = f" Ч{d['queue']}" if d['queue'] else ""
+ message += f"{d['icon']} {d['time']}{queue_info}\n"
+ else:
+ message += "🟢 Відключень немає\n"
message += "\n"
- message += "🟢 - Світло є\n"
- message += "🔴 - Відключення підтверджено\n"
- message += "🟠 - Можливе відключення\n"
- message += "⚪️ - Інформація відсутня"
+ message += "🔴 підтверджено • 🟠 можливо"
+
+ return message
+
+
+def format_single_day_schedule(schedule, day_num):
+ """Форматирует расписание для одного дня (еще более компактно)"""
+ start_date = datetime.now()
+ date_str = (start_date + timedelta(days=day_num)).strftime('%d.%m')
+ day_name = "Сьогодні" if day_num == 0 else "Завтра"
+
+ message = f"⚡️ {day_name} ({date_str})\n\n"
+
+ day_blocks = [b for b in schedule if b["day"] == day_num]
+ if not day_blocks:
+ return message + "❓ Немає даних"
+
+ # Создаем почасовой график одной строкой
+ hourly = []
+ for block in day_blocks:
+ first = block["first_half"]
+ second = block["second_half"]
+
+ def get_short_icon(half_data):
+ if half_data["status"] == "off":
+ return "●" if half_data["confirmed"] is True else "○"
+ elif half_data["status"] == "on":
+ return "·"
+ return "?"
+
+ hour_str = f"{block['hour']:02d}"
+ first_icon = get_short_icon(first)
+ second_icon = get_short_icon(second)
+ hourly.append(f"{hour_str}|{first_icon}{second_icon}")
+
+ # Выводим по 6 часов в строке
+ for i in range(0, len(hourly), 6):
+ chunk = hourly[i:i+6]
+ message += " ".join(chunk) + "\n"
+
+ message += "\n● підтв • ○ можл • · світло\n\n"
+
+ # Добавляем список отключений
+ disconnections = []
+ for block in day_blocks:
+ first = block["first_half"]
+ second = block["second_half"]
+
+ if first["status"] == "off":
+ icon = "🔴" if first["confirmed"] is True else "🟠"
+ time = f"{block['hour']:02d}:00-30"
+ queue = f" Ч{first['queue']}" if first['queue'] else ""
+ disconnections.append(f"{icon} {time}{queue}")
+
+ if second["status"] == "off":
+ icon = "🔴" if second["confirmed"] is True else "🟠"
+ time = f"{block['hour']:02d}:30-{block['hour']+1:02d}:00"
+ queue = f" Ч{second['queue']}" if second['queue'] else ""
+ disconnections.append(f"{icon} {time}{queue}")
+
+ if disconnections:
+ message += "Відключення:\n" + "\n".join(disconnections)
+ else:
+ message += "🟢 Відключень немає"
return message
@@ -245,6 +350,100 @@ async def check_schedule():
await send_to_all_users(f"❌ Помилка!\n\nНе вдалося отримати графік: {str(e)}")
+async def check_upcoming_disconnections():
+ """Проверяет предстоящие отключения и включения, отправляет предупреждения"""
+ global last_schedule
+
+ if last_schedule is None:
+ return
+
+ now = datetime.now()
+ current_time = now.time()
+
+ # Проверяем только текущий день
+ today_blocks = [b for b in last_schedule if b["day"] == 0]
+
+ for block in today_blocks:
+ hour = block["hour"]
+
+ # Проверяем первую половину часа (XX:00 - XX:30)
+ first_half_start = now.replace(hour=hour, minute=0, second=0, microsecond=0)
+ time_until_first = (first_half_start - now).total_seconds() / 60
+
+ # Если до события осталось 5 минут (с погрешностью ±1 минута)
+ if 4 <= time_until_first <= 6:
+ first = block["first_half"]
+
+ # ОТКЛЮЧЕНИЕ
+ if first["status"] == "off":
+ icon = "🔴" if first["confirmed"] is True else "🟠"
+ status_text = "підтверджено" if first["confirmed"] is True else "можливе"
+ queue_info = f" (Черга {first['queue']})" if first["queue"] else ""
+
+ warning = (
+ f"⚠️ УВАГА! ВІДКЛЮЧЕННЯ\n\n"
+ f"Через 5 хвилин відключення {icon}\n"
+ f"Час: {hour:02d}:00 - {hour:02d}:30\n"
+ f"Статус: {status_text}{queue_info}"
+ )
+ await send_to_all_users(warning)
+ logger.info(f"Отправлено предупреждение о отключении в {hour:02d}:00")
+
+ # ВКЛЮЧЕНИЕ
+ elif first["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"✅ УВАГА! ВКЛЮЧЕННЯ\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 = (
+ f"⚠️ УВАГА! ВІДКЛЮЧЕННЯ\n\n"
+ f"Через 5 хвилин відключення {icon}\n"
+ f"Час: {hour:02d}:30 - {hour+1:02d}:00\n"
+ 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:
+ warning = (
+ f"✅ УВАГА! ВКЛЮЧЕННЯ\n\n"
+ f"Через 5 хвилин світло має з'явитися 🟢\n"
+ f"Час: {hour:02d}:30"
+ )
+ await send_to_all_users(warning)
+ logger.info(f"Отправлено предупреждение о включении в {hour:02d}:30")
+
+
async def monitoring_loop():
"""Основной цикл мониторинга"""
# Первая проверка сразу при запуске
@@ -254,6 +453,7 @@ async def monitoring_loop():
while True:
await asyncio.sleep(CHECK_INTERVAL)
await check_schedule()
+ await check_upcoming_disconnections()
@dp.message(Command("start"))
@@ -265,17 +465,17 @@ async def cmd_start(message: Message):
await message.answer(
"👋 Вітаю!\n\n"
- "Я буду відстежувати зміни в графіку відключень світла.\n\n"
- "Доступні команди:\n"
- "/schedule - Показати поточний графік\n"
- "/check - Перевірити графік зараз",
- parse_mode="HTML"
+ "Я відстежую зміни в графіку відключень світла та попереджаю за 5 хвилин до відключення.\n\n"
+ "Використовуйте кнопки нижче 👇",
+ parse_mode="HTML",
+ reply_markup=get_main_keyboard()
)
+@dp.message(lambda message: message.text == "📊 Графік")
@dp.message(Command("schedule"))
async def cmd_schedule(message: Message):
- """Показывает текущий график"""
+ """Показывает текущий график (2 дня)"""
if message.chat.id not in ALLOWED_CHAT_IDS:
await message.answer("❌ У вас немає доступу до цього бота.")
return
@@ -284,11 +484,44 @@ async def cmd_schedule(message: Message):
html = get_voe_html(VOE_CITY_ID, VOE_STREET_ID, VOE_HOUSE_ID)
schedule = parse_html(html)
text = format_schedule_message(schedule, days_to_show=2)
- await message.answer(text, parse_mode="HTML")
+ await message.answer(text, parse_mode="HTML", reply_markup=get_main_keyboard())
except Exception as e:
- await message.answer(f"❌ Помилка: {str(e)}")
+ await message.answer(f"❌ Помилка: {str(e)}", reply_markup=get_main_keyboard())
+@dp.message(lambda message: message.text == "📅 Сьогодні")
+async def cmd_today(message: Message):
+ """Показывает график на сегодня"""
+ if message.chat.id not in ALLOWED_CHAT_IDS:
+ await message.answer("❌ У вас немає доступу до цього бота.")
+ return
+
+ try:
+ html = get_voe_html(VOE_CITY_ID, VOE_STREET_ID, VOE_HOUSE_ID)
+ schedule = parse_html(html)
+ text = format_single_day_schedule(schedule, 0)
+ await message.answer(text, parse_mode="HTML", reply_markup=get_main_keyboard())
+ except Exception as e:
+ await message.answer(f"❌ Помилка: {str(e)}", reply_markup=get_main_keyboard())
+
+
+@dp.message(lambda message: message.text == "📅 Завтра")
+async def cmd_tomorrow(message: Message):
+ """Показывает график на завтра"""
+ if message.chat.id not in ALLOWED_CHAT_IDS:
+ await message.answer("❌ У вас немає доступу до цього бота.")
+ return
+
+ try:
+ html = get_voe_html(VOE_CITY_ID, VOE_STREET_ID, VOE_HOUSE_ID)
+ schedule = parse_html(html)
+ text = format_single_day_schedule(schedule, 1)
+ await message.answer(text, parse_mode="HTML", reply_markup=get_main_keyboard())
+ except Exception as e:
+ await message.answer(f"❌ Помилка: {str(e)}", reply_markup=get_main_keyboard())
+
+
+@dp.message(lambda message: message.text == "🔄 Перевірити")
@dp.message(Command("check"))
async def cmd_check(message: Message):
"""Принудительная проверка графика"""
@@ -296,7 +529,7 @@ async def cmd_check(message: Message):
await message.answer("❌ У вас немає доступу до цього бота.")
return
- await message.answer("🔄 Перевіряю графік...")
+ await message.answer("🔄 Перевіряю графік...", reply_markup=get_main_keyboard())
await check_schedule()