2025年10月20日 星期一

台灣地區主要水庫蓄水量報告表 (Node-Red)

 台灣地區主要水庫蓄水量報告表 (Node-Red)

https://fhy.wra.gov.tw/ReservoirPage_2011/StorageCapacity.aspx








[{"id":"357295f693e9312c","type":"inject","z":"fc9e6d27522970cd","name":"手動/定時觸發 (每小時)","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"3600","crontab":"","once":true,"onceDelay":"0.1","topic":"","payload":"","payloadType":"date","x":140,"y":100,"wires":[["18457e53ea60c02c"]]},{"id":"18457e53ea60c02c","type":"http request","z":"fc9e6d27522970cd","name":"擷取水庫網頁 (文字模式)","method":"GET","ret":"txt","paytoqs":"ignore","url":"https://fhy.wra.gov.tw/ReservoirPage_2011/StorageCapacity.aspx","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"x":250,"y":140,"wires":[["8d80d24007d7ec6a"]]},{"id":"8d80d24007d7ec6a","type":"function","z":"fc9e6d27522970cd","name":"自定義字串解析 (所有水庫)","func":"// 由於 html 節點可能因複雜的 HTML/Selector 失敗,此處直接使用 RegExp/字串解析。\n\nlet htmlString = msg.payload;\nlet outputMsgs = [];\n\n// *** 包含您要求的所有 21 個水庫名稱 ***\nconst targetReservoirs = [\n    '石門水庫', \n    '新山水庫', \n    '翡翠水庫', \n    '寶山第二水庫', \n    '永和山水庫', \n    '明德水庫', \n    '鯉魚潭水庫', \n    '德基水庫', \n    '石岡壩', \n    '霧社水庫', \n    '日月潭水庫', \n    '集集攔河堰', \n    '湖山水庫', \n    '仁義潭水庫', \n    '白河水庫', \n    '烏山頭水庫', \n    '曾文水庫', \n    '南化水庫', \n    '阿公店水庫', \n    '高屏溪攔河堰', \n    '牡丹水庫'\n];\n\n\n// 步驟 1: 找到表格的內容 (<tr>...</tr>)\nconst tableContentRegex = /<table[^>]*id=\"ctl00_cphMain_gvList\"[^>]*>([\\s\\S]*?)<\\/table>/;\nconst match = htmlString.match(tableContentRegex);\n\nif (!match || match.length < 2) {\n    node.warn(\"未找到水庫表格內容。可能網站結構已更改。\");\n    return null;\n}\n\nconst tableHTML = match[1];\n\n// 步驟 2: 提取每一行的數據\n// 正則表達式: 尋找 <tr>...</tr> 內部的 <td> 值\nconst rowRegex = /<tr[^>]*>([\\s\\S]*?)<\\/tr>/g;\nlet rowMatch;\n\nwhile ((rowMatch = rowRegex.exec(tableHTML)) !== null) {\n    const rowHTML = rowMatch[1];\n    \n    // 提取所有的 <td> 值\n    const tdRegex = /<td[^>]*>([\\s\\S]*?)<\\/td>/g;\n    const tdMatches = [...rowHTML.matchAll(tdRegex)].map(m => m[1]);\n    \n    // 確保有足夠的欄位(至少11個 <td>, 從 0 開始編號)\n    if (tdMatches.length < 11) {\n        continue; \n    }\n    \n    // 欄位索引 (從 0 開始): 0: 水庫名稱, 1: 有效容量, 10: 蓄水量百分比\n    const name = tdMatches[0] ? tdMatches[0].trim() : '';\n    const capacityStr = tdMatches[1] ? tdMatches[1].trim() : 'N/A';\n    const percentageStr = tdMatches[10] ? tdMatches[10].trim() : '';\n    \n    // 過濾非目標水庫和附註行 (如'附註')\n    if (name && targetReservoirs.includes(name)) {\n        \n        // 處理百分比 (移除 % 和空格,轉為數字)\n        const percentage = parseFloat(percentageStr.replace('%', '').trim());\n        \n        if (name === '高屏溪攔河堰' || isNaN(percentage)) {\n             // 處理無法取得百分比的水庫(例如攔河堰)\n            outputMsgs.push({\n                topic: name,\n                payload: 0, // 儀表板繪圖用,值不重要\n                capacity: capacityStr, \n                special_status: '無百分比數據' \n            });\n        }\n        else {\n             // 正常水庫數據\n            outputMsgs.push({\n                topic: name,\n                payload: percentage, // 百分比數字\n                capacity: capacityStr, // 容量文字\n            });\n        }\n    }\n}\n\n// 檢查是否有提取到數據\nif (outputMsgs.length === 0) {\n    node.warn(\"自定義解析後未找到目標水庫數據。\");\n}\n\n// 將結果包裝在一個訊息中,輸出到 ui_template\nreturn { payload: outputMsgs };\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":420,"y":200,"wires":[["3df62c3d986ea509","8f42813483ec6d8c"]]},{"id":"3df62c3d986ea509","type":"ui_template","z":"fc9e6d27522970cd","group":"4d5c1a2b.3e4f5g","name":"儀表板圓形顯示 (21水庫適用)","order":1,"width":24,"height":14,"format":"<style>\n    /* 調整整體佈局為彈性佈局 */\n    #reservoir_dashboard {\n        display: flex;\n        flex-wrap: wrap;\n        justify-content: flex-start; \n        width: 100%;\n    }\n    /* 圓形卡片樣式 */\n    .reservoir-card {\n        display: flex; \n        flex-direction: column;\n        align-items: center;\n        justify-content: space-between;\n        margin: 10px; \n        width: 150px; /* 寬度略減以容納更多卡片 */\n        height: 190px;\n        padding: 5px; \n        background-color: #fff;\n        box-shadow: 0 2px 4px rgba(0,0,0,0.1); \n        border-radius: 8px; \n    }\n    /* 水庫名稱標題 */\n    .reservoir-card h3 {\n        font-size: 1em; \n        font-weight: bold;\n        margin: 5px 0;\n        text-align: center;\n        overflow: hidden; \n        white-space: nowrap;\n        text-overflow: ellipsis;\n        max-width: 100%;\n        height: 20px;\n    }\n    /* 圓形進度條容器 */\n    .circular-progress {\n        position: relative;\n        height: 120px;\n        width: 120px;\n        border-radius: 50%;\n        display: grid;\n        place-items: center;\n        margin: 5px auto;\n        background: conic-gradient(#f0f0f0 360deg, #f0f0f0 360deg);\n    }\n    /* 圓心白色遮罩 */\n    .value-container {\n        position: absolute;\n        height: 100px;\n        width: 100px;\n        border-radius: 50%;\n        background-color: #fff;\n        display: grid;\n        place-items: center;\n        font-size: 1.2em;\n        font-weight: bold;\n        box-shadow: inset 0 0 5px rgba(0,0,0,0.05);\n    }\n    /* 容量資訊文字 */\n    .info-text {\n        font-size: 0.75em;\n        color: #666;\n        margin-bottom: 5px;\n        height: 15px;\n    }\n</style>\n\n<div id=\"reservoir_dashboard\"></div>\n\n<script>\n(function(scope) {\n    // 監聽來自 Node-RED 的資料\n    scope.$watch('msg', function(msg) {\n        if (!msg || !msg.payload || !Array.isArray(msg.payload)) {\n            return;\n        }\n        \n        const container = document.getElementById('reservoir_dashboard');\n        container.innerHTML = ''; // 清空舊內容\n        \n        msg.payload.forEach(function(data) {\n            const name = data.topic;\n            const percentage = data.payload;\n            const capacity = data.capacity || 'N/A';\n            const specialStatus = data.special_status || null; \n\n            let color = '#28a745'; // 預設綠色\n            let displayPercentage = percentage.toFixed(1) + '%';\n            let gradient = `conic-gradient(${color} ${percentage * 3.6}deg, #e9ecef ${percentage * 3.6}deg)`;\n\n            if (specialStatus === '無百分比數據') {\n                color = '#6c757d'; // 灰色\n                displayPercentage = 'N/A'; \n                gradient = `conic-gradient(#6c757d 10deg, #e9ecef 10deg)`; // 顯示一個灰色標記,代表數據不適用\n            } else if (percentage < 60) {\n                color = '#ffc107'; // 黃色\n            } else if (percentage < 30) {\n                color = '#dc3545'; // 紅色\n            }\n\n            const card = document.createElement('div');\n            card.className = 'reservoir-card';\n            card.innerHTML = `\n                <h3>${name}</h3>\n                <div class=\"circular-progress\" style=\"background: ${gradient};\">\n                    <div class=\"value-container\" style=\"color:${color};\">${displayPercentage}</div>\n                </div>\n                <div class=\"info-text\">${capacity} 萬方</div>\n            `;\n            \n            container.appendChild(card);\n        });\n    });\n})(scope);\n</script>","storeOutMessages":true,"fwdInMessages":true,"x":700,"y":200,"wires":[[]]},{"id":"8f42813483ec6d8c","type":"debug","z":"fc9e6d27522970cd","name":"自定義解析輸出檢查","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":680,"y":280,"wires":[]},{"id":"4d5c1a2b.3e4f5g","type":"ui_group","name":"水庫水情儀表板","tab":"1a2b3c4d.5e6f7g","order":1,"disp":true,"width":24,"collapse":false},{"id":"1a2b3c4d.5e6f7g","type":"ui_tab","name":"台灣水庫水情","icon":"water-meter","order":1,"disabled":false,"hidden":false},{"id":"686bc41bd76eb607","type":"global-config","env":[],"modules":{"node-red-dashboard":"3.6.6"}}]

沒有留言:

張貼留言

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...