2025年11月12日 星期三

大里 即時空氣品質資訊 推播到 line Message API

 大里 即時空氣品質資訊 推播到 line Message API


大里 即時空氣品質資訊

https://airtw.moenv.gov.tw/AirQuality_APIs/WebWidget.aspx?site=7

需修改

LINE_CHANNEL_ACCESS_TOKEN =

LINE_TARGET_ID =





✅【台中市 大里】即時空品報告
==========================
主要指標:
AQI: 55 (普通 (對少數敏感族群有輕微影響))
更新時間: 2025/11/13 13:00
--------------------------
📌 細懸浮微粒 (PM2.5):
小時濃度: 0 μg/m³
24小時平均: 1.7 μg/m³
--------------------------
📌 懸浮微粒 (PM10):
小時濃度: 9 μg/m³
小時平均: 11 μg/m³
--------------------------
📌 臭氧 (O3):
小時濃度: 18 ppb
8小時平均: 30 ppb
==========================



import tkinter as tk

from tkinter import ttk, messagebox

import requests

import threading

import json

import time


# ===============================================

# 全域設定區塊 (請修改此處)

# ===============================================


# --- 空氣品質測站資訊 ---

STATION_ID = "7"  # 網站代碼。目前設定為 7 (新北市土城)。

STATION_NAME = "台中市 大里" # 這是 GUI 顯示名稱,若要修改測站請同時修改 STATION_ID

AJAX_URL = "https://airtw.moenv.gov.tw/AirQuality_APIs/ajax_Widget.aspx"


# --- LINE Messaging API 設定 (請務必修改此處) ---

# *** 1. 請在此處填入您的 LINE Channel Access Token ***

LINE_CHANNEL_ACCESS_TOKEN = "P7R4jd35usv1YhlJa1C9HGXwcqr6G0Yknp2vDxOb356AnOGHt5MPpzXmJrxj5L9OY5Z70h1DSdKRGr2/6Q8cN0bVoh6PcUMISbfncKvnMmv2HG5GCR+HMgpPj2LQYqOLDKgDqUGchzrkgkrGf1KhnhfnugdB04t89/1O/w1cDnyilFU=" 

# *** 2. 請在此處填入接收訊息的 User ID 或 Group ID ***

LINE_TARGET_ID = "U601f091afaaf1d41de21ace452105bfd3cf" 

LINE_MESSAGE_API_URL = "https://api.line.me/v2/bot/message/push"


# 定義一個共用的 headers 模板

COMMON_HEADERS = {

    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',

    'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',

    'X-Requested-With': 'XMLHttpRequest',

    'Referer': f'https://airtw.moenv.gov.tw/AirQuality_APIs/WebWidget.aspx?site={STATION_ID}'

}


# 定義 AQI 等級對應的中文描述

AQI_LEVELS = [

    (0, 50, "良好 (一般人群可安心活動)"),

    (51, 100, "普通 (對少數敏感族群有輕微影響)"),

    (101, 150, "對敏感群族群不健康"),

    (151, 200, "對所有群族群不健康"),

    (201, 300, "非常不健康"),

    (301, 500, "危害")

]


# ===============================================

# 函式定義區塊

# ===============================================


def get_aqi_description(aqi_value):

    """根據 AQI 數值返回對應的中文健康影響描述。"""

    try:

        if not aqi_value or not str(aqi_value).isdigit():

             return "有效數據不足"

             

        aqi = int(aqi_value)

        for lower, upper, description in AQI_LEVELS:

            if lower <= aqi <= upper:

                return description

        if aqi > 500:

             return "危害" 

    except Exception:

        return "有效數據不足"

    return "有效數據不足"



def get_server_post_time():

    """發送請求獲取伺服器時間 (tw_post_time)。"""

    time_payload = {"Target": "system_time"}

    

    try:

        response = requests.post(AJAX_URL, data=time_payload, headers=COMMON_HEADERS, timeout=5)

        response.raise_for_status()

        

        raw_text = response.text.strip()

        clean_text = raw_text.encode('utf-8').decode('utf-8-sig').strip()

        

        if clean_text.startswith("error:"):

            return None

            

        time_data = json.loads(clean_text)

        return time_data.get('tw_post_time') 

        

    except Exception as e:

        print(f"獲取伺服器時間失敗: {e}")

        return None



