2025年10月22日 星期三

Telegram + PythonTkinter

 Telegram + PythonTkinter 







  • GUI 介面:

    • 你會看到一個 Tkinter 視窗,顯示一個圓形 LED 燈(初始為紅色,off)。

    • 你可以輸入模擬的溫度和濕度值。

    • 點擊「輸出資料到 Telegram」按鈕,會將目前的 LED 狀態和溫濕度數據發送到你的 Telegram 聊天中。

  • Telegram Bot 交互:

    • /on: LED 變為綠色。

    • /off: LED 變為紅色。

    • /flash: LED 每 0.5 秒在綠色和紅色之間交替閃爍。

    • /Timer: LED 變為綠色,持續 10 秒。10 秒後,它會自動變為紅色(off),並在 Telegram 中收到通知。

    • /status: Bot 會回覆目前的 LED 狀態(on/off/flash/timer)以及 GUI 中輸入的溫度和濕度數據。

    • 初始化: 程式啟動後,會自動向你的聊天 ID 發送一條初始化狀態訊息。

    # ⚠️ 請替換成你的實際 Token 和 Chat ID
    #TELEGRAM_TOKEN = "YOUR_TELEGRAM_BOT_TOKEN"  
    #TARGET_CHAT_ID = "YOUR_CHAT_ID"


    import tkinter as tk
    from tkinter import messagebox
    import threading
    import time
    import asyncio
    from telegram import Update, Bot
    from telegram.ext import Application, CommandHandler, ContextTypes

    # --- 1. 配置與全域狀態 ---
    # ⚠️ 請替換成你的實際 Token 和 Chat ID
    #TELEGRAM_TOKEN = "YOUR_TELEGRAM_BOT_TOKEN"  
    #TARGET_CHAT_ID = "YOUR_CHAT_ID"
    TELEGRAM_TOKEN = "77138940254:AAHbrWu9ovb1BKPQy2WsbNSjNxfCGfCrEWU-o"  
    TARGET_CHAT_ID = "719265218469"



    # LED 狀態變數:'off', 'on', 'flash', 'timer'
    led_state = 'off'
    # Timer 持續時間 (秒)
    TIMER_DURATION = 10

    # 全域變數來保存 Tkinter App 的引用,用於 Bot 執行緒存取
    app_instance = None 

    # --- 2. Tkinter GUI 應用程式類別 ---

    class LEDControllerApp:
        def __init__(self, master, bot_application):
            self.master = master
            master.title("LED & 感測器控制器")

            self.bot_application = bot_application
            self.led_status_str = tk.StringVar(value="off")
            
            # 繪製 LED 的 Canvas
            self.canvas = tk.Canvas(master, width=100, height=100)
            self.canvas.pack(pady=10)
            # 初始狀態:紅色 (off)
            self.led_circle = self.canvas.create_oval(10, 10, 90, 90, fill="red", outline="black")

            # 狀態標籤
            tk.Label(master, text="LED 狀態:").pack()
            self.status_label = tk.Label(master, textvariable=self.led_status_str)
            self.status_label.pack()

            # 模擬溫度和濕度的輸入框
            tk.Label(master, text="溫度 (°C):").pack()
            self.temp_entry = tk.Entry(master)
            self.temp_entry.insert(0, "25.0")
            self.temp_entry.pack()

            tk.Label(master, text="濕度 (%):").pack()
            self.humidity_entry = tk.Entry(master)
            self.humidity_entry.insert(0, "60.0")
            self.humidity_entry.pack()

            # 輸出按鈕:將數據送給 Telegram
            self.send_button = tk.Button(master, text="輸出資料到 Telegram", command=self.send_status_to_telegram_from_gui)
            self.send_button.pack(pady=10)
            
            # Timer 和 Flash 相關狀態
            self.flash_toggle = False
            self.timer_end_time = None
            
            # 啟動 LED 顯示更新迴圈
            self.update_led_display()
            
            # 程式初始化後將狀態發送給 Telegram
            master.after(100, self.initial_status_send) 
            
        def initial_status_send(self):
            """程式初始化後發送狀態給 Telegram"""
            current_status_msg = self.get_current_status_message()
            # 使用 run_coroutine_threadsafe 將非同步任務提交給 Bot 執行緒的事件迴圈
            asyncio.run_coroutine_threadsafe(
                self.bot_application.bot.send_message(chat_id=TARGET_CHAT_ID, text=f"程式已啟動。\n目前狀態:{current_status_msg}"), 
                self.bot_application.loop
            )

        def get_current_status_message(self):
            """獲取目前 LED 和感測器數據的狀態資訊"""
            try:
                temp = self.temp_entry.get()
                humidity = self.humidity_entry.get()
                
                # 檢查 Timer 剩餘時間
                timer_info = ""
                if led_state == 'timer' and self.timer_end_time:
                    remaining = int(self.timer_end_time - time.time())
                    if remaining > 0:
                        timer_info = f" (剩餘 {remaining} 秒)"
                    else:
                        timer_info = f" (即將結束)"
                
                status_msg = (
                    f"LED 狀態: {led_state.upper()}{timer_info}\n"
                    f"溫度: {temp} °C\n"
                    f"濕度: {humidity} %"
                )
                return status_msg
            except Exception:
                return f"LED 狀態: {led_state.upper()}\n溫度/濕度資料讀取錯誤。"

        def send_status_to_telegram_from_gui(self):
            """從 GUI 按鈕觸發發送狀態"""
            status_msg = self.get_current_status_message()
            
            # 將發送操作排程到 Bot 執行緒的事件迴圈中
            asyncio.run_coroutine_threadsafe(
                self.bot_application.bot.send_message(chat_id=TARGET_CHAT_ID, text=f"手動報告:\n{status_msg}"), 
                self.bot_application.loop
            )
            messagebox.showinfo("發送成功", "狀態已發送到 Telegram。")

        def update_led_display(self):
            """根據 led_state 更新 LED 顯示和狀態標籤"""
            global led_state
            
            self.led_status_str.set(led_state)
            
            if led_state == 'on':
                # 綠色圓形 LED
                self.canvas.itemconfig(self.led_circle, fill="green")
                self.master.after(500, self.update_led_display) # 繼續循環
                
            elif led_state == 'off':
                # 紅色圓形 LED
                self.canvas.itemconfig(self.led_circle, fill="red")
                self.master.after(500, self.update_led_display) # 繼續循環
                
            elif led_state == 'flash':
                # 綠色 (0.5s) / 紅色 (0.5s) 交替閃爍
                if self.flash_toggle:
                    self.canvas.itemconfig(self.led_circle, fill="green")
                else:
                    self.canvas.itemconfig(self.led_circle, fill="red")
                self.flash_toggle = not self.flash_toggle
                self.master.after(500, self.update_led_display) # 每 0.5 秒更新一次
                
            elif led_state == 'timer':
                current_time = time.time()
                if self.timer_end_time and current_time < self.timer_end_time:
                    # 倒計時期間保持綠色
                    self.canvas.itemconfig(self.led_circle, fill="green")
                    self.master.after(500, self.update_led_display)
                else:
                    # 倒計時結束,切換到 off (紅色)
                    led_state = 'off'
                    self.canvas.itemconfig(self.led_circle, fill="red")
                    self.timer_end_time = None
                    
                    # 發送通知給 Telegram
                    asyncio.run_coroutine_threadsafe(
                        self.bot_application.bot.send_message(chat_id=TARGET_CHAT_ID, text="Timer 結束,LED 已切換到 OFF (紅色)。"), 
                        self.bot_application.loop
                    )
                    self.master.after(500, self.update_led_display)
                    
            else:
                 # 預設或未知狀態 (灰色)
                self.canvas.itemconfig(self.led_circle, fill="gray")
                self.master.after(500, self.update_led_display)

        def set_led_state_from_telegram(self, new_state):
            """供 Telegram 處理器調用的方法,安全地更新狀態"""
            global led_state
            led_state = new_state
            
            if new_state == 'timer':
                self.timer_end_time = time.time() + TIMER_DURATION
            
            # ⚠️ 確保在 Tkinter 主執行緒中更新顯示
            self.master.after_idle(self.update_led_display)
            
    # --- 3. Telegram Bot 處理函式 ---

    async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
        """處理 /start 命令"""
        await update.message.reply_text('歡迎使用 LED 控制 Bot。\n請使用 /on, /off, /flash, /Timer, 或 /status 命令。')

    async def on_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
        """處理 /on 命令:綠色"""
        if app_instance:
            app_instance.set_led_state_from_telegram('on')
            await update.message.reply_text('LED 已開啟 (綠色)。')

    async def off_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
        """處理 /off 命令:紅色"""
        if app_instance:
            app_instance.set_led_state_from_telegram('off')
            await update.message.reply_text('LED 已關閉 (紅色)。')

    async def flash_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
        """處理 /flash 命令:綠/紅交替"""
        if app_instance:
            app_instance.set_led_state_from_telegram('flash')
            await update.message.reply_text('LED 正在閃爍 (綠/紅交替,每 0.5 秒)。')

    async def timer_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
        """處理 /Timer 命令:10 秒綠色後轉紅色"""
        if app_instance:
            app_instance.set_led_state_from_telegram('timer')
            await update.message.reply_text(f'LED 已開啟 Timer,將持續 {TIMER_DURATION} 秒 (綠色),隨後轉為 OFF (紅色)。')

    async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
        """處理 /status 命令:回傳目前 LED 狀態和溫濕度"""
        if app_instance:
            status_msg = app_instance.get_current_status_message()
            await update.message.reply_text(f"目前狀態:\n{status_msg}")
        else:
            await update.message.reply_text("GUI 應用程式未完全初始化,無法獲取狀態。")

    def start_telegram_bot(application: Application):
        """在單獨的執行緒中啟動 Telegram Bot 輪詢"""
        # 建立一個新的事件迴圈並設定給此執行緒
        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)
        application.loop = loop  # 儲存 loop,以便在 Tkinter 執行緒中使用 run_coroutine_threadsafe
        
        # 添加命令處理器
        application.add_handler(CommandHandler("start", start_command))
        application.add_handler(CommandHandler("on", on_command))
        application.add_handler(CommandHandler("off", off_command))
        application.add_handler(CommandHandler("flash", flash_command))
        application.add_handler(CommandHandler("Timer", timer_command))
        application.add_handler(CommandHandler("status", status_command))

        # 啟動 Bot
        print("Telegram Bot 正在啟動...")
        # 使用 run_polling 進行阻塞式輪詢
        application.run_polling(poll_interval=1)
        print("Telegram Bot 停止。")


    # --- 4. 主程式入口 ---
    if __name__ == '__main__':
        # 檢查是否替換了 Token 和 Chat ID
        if TELEGRAM_TOKEN == "YOUR_TELEGRAM_BOT_TOKEN" or TARGET_CHAT_ID == "YOUR_CHAT_ID":
            print("⚠️ 錯誤:請務必在程式碼開頭替換你的 TELEGRAM_TOKEN 和 TARGET_CHAT_ID!")
            exit()
            
        # 初始化 Telegram Bot 應用
        bot_application = Application.builder().token(TELEGRAM_TOKEN).build()

        # 啟動 Telegram Bot 執行緒
        # 必須在單獨的執行緒中運行,以免阻塞 Tkinter 的 mainloop
        bot_thread = threading.Thread(target=start_telegram_bot, args=(bot_application,))
        bot_thread.daemon = True # 設定為守護執行緒,主程式退出時它也退出
        bot_thread.start()

        # 啟動 Tkinter 主程式
        root = tk.Tk()
        app_instance = LEDControllerApp(root, bot_application)
        
        # 運行 Tkinter 主迴圈 (阻塞式)
        root.mainloop()

        # Tkinter 退出後,停止 Telegram Bot
        print("Tkinter 視窗關閉。正在停止 Telegram Bot...")
        # 異步停止 Bot
        try:
            # 將停止操作提交給 Bot 執行緒的事件迴圈
            asyncio.run_coroutine_threadsafe(bot_application.updater.stop(), bot_application.loop).result(timeout=5)
        except Exception as e:
            print(f"停止 Bot 發生錯誤: {e}")
            
        bot_thread.join(timeout=5)
        print("程式退出。")

    沒有留言:

    張貼留言

    ESP32 (ESP-IDF in VS Code) MFRC522 + MQTT + PYTHON TKinter +SQLite

     ESP32 (ESP-IDF in VS Code) MFRC522 + MQTT + PYTHON TKinter +SQLite  ESP32 VS Code 程式 ; PlatformIO Project Configuration File ; ;   Build op...