# --- routers/diagnostics.py --- import re import asyncssh from logger import get_logger from fastapi import APIRouter, HTTPException, Query from typing import Dict, Any logger = get_logger("app.diagnostics") router = APIRouter(prefix="/cm-diagnostics", tags=["Diagnostics"]) @router.get("/") async def get_cm_diagnostics( host: str = Query(...), username: str = Query(...), password: str = Query(...), mac: str = Query(..., description="Cable Modem MAC Address") ) -> Dict[str, Any]: mac = mac.strip().lower() if not mac: raise HTTPException(status_code=400, detail="MAC Address is required") result_data = { "mac": mac, "ip": "N/A", "state": "N/A", "cpe_count": 0, "phy": { "tx_power": None, "rx_power": None }, "upstreams": [], "mer_channels": {} } try: async with asyncssh.connect(host, username=username, password=password, known_hosts=None) as conn: logger.debug(f"🚀 [Diagnostics] 開始診斷 MAC: {mac}") # ========================================== # 1. 查詢基本狀態 # ========================================== cmd_base = "show cable modem " + mac + " | nomore" res_base = await conn.run(cmd_base, check=False) if res_base.exit_status == 0 and res_base.stdout: for line in res_base.stdout.splitlines(): if mac in line.lower(): tokens = line.split() for t in tokens: if re.match(r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$", t): result_data["ip"] = t elif re.match(r"^[a-z]-online|online|offline|init.*|reject", t, re.IGNORECASE): result_data["state"] = t nums = [t for t in tokens if t.isdigit()] if nums: result_data["cpe_count"] = int(nums[-1]) break # ========================================== # 2. 查詢 PHY 狀態 (💡 修復:移除寫死的 us/oad 判斷,改用通用特徵) # ========================================== cmd_phy = "show cable modem " + mac + " phy | nomore" res_phy = await conn.run(cmd_phy, check=False) if res_phy.exit_status == 0 and res_phy.stdout: tx_list = [] rx_list = [] for line in res_phy.stdout.splitlines(): line_lower = line.lower() if mac in line_lower: tokens = line.split() if len(tokens) >= 6: ch_name = tokens[1] # 💡 只要名稱包含 ":" 和 "/",就認定是合法的通道 (例如 Oa32, Oad32, Us32) if ":" in ch_name and "/" in ch_name: try: snr_val = float(tokens[4]) result_data["upstreams"].append({"channel": ch_name, "snr": snr_val}) except: pass try: tx_val = float(tokens[3].split('/')[0]) rx_list_val = float(tokens[5].split('/')[0]) tx_list.append(tx_val) rx_list.append(rx_list_val) except: pass if tx_list: result_data["phy"]["tx_power"] = round(sum(tx_list)/len(tx_list), 2) if rx_list: result_data["phy"]["rx_power"] = round(sum(rx_list)/len(rx_list), 2) # ========================================== # 3. 尋找所有 OFDM 通道 # ========================================== cmd_help = "show cable modem " + mac + " ofdm-channel ?" res_help = await conn.run(cmd_help, check=False) text_to_search = (res_help.stdout or "") + "\n" + (res_help.stderr or "") cmd_verb = "show cable modem " + mac + " verbose | nomore" res_verb = await conn.run(cmd_verb, check=False) text_to_search += "\n" + (res_verb.stdout or "") matches = re.findall(r"\b(Of(?:dm)?\d*[\d/:]+)\b", text_to_search, re.IGNORECASE) ofdm_channels = [] for m in matches: clean_m = m.lower() if clean_m.startswith("of"): clean_m = "Of" + clean_m[2:] if clean_m not in ofdm_channels: ofdm_channels.append(clean_m) ofdm_channels.sort() # ========================================== # 4. 對每一個 OFDM 通道查詢 MER # ========================================== for ch in ofdm_channels: cmd_mer = "show cable modem " + mac + " ofdm-channel " + ch + " mer | nomore" res_mer = await conn.run(cmd_mer, check=False) histogram = {} min_mer = 999 if res_mer.exit_status == 0 and res_mer.stdout: mer_lines = re.finditer(r"(\d+)\s*(?:db|dB)?\s*\|[^|]*\|?\s*(\**)", res_mer.stdout, re.IGNORECASE) for match in mer_lines: db_val = match.group(1) stars = match.group(2) if stars: histogram[db_val] = len(stars) * 100 db_int = int(db_val) if db_int < min_mer: min_mer = db_int health = "unknown" if histogram and min_mer != 999: if min_mer >= 41: health = "good" elif min_mer >= 38: health = "warning" else: health = "critical" result_data["mer_channels"][ch] = { "histogram": histogram, "health": health, "min_mer": min_mer if min_mer != 999 else "N/A" } logger.debug(f"✅ [Diagnostics] 解析結果: {result_data}") return {"status": "success", "data": result_data} except Exception as e: logger.error("CM Diagnostics Error: " + str(e)) raise HTTPException(status_code=500, detail="設備連線或查詢失敗: " + str(e))