def fetch_aqi_data_json():

    """使用伺服器時間,發送請求獲取空氣品質數據。"""

    post_time = get_server_post_time()

    if not post_time:

        return None

        

    data_payload = {

        "Target": "air_list",

        "SiteID": STATION_ID,

        "Datatime": post_time, 

        "Type": "home"

    }

    

    raw_text = ""

    try:

        response = requests.post(AJAX_URL, data=data_payload, headers=COMMON_HEADERS, timeout=10)

        response.raise_for_status() 

        

        raw_text = response.text.strip()

        clean_text = raw_text.encode('utf-8').decode('utf-8-sig').strip() 

        

        if clean_text.startswith("error:"):

             print(f"後端返回數據錯誤: {clean_text}")

             return None

             

        json_data = json.loads(clean_text)

        

        # 提取所有數據點

        data = {

            'Time': json_data[0].get('date', 'N/A'),

            'AQI': json_data[4].get('AQI', 'N/A'),

            'PM2.5_FIX': json_data[6].get('PM25_FIX', 'N/A'),

            'AVPM25': json_data[5].get('AVPM25', 'N/A'),

            'PM10_FIX': json_data[8].get('PM10_FIX', 'N/A'),

            'AVPM10': json_data[7].get('AVPM10', 'N/A'),

            'O3_FIX': json_data[10].get('O3_FIX', 'N/A'),

            'AVO3': json_data[9].get('AVO3', 'N/A')

        }

        

        return data

        

    except requests.exceptions.RequestException as e:

        print(f"數據請求網路錯誤: {e}")

        return None

    except json.JSONDecodeError as e:

        print(f"解析 JSON 響應失敗。錯誤詳情: {e}")

        print(f"嘗試解析的原始文本: {raw_text[:200]}...")

        return None

    except IndexError:

        print("JSON 數據結構不符合預期,無法提取 (索引錯誤)。")

        return None


def format_data_for_line(data):

    """將擷取到的數據格式化為 LINE 訊息所需的字串。"""

    if not data or data.get('AQI') == 'N/A':

        return f"\n【{STATION_NAME} 空氣品質】\n無法獲取最新的空氣品質數據。\n請檢查來源網站或網路連線。"

        

    aqi_value = data.get('AQI')

    description = get_aqi_description(aqi_value)

    

    # 判斷空氣品質等級,用於訊息開頭

    if aqi_value and str(aqi_value).isdigit() and int(aqi_value) >= 151:

        header = f"🚨🚨🚨 警報:空氣品質對所有群族群不健康 🚨🚨🚨"

    elif aqi_value and str(aqi_value).isdigit() and int(aqi_value) >= 101:

        header = f"⚠️ 注意:空氣品質對敏感群族群不健康 ⚠️"

    else:

        header = f"✅ 空氣品質尚可"

        

    message = (

        f"\n{header}\n"

        f"【{STATION_NAME} 即時空氣品質報告】\n"

        f"============================\n"

        f"📢 AQI 指數: {aqi_value}\n"

        f"健康影響: {description}\n"

        f"----------------------------\n"

        f"💧 PM2.5 (小時濃度): {data.get('PM2.5_FIX')} μg/m³\n"

        f"💧 PM2.5 (24H移動平均): {data.get('AVPM25')} μg/m³\n"

        f"💨 PM10 (小時濃度): {data.get('PM10_FIX')} μg/m³\n"

        f"💨 PM10 (小時移動平均): {data.get('AVPM10')} μg/m³\n"

        f"🔆 O3 (小時濃度): {data.get('O3_FIX')} ppb\n"

        f"🔆 O3 (8H移動平均): {data.get('AVO3')} ppb\n"

        f"============================\n"

        f"🕒 更新時間: {data.get('Time', 'N/A')}"

    )

    return message



def send_line_message_api(message):

    """

    發送訊息到 LINE Messaging API (Push Message)。

    使用 Channel Access Token 和 User/Group ID 發送 JSON 格式訊息。

    """

    if LINE_CHANNEL_ACCESS_TOKEN == "YOUR_LINE_CHANNEL_ACCESS_TOKEN_HERE":

        return False, "LINE Messaging API 錯誤:請先設定您的 Channel Access Token!"

    if LINE_TARGET_ID == "YOUR_USER_OR_GROUP_ID_HERE":

        return False, "LINE Messaging API 錯誤:請先設定接收訊息的 User ID 或 Group ID!"

        

    headers = {

        # Messaging API 使用 Bearer 授權

        "Authorization": f"Bearer {LINE_CHANNEL_ACCESS_TOKEN}",

        "Content-Type": "application/json"

    }

    

    # LINE Messaging API 採用 JSON Push 格式

    payload = {

        "to": LINE_TARGET_ID,

        "messages": [

            {

                "type": "text",

                "text": message

            }

        ]

    }

    

    try:

        response = requests.post(LINE_MESSAGE_API_URL, headers=headers, json=payload, timeout=10)

        response.raise_for_status()

        

        # Messaging API 成功時返回 status 200

        if response.status_code == 200:

            return True, "LINE 訊息發送成功!"

        else:

            # 嘗試解析錯誤訊息

            response_json = response.json()

            error_msg = response_json.get('message', '未知的 LINE API 錯誤')

            return False, f"LINE API 錯誤 ({response.status_code}): {error_msg}"


    except requests.exceptions.RequestException as e:

        return False, f"LINE 訊息網路連線錯誤: {e}"



def start_line_notification():

    """

    啟動 LINE 訊息發送流程 (必須先有數據)

    """

    # 檢查是否有數據

    if not hasattr(root, 'latest_data') or root.latest_data.get('AQI') == 'N/A':

        messagebox.showwarning("無法發送", "請先點擊「獲取最新數據」成功抓取資料後再發送 LINE 訊息。")

        return


    # 設置按鈕狀態

    line_button.config(state=tk.DISABLED)

    status_var.set("狀態:正在準備並發送 LINE 訊息...")


    def run_line_sender():

        try:

            message = format_data_for_line(root.latest_data)

            # 使用新的 Messaging API 函式

            success, result_msg = send_line_message_api(message) 

            

            # 更新狀態

            root.after(0, lambda: status_var.set(f"狀態:{result_msg}"))

            if not success:

                 root.after(0, lambda: messagebox.showerror("LINE 訊息失敗", result_msg))

        finally:

            # 恢復按鈕狀態

            root.after(0, lambda: line_button.config(state=tk.NORMAL))


    threading.Thread(target=run_line_sender).start()



def update_gui(data):

    """

    在 Tkinter 視窗中更新數據顯示,並儲存最新數據以供 LINE 使用。

    """

    # 儲存最新數據 (供 LINE 訊息使用)

    root.latest_data = data 

    

    if data and data.get('AQI') != 'N/A':

        aqi_value = data.get('AQI')

        description = get_aqi_description(aqi_value)

        

        # 更新 AQI 

        aqi_var.set(f"AQI 指數: {aqi_value} \n健康影響: {description}") 

        

        # 更新 PM2.5 數據

        pm25_fix_var.set(f"PM2.5 (小時濃度): {data.get('PM2.5_FIX')} μg/m³")

        avpm25_var.set(f"PM2.5 (24小時移動平均): {data.get('AVPM25')} μg/m³")

        

        # 更新 PM10 數據

        pm10_fix_var.set(f"PM10 (小時濃度): {data.get('PM10_FIX')} μg/m³")

        avpm10_var.set(f"PM10 (小時移動平均): {data.get('AVPM10')} μg/m³")

        

        # 更新 O3 數據

        o3_fix_var.set(f"O3 (小時濃度): {data.get('O3_FIX')} ppb")

        avo3_var.set(f"O3 (8小時移動平均): {data.get('AVO3')} ppb")

        

        time_var.set(f"更新時間: {data.get('Time', 'N/A')}")

        status_var.set("狀態:數據更新成功!")

    else:

        status_var.set("狀態:數據爬取失敗或無有效數據。")

        # 清空數據顯示

        aqi_var.set("AQI 指數: N/A \n健康影響: 有效數據不足")

        pm25_fix_var.set("PM2.5 (小時濃度): N/A")

        avpm25_var.set("PM2.5 (24小時移動平均): N/A")

        pm10_fix_var.set("PM10 (小時濃度): N/A")

        avpm10_var.set("PM10 (小時移動平均): N/A")

        o3_fix_var.set("O3 (小時濃度): N/A")

        avo3_var.set("O3 (8小時移動平均): N/A")

        time_var.set("更新時間: N/A")

        

        messagebox.showerror("錯誤", "無法獲取空氣品質數據。請檢查網路連線或站點 ID。")



def start_scraping():

    """

    啟動爬蟲並在一個獨立的線程中執行,避免 GUI 凍結。

    """

    status_var.set("狀態:正在獲取伺服器時間,準備請求數據...")

    # 禁用所有按鈕

    fetch_button.config(state=tk.DISABLED) 

    update_button.config(state=tk.DISABLED)

    line_button.config(state=tk.DISABLED)

    

    def run_scraper():

        data = fetch_aqi_data_json()

        root.after(0, lambda: update_gui(data))

        

        # 爬取完成後重新啟用按鈕

        root.after(0, lambda: fetch_button.config(state=tk.NORMAL))

        root.after(0, lambda: update_button.config(state=tk.NORMAL))

        # 只有在成功取得資料後才啟用 LINE 訊息按鈕

        if data and data.get('AQI') != 'N/A':

            root.after(0, lambda: line_button.config(state=tk.NORMAL))


    threading.Thread(target=run_scraper).start()


# ===============================================

# Tkinter GUI 設定區塊

# ===============================================

root = tk.Tk()

root.title(f"{STATION_NAME} 即時空氣品質資訊")

# 調整視窗高度以容納新增的 LINE 按鈕

root.geometry("550x750") 

root.resizable(False, False)


# 樣式設定

style = ttk.Style()

style.configure('TLabel', font=('Arial', 14))

style.configure('TButton', font=('Arial', 12))


# 數據變量

aqi_var = tk.StringVar(value="AQI 指數: N/A \n健康影響: 點擊獲取數據")

pm25_fix_var = tk.StringVar(value="PM2.5 (小時濃度): N/A")

avpm25_var = tk.StringVar(value="PM2.5 (24小時移動平均): N/A")

pm10_fix_var = tk.StringVar(value="PM10 (小時濃度): N/A")

avpm10_var = tk.StringVar(value="PM10 (小時移動平均): N/A")

o3_fix_var = tk.StringVar(value="O3 (小時濃度): N/A")

avo3_var = tk.StringVar(value="O3 (8小時移動平均): N/A")

time_var = tk.StringVar(value="更新時間: N/A")

status_var = tk.StringVar(value="狀態:等待操作")


# 初始化一個儲存最新數據的屬性,用於 LINE 發送

root.latest_data = {'AQI': 'N/A'}


# 標題

title_label = ttk.Label(root, text=f"{STATION_NAME} 即時空氣品質", font=('Arial', 18, 'bold'))

title_label.pack(pady=(20, 15))


# 數據顯示框架

data_frame = ttk.Frame(root, padding="20")

data_frame.pack(fill='x', padx=30)


# AQI

ttk.Label(data_frame, textvariable=aqi_var, justify=tk.LEFT).pack(anchor='w', pady=10)


# PM2.5

ttk.Separator(data_frame, orient='horizontal').pack(fill='x', pady=8)

ttk.Label(data_frame, text="--- PM2.5 ---", font=('Arial', 12, 'bold')).pack(anchor='w')

ttk.Label(data_frame, textvariable=pm25_fix_var).pack(anchor='w', pady=4)

ttk.Label(data_frame, textvariable=avpm25_var).pack(anchor='w', pady=4)


# PM10

ttk.Separator(data_frame, orient='horizontal').pack(fill='x', pady=8)

ttk.Label(data_frame, text="--- PM10 ---", font=('Arial', 12, 'bold')).pack(anchor='w')

ttk.Label(data_frame, textvariable=pm10_fix_var).pack(anchor='w', pady=4)

ttk.Label(data_frame, textvariable=avpm10_var).pack(anchor='w', pady=4)


# O3

ttk.Separator(data_frame, orient='horizontal').pack(fill='x', pady=8)

ttk.Label(data_frame, text="--- O3 ---", font=('Arial', 12, 'bold')).pack(anchor='w')

ttk.Label(data_frame, textvariable=o3_fix_var).pack(anchor='w', pady=4)

ttk.Label(data_frame, textvariable=avo3_var).pack(anchor='w', pady=4)


# 更新時間

ttk.Separator(data_frame, orient='horizontal').pack(fill='x', pady=8)

ttk.Label(data_frame, textvariable=time_var).pack(anchor='w', pady=15)



# --- 按鈕框架 1 (數據獲取) ---

fetch_button_frame = ttk.Frame(root)

fetch_button_frame.pack(fill='x', pady=10) 


fetch_button = ttk.Button(fetch_button_frame, text="獲取最新數據", command=start_scraping)

fetch_button.pack(side=tk.LEFT, padx=30, expand=True) 


update_button = ttk.Button(fetch_button_frame, text="更新數據", command=start_scraping)

update_button.pack(side=tk.RIGHT, padx=30, expand=True) 



# --- 按鈕框架 2 (LINE 訊息) ---

line_button_frame = ttk.Frame(root)

line_button_frame.pack(fill='x', pady=10)


# 初始禁用 LINE 訊息按鈕,必須先抓到數據才能發送

line_button = ttk.Button(line_button_frame, text="發送 LINE 訊息 (Messaging API)", command=start_line_notification, state=tk.DISABLED)

line_button.pack(padx=30, fill='x')



# 狀態欄 (放在最後,讓它緊貼底部)

status_label = ttk.Label(root, textvariable=status_var, font=('Arial', 10), foreground='blue')

status_label.pack(fill='x', pady=(0, 10)) 


root.mainloop()


沒有留言:

張貼留言

WOKWI 模擬 MFRC522 RFID Reader + 5個 Tag 發行到MQTT 上

WOKWI 模擬 MFRC522 RFID Reader + 5個 Tag  發行到MQTT 上 MQTTgo.io # MQTT 設定 MQTT_BROKER = "mqttgo.io" MQTT_PORT = 1883 MQTT_TOPIC = ...