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