Add main keyboard and enhance schedule formatting in DTEK notification bot

FOR HISTORY
This commit is contained in:
2025-11-12 18:01:42 +01:00
parent ead3ab2a76
commit c820aac02c
+276 -43
View File
@@ -5,7 +5,8 @@ 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 from aiogram.types import Message, ReplyKeyboardMarkup, KeyboardButton
from aiogram.utils.keyboard import ReplyKeyboardBuilder
import logging import logging
# Настройки # Настройки
@@ -127,10 +128,24 @@ def get_voe_html(city_id, street_id, house_id):
return insert_html 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): def format_schedule_message(schedule, days_to_show=2):
"""Форматирует расписание для отправки в Telegram""" """Форматирует расписание для отправки в Telegram (компактная версия)"""
message = f"⚡️ <b>Графік відключень світла</b>\n" message = f"⚡️ <b>Графік відключень</b>\n"
message += f"🕐 Оновлено: {datetime.now().strftime('%d.%m.%Y %H:%M')}\n\n" message += f"🕐 {datetime.now().strftime('%d.%m.%Y %H:%M')}\n\n"
start_date = datetime.now() start_date = datetime.now()
@@ -143,45 +158,135 @@ def format_schedule_message(schedule, days_to_show=2):
day_name = "Сьогодні" if day == 0 else "Завтра" day_name = "Сьогодні" if day == 0 else "Завтра"
message += f"📅 <b>{day_name} ({date_str})</b>\n" message += f"📅 <b>{day_name} ({date_str})</b>\n"
message += "━━━━━━━━━━━━━━━━━━━━\n"
for block in day_blocks: # Группируем последовательные отключения
disconnections = []
i = 0
while i < len(day_blocks):
block = day_blocks[i]
first = block["first_half"] first = block["first_half"]
second = block["second_half"] second = block["second_half"]
def get_icon(half_data): # Проверяем статус отключения
if half_data["status"] == "off": first_off = first["status"] == "off"
if half_data["confirmed"] is True: second_off = second["status"] == "off"
return "🔴" # Точно відключено
elif half_data["confirmed"] is False:
return "🟠" # Можливо відключено
return "🔴"
elif half_data["status"] == "on":
return "🟢"
return "⚪️"
first_icon = get_icon(first) if first_off or second_off:
second_icon = get_icon(second) start_time = f"{block['hour']:02d}:00" if first_off else f"{block['hour']:02d}:30"
# Форматируем время # Ищем конец периода отключения
hour_start = f"{block['hour']:02d}:00" j = i
hour_mid = f"{block['hour']:02d}:30" while j < len(day_blocks):
hour_end = f"{block['hour']+1:02d}:00" b = day_blocks[j]
f = b["first_half"]
# Информация о черге s = b["second_half"]
queue_info = ""
if first["queue"] or second["queue"]: # Если обе половины не отключены, прерываем
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 = first["queue"] or second["queue"]
queue_info = f" | Черга {queue}" confirmed = first["confirmed"] if first_off else second["confirmed"]
message += f"{hour_start}-{hour_mid} {first_icon} {hour_mid}-{hour_end} {second_icon}{queue_info}\n" 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 += "<i>🔴 підтверджено • 🟠 можливо</i>"
message += "🔴 - Відключення підтверджено\n"
message += "🟠 - Можливе відключення\n" return message
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"⚡️ <b>{day_name} ({date_str})</b>\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<i>● підтв • ○ можл • · світло</i>\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 += "<b>Відключення:</b>\n" + "\n".join(disconnections)
else:
message += "🟢 <b>Відключень немає</b>"
return message return message
@@ -245,6 +350,100 @@ async def check_schedule():
await send_to_all_users(f"❌ <b>Помилка!</b>\n\nНе вдалося отримати графік: {str(e)}") await send_to_all_users(f"❌ <b>Помилка!</b>\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"⚠️ <b>УВАГА! ВІДКЛЮЧЕННЯ</b>\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"✅ <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 = (
f"⚠️ <b>УВАГА! ВІДКЛЮЧЕННЯ</b>\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"✅ <b>УВАГА! ВКЛЮЧЕННЯ</b>\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(): async def monitoring_loop():
"""Основной цикл мониторинга""" """Основной цикл мониторинга"""
# Первая проверка сразу при запуске # Первая проверка сразу при запуске
@@ -254,6 +453,7 @@ async def monitoring_loop():
while True: while True:
await asyncio.sleep(CHECK_INTERVAL) await asyncio.sleep(CHECK_INTERVAL)
await check_schedule() await check_schedule()
await check_upcoming_disconnections()
@dp.message(Command("start")) @dp.message(Command("start"))
@@ -265,17 +465,17 @@ async def cmd_start(message: Message):
await message.answer( await message.answer(
"👋 <b>Вітаю!</b>\n\n" "👋 <b>Вітаю!</b>\n\n"
"Я буду відстежувати зміни в графіку відключень світла.\n\n" "Я відстежую зміни в графіку відключень світла та попереджаю за 5 хвилин до відключення.\n\n"
"Доступні команди:\n" "Використовуйте кнопки нижче 👇",
"/schedule - Показати поточний графік\n" parse_mode="HTML",
"/check - Перевірити графік зараз", reply_markup=get_main_keyboard()
parse_mode="HTML"
) )
@dp.message(lambda message: message.text == "📊 Графік")
@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
@@ -284,11 +484,44 @@ async def cmd_schedule(message: Message):
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") 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)}") 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")) @dp.message(Command("check"))
async def cmd_check(message: Message): async def cmd_check(message: Message):
"""Принудительная проверка графика""" """Принудительная проверка графика"""
@@ -296,7 +529,7 @@ async def cmd_check(message: Message):
await message.answer("❌ У вас немає доступу до цього бота.") await message.answer("❌ У вас немає доступу до цього бота.")
return return
await message.answer("🔄 Перевіряю графік...") await message.answer("🔄 Перевіряю графік...", reply_markup=get_main_keyboard())
await check_schedule() await check_schedule()