File size: 6,934 Bytes
3aab70e
 
 
 
 
cc337b3
 
3aab70e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cc337b3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# cwa_service.py (Final Defensive Parsing Version)
import requests
import re
import pandas as pd
from datetime import datetime, timedelta, timezone
# ✨ CORRECTED IMPORT
from config.settings import CWA_API_KEY, CWA_ALARM_API, CWA_SIGNIFICANT_API

TAIPEI_TZ = timezone(timedelta(hours=8))

def _to_float(x):
    if x is None: return None
    s = str(x).strip()
    m = re.search(r"[-+]?\d+(?:\.\d+)?", s)
    return float(m.group()) if m else None

def _parse_cwa_time(s: str) -> tuple[str, str]:
    if not s: return ("未知", "未知")
    dt_utc = None
    try:
        dt_utc = datetime.fromisoformat(s.replace("Z", "+00:00"))
    except ValueError:
        try:
            dt_local = datetime.strptime(s, "%Y-%m-%d %H:%M:%S")
            dt_local = dt_local.replace(tzinfo=TAIPEI_TZ)
            dt_utc = dt_local.astimezone(timezone.utc)
        except Exception:
            return (s, "未知")
    if dt_utc:
        tw_str = dt_utc.astimezone(TAIPEI_TZ).strftime("%Y-%m-%d %H:%M")
        utc_str = dt_utc.astimezone(timezone.utc).strftime("%Y-%m-%d %H:%M")
        return (tw_str, utc_str)
    return (s, "未知")

def fetch_cwa_alarm_list(limit: int = 5) -> str:
    try:
        r = requests.get(CWA_ALARM_API, timeout=10)
        r.raise_for_status()
        payload = r.json()
    except Exception as e:
        return f"❌ 地震預警查詢失敗:{e}"
    items = payload.get("data", [])
    if not items: return "✅ 目前沒有地震預警。"
    def _key(it):
        try: return datetime.fromisoformat(it.get("originTime", "").replace("Z", "+00:00"))
        except: return datetime.min.replace(tzinfo=timezone.utc)
    items = sorted(items, key=_key, reverse=True)
    lines = ["🚨 地震預警(最新):", "-" * 20]
    for it in items[:limit]:
        mag = _to_float(it.get("magnitudeValue"))
        depth = _to_float(it.get("depth"))
        tw_str, _ = _parse_cwa_time(it.get("originTime", ""))
        identifier = str(it.get('identifier', '—')).replace('{', '{{').replace('}', '}}')
        msg_type = str(it.get('msgType', '—')).replace('{', '{{').replace('}', '}}')
        msg_no = str(it.get('msgNo', '—')).replace('{', '{{').replace('}', '}}')
        location_desc_list = it.get('locationDesc')
        areas_str = ", ".join(str(area) for area in location_desc_list) if isinstance(location_desc_list, list) and location_desc_list else "—"
        areas = areas_str.replace('{', '{{').replace('}', '}}')
        mag_str = f"{mag:.1f}" if mag is not None else "—"
        depth_str = f"{depth:.0f}" if depth is not None else "—"
        lines.append(
            f"事件: {identifier} | 類型: {msg_type}#{msg_no}\n"
            f"規模/深度: M{mag_str} / {depth_str} km\n"
            f"時間: {tw_str}(台灣)\n"
            f"地點: {areas}"
        )
    return "\n\n".join(lines).strip()

