Add support for timezone handling and improve terminal data fetching

- Added `pytz` dependency to `pyproject.toml` and `uv.lock`.
- Introduced Taipei timezone handling in `tg_bot.py`.
- Enhanced terminal data fetching to include fallback mechanisms and display for the next 12 hours.
This commit is contained in:
TzuWei 2026-02-28 20:53:51 +09:00
parent 7d90cf0564
commit 1686b40a42
Signed by: tzuwei-huang
GPG Key ID: 88A0D3DA7558EE01
3 changed files with 110 additions and 37 deletions

View File

@ -3,6 +3,7 @@ import sys
import datetime
import json
import logging
import pytz
from dotenv import load_dotenv
from telegram import Update, BotCommand
@ -19,6 +20,8 @@ logging.basicConfig(
level=logging.INFO
)
TAIPEI_TZ = pytz.timezone('Asia/Taipei')
class TelegramBot:
def __init__(self, token: str):
self.token = token
@ -32,7 +35,7 @@ class TelegramBot:
BotCommand("help", "顯示說明文字"),
]
await application.bot.set_my_commands(commands)
print("Bot commands registered.")
logging.info("Bot commands registered.")
async def help_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Display help message."""
@ -40,57 +43,115 @@ class TelegramBot:
"<b>桃園機場航班人次預報 Bot</b>\n\n"
"/t1 - 獲取第一航廈最新出境與過境預報\n"
"/t2 - 獲取第二航廈最新出境與過境預報\n"
"/help - 顯示此說明"
"/help - 顯示此說明\n\n"
"資料顯示為台北時間,從現在開始往後顯示最多 12 小時。"
)
await update.message.reply_text(help_text, parse_mode=ParseMode.HTML)
async def get_terminal_data(self, update: Update, terminal_key: str):
"""Generic method to fetch and display terminal data."""
date_str = datetime.datetime.now().strftime("%Y_%m_%d")
def _get_file_for_date(self, target_date: datetime.date):
"""Helper to try getting a file for a specific date (with fallback)."""
date_str = target_date.strftime("%Y_%m_%d")
# Try _update first
filename = f"{date_str}_update.json"
url = f"https://www.taoyuan-airport.com/uploads/fos/{date_str}_update.xls"
""
# Download and store
file_path = self.downloader.download_and_store_as_json(url, filename, verify=False)
if not file_path or not os.path.exists(file_path):
await update.message.reply_text("抱歉,無法獲取目前的資料。")
return
with open(file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
# Fallback to base
if not file_path:
filename = f"{date_str}.json"
url = f"https://www.taoyuan-airport.com/uploads/fos/{date_str}.xls"
file_path = self.downloader.download_and_store_as_json(url, filename, verify=False)
table_data = data.get('data', {}).get(terminal_key, {})
records = table_data.get('records', [])
title = table_data.get('title', f"{terminal_key.upper()} 預報表")
return file_path
if not records:
await update.message.reply_text(f"找不到 {title} 的資料。")
async def get_terminal_data(self, update: Update, terminal_key: str):
"""Fetch and display terminal data for the next 12 hours (Taipei time)."""
now_taipei = datetime.datetime.now(TAIPEI_TZ)
# Dates to check: today and tomorrow
today = now_taipei.date()
tomorrow = (now_taipei + datetime.timedelta(days=1)).date()
all_records = []
titles = []
# Track which dates we successfully got data for
available_dates = []
for d in [today, tomorrow]:
file_path = self._get_file_for_date(d)
if file_path and os.path.exists(file_path):
try:
with open(file_path, 'r', encoding='utf-8') as f:
day_data = json.load(f)
term_data = day_data.get('data', {}).get(terminal_key, {})
day_records = term_data.get('records', [])
if day_records:
available_dates.append(d)
for r in day_records:
r['_date'] = d
all_records.append(r)
if term_data.get('title'):
titles.append(term_data['title'])
except Exception as e:
logging.error(f"Error reading JSON for {d}: {e}")
if not all_records:
await update.message.reply_text("抱歉,目前無法從桃園機場官網獲取資料。")
return
# Format as a simple table
message = f"<b>{title}</b>\n"
# Filtering logic:
final_list = []
for i in range(12):
target_dt = now_taipei + datetime.timedelta(hours=i)
target_hour = target_dt.hour
target_date = target_dt.date()
# Find the record for this target_date and hour
for r in all_records:
if r['_date'] == target_date:
time_range = r.get('時間區間', '')
# Format is "HH:00 ~ HH:59"
try:
record_hour = int(time_range.split(':')[0])
if record_hour == target_hour:
final_list.append(r)
break
except (ValueError, IndexError):
continue
if not final_list:
await update.message.reply_text("找不到當前時段往後的預報資料。")
return
# Format output
display_title = titles[0] if titles else f"{terminal_key.upper()} 預報表"
message = f"<b>{display_title}</b>\n"
message += "<pre>"
message += f"{'時間':<15} {'出境':<6} {'過境':<6}\n"
message += "-" * 30 + "\n"
now_hour = datetime.datetime.now().hour
count = 0
for r in records:
time_str = r.get('時間區間', '')
try:
hour = int(time_str.split(':')[0])
if hour >= now_hour:
out_count = r.get('出境桃園', 0)
transfer_count = r.get('到站轉機', 0)
message += f"{time_str:<15} {out_count:<6} {transfer_count:<6}\n"
count += 1
if count >= 10: break
except:
continue
for r in final_list:
rec_date = r['_date']
time_range = r.get('時間區間', '')
# 取出起始時間 (例如 "22:00")
start_time = time_range.split(' ~ ')[0] if ' ~ ' in time_range else time_range
display_time = f"{rec_date.strftime('%m/%d')} {start_time}"
out_count = r.get('出境桃園', 0)
transfer_count = r.get('到站轉機', 0)
message += f"{display_time:<15} {out_count:<6} {transfer_count:<6}\n"
message += "</pre>"
# Info about missing data if we couldn't get the full 12 hours
if len(final_list) < 12:
if tomorrow not in available_dates:
message += f"\n<i>註:機場尚未發佈明日 ({tomorrow}) 的預報檔案。</i>"
await update.message.reply_text(message, parse_mode=ParseMode.HTML)
async def get_t1_data(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
@ -106,11 +167,11 @@ class TelegramBot:
application.add_handler(CommandHandler('t2', self.get_t2_data))
application.add_handler(CommandHandler('help', self.help_command))
print("Bot is running... Press Ctrl+C to stop.")
print("Bot is running (Taipei Time Locked)... Press Ctrl+C to stop.")
application.run_polling()
if __name__ == "__main__":
# Get token from environment variable for security
# Get token from environment variable or .env
load_dotenv()
TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
if not TOKEN:

View File

@ -9,5 +9,6 @@ dependencies = [
"pandas>=3.0.1",
"python-dotenv>=1.2.1",
"python-telegram-bot>=22.6",
"pytz>=2025.2",
"xlrd>=2.0.2",
]

11
uv.lock generated
View File

@ -222,6 +222,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/13/97/7298f0e1afe3a1ae52ff4c5af5087ed4de319ea73eb3b5c8c4dd4e76e708/python_telegram_bot-22.6-py3-none-any.whl", hash = "sha256:e598fe171c3dde2dfd0f001619ee9110eece66761a677b34719fb18934935ce0", size = 737267, upload-time = "2026-01-24T13:56:58.06Z" },
]
[[package]]
name = "pytz"
version = "2025.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
]
[[package]]
name = "rctp-pax-fcst"
version = "0.1.0"
@ -232,6 +241,7 @@ dependencies = [
{ name = "pandas" },
{ name = "python-dotenv" },
{ name = "python-telegram-bot" },
{ name = "pytz" },
{ name = "xlrd" },
]
@ -242,6 +252,7 @@ requires-dist = [
{ name = "pandas", specifier = ">=3.0.1" },
{ name = "python-dotenv", specifier = ">=1.2.1" },
{ name = "python-telegram-bot", specifier = ">=22.6" },
{ name = "pytz", specifier = ">=2025.2" },
{ name = "xlrd", specifier = ">=2.0.2" },
]