def _parse_significant_earthquakes(obj: dict) -> pd.DataFrame:
    records = obj.get("records", {})
    quakes = records.get("Earthquake", [])
    rows = []
    for q in quakes:
        # [偵錯] 如果需要,可以取消下面這行的註解,它會在 Log 中印出最原始的資料
        # print(f"原始地震資料: {q}") 
        
        ei = q.get("EarthquakeInfo", {})
        
        # [修正] 使用更穩健的方式取得所有資料,檢查所有已知的大小寫和備用名稱
        epic = ei.get("Epicenter") or ei.get("epicenter") or {}
        mag_info = ei.get("Magnitude") or ei.get("magnitude") or ei.get("EarthquakeMagnitude") or {}
        depth_raw = ei.get("FocalDepth") or ei.get("depth") or ei.get("Depth")
        mag_raw = mag_info.get("MagnitudeValue") or mag_info.get("magnitudeValue") or mag_info.get("Value") or mag_info.get("value")
        
        rows.append({
            "ID": q.get("EarthquakeNo"), "Time": ei.get("OriginTime"),
            "Lat": _to_float(epic.get("EpicenterLatitude") or epic.get("epicenterLatitude")),
            "Lon": _to_float(epic.get("EpicenterLongitude") or epic.get("epicenterLongitude")),
            "Depth": _to_float(depth_raw), 
            "Magnitude": _to_float(mag_raw),
            "Location": epic.get("Location") or epic.get("location"), 
            "URL": q.get("Web") or q.get("ReportURL"),
        })
        
    df = pd.DataFrame(rows)
    if not df.empty and "Time" in df.columns:
        # df["Time"] = pd.to_datetime(df["Time"], errors="coerce", utc=True).dt.tz_convert(TAIPEI_TZ)
        # 假設 TAIPEI_TZ 已在檔案開頭定義
        df["Time"] = pd.to_datetime(df["Time"], errors="coerce").dt.tz_localize(TAIPEI_TZ)
    return df

def fetch_significant_earthquakes(days: int = 7, limit: int = 5) -> str:
    if not CWA_API_KEY: return "❌ 顯著地震查詢失敗:管理者尚未設定 CWA_API_KEY。"
    now = datetime.now(timezone.utc)
    time_from = (now - timedelta(days=days)).strftime("%Y-%m-%d")
    params = {"Authorization": CWA_API_KEY, "format": "JSON", "timeFrom": time_from}
    try:
        r = requests.get(CWA_SIGNIFICANT_API, params=params, timeout=15)
        r.raise_for_status()
        data = r.json()
        df = _parse_significant_earthquakes(data)
        if df.empty: return f"✅ 過去 {days} 天內沒有顯著有感地震報告。"
        df = df.sort_values(by="Time", ascending=False).head(limit)
        lines = [f"🚨 CWA 最新顯著有感地震 (近{days}天内):", "-" * 20]
        for _, row in df.iterrows():
            mag_str = f"{row['Magnitude']:.1f}" if pd.notna(row['Magnitude']) else "—"
            depth_str = f"{row['Depth']:.0f}" if pd.notna(row['Depth']) else "—"
            lines.append(
                f"時間: {row['Time'].strftime('%Y-%m-%d %H:%M') if pd.notna(row['Time']) else '—'}\n"
                f"地點: {row['Location'] or '—'}\n"
                f"規模: M{mag_str} | 深度: {depth_str} km\n"
                f"報告: {row['URL'] or '無'}"
            )
        return "\n\n".join(lines)
    except Exception as e:
        return f"❌ 顯著地震查詢失敗:{e}"

def fetch_latest_significant_earthquake() -> dict | None:
    try:
        if not CWA_API_KEY: raise ValueError("錯誤:尚未設定 CWA_API_KEY Secret。")
        params = {"Authorization": CWA_API_KEY, "format": "JSON", "limit": 1, "orderby": "OriginTime desc"}
        r = requests.get(CWA_SIGNIFICANT_API, params=params, timeout=15)
        r.raise_for_status()
        data = r.json()
        df = _parse_significant_earthquakes(data)
        if df.empty: return None

        latest_eq_data = df.iloc[0].to_dict()
        
        quakes = data.get("records", {}).get("Earthquake", [])
        if quakes:
            latest_eq_data["ImageURL"] = quakes[0].get("ReportImageURI")

        if pd.notna(latest_eq_data.get("Time")):
            latest_eq_data["TimeStr"] = latest_eq_data["Time"].strftime('%Y-%m-%d %H:%M')

        return latest_eq_data
    except Exception as e:
        raise e