iT邦幫忙

2025 iThome 鐵人賽

DAY 9
0

每天出門前總在猶豫要不要帶傘嗎?我們可以利用自動化工具 n8n,串接 OpenWeatherMap 的天氣預報服務,每天自動檢查隔天的降雨機率。如果可能會下雨,就自動發送一則提醒到你的 Discord,讓你再也不用煩惱這個問題

前置作業

預計會使用 OpenWeatherMap 的免費 API 來取得天氣資料,所以要先去註冊帳號,並拿到 API Key

workflow

步驟一:建立 n8n 排程

  • 接著回到 n8n,點選右上角的「Create Workflow」來建立新的流程

    image 2.png

  • 點下畫布正中間的「Add first step」

    image 3.png

  • 選擇排程「On a schedule」

    image 4.png

  • 排程選擇你喜歡的時間

    image 5.png

  • 接下來到設定的地方設定時區

    image 6.png

  • 設定為台灣時間

    image 7.png

步驟二:抓取天氣預報資料

設定好排程後,我們要加入節點來抓取天氣資料

  • 下個節點選擇「OpenWeatherMap」

    image 8.png

  • 接著選要拿近期的資料

    image 9.png

  • 再來同樣要設定憑證

    image 10.png

  • 把剛剛的 API Key 貼上來

    image 11.png

  • 城市名稱寫「Taipei」,語系寫「zh_tw」

    image 12.png

  • 接著點選上方的「Execute step」試跑看看,可以看到回傳很多資料

    image 13.png

步驟三:用程式碼整理所需資訊

  • 為了過濾出需要的資料,下個節點選擇「Code」來寫程式碼

    image 14.png

  • 程式碼撰寫如下

    const forecastData = $input.item.json.list;
    
    // 計算明天的日期 (格式:YYYY-MM-DD)
    const tomorrow = new Date();
    tomorrow.setDate(tomorrow.getDate() + 1);
    const year = tomorrow.getFullYear();
    const month = String(tomorrow.getMonth() + 1).padStart(2, "0");
    const day = String(tomorrow.getDate()).padStart(2, "0");
    const tomorrowDateString = `${year}-${month}-${day}`;
    
    // 篩選出所有屬於明天的預報資料
    const tomorrowsForecasts = forecastData.filter((item) => {
      return item.dt_txt.startsWith(tomorrowDateString);
    });
    
    // 如果找不到明天的資料,就回傳空物件
    if (tomorrowsForecasts.length === 0) {
      return { bringUmbrella: false, reason: "找不到明天的天氣資料" };
    }
    
    let maxPop = 0;
    const weatherDescriptions = new Set();
    let totalHumidity = 0; // 用來累加濕度的變數
    
    for (const forecast of tomorrowsForecasts) {
      // 找出最高的降雨機率
      if (forecast.pop > maxPop) {
        maxPop = forecast.pop;
      }
      // 收集天氣描述
      weatherDescriptions.add(forecast.weather[0].description);
    
      // 將每個時段的濕度 (humidity) 加總起來
      totalHumidity += forecast.main.humidity;
    }
    
    // 計算平均濕度
    const averageHumidity = Math.round(totalHumidity / tomorrowsForecasts.length);
    
    const descriptions = Array.from(weatherDescriptions).join("、");
    
    return {
      maxPop: maxPop,
      maxPopPercent: Math.round(maxPop * 100),
      descriptions: descriptions,
      date: tomorrowDateString,
      averageHumidity: averageHumidity,
    };
    

    image 15.png

步驟四:設定條件判斷

  • 下個節點選擇「If」來做判斷,因為我希望會下雨才傳送通知

    image 16.png

  • 設定的時候把上個節點的「maxPop」變數資料抓到欄位裡面

    image 17.png

  • 運算子選擇大於的「is greater than」

    image 18.png

  • 數字可以寫「0.2」

    image 19.png

步驟五:發送 Discord 通知

  • 接著在「true」的下個節點選擇「Discord」來發送訊息

    image 20.png

  • 「Connection Type」選擇「Webhook」,憑證的串接在之前的文章有撰寫過,這邊就不重複惹

    image 21.png

  • 接著在「Message」的欄位撰寫以下內容,接著點選「Execute step」試跑看看

    **明日天氣提醒 ☔️**
    
    日期:{{ $json.date }}
    最高降雨機率:{{ $json.maxPopPercent }}%
    平均濕度:{{ $json.averageHumidity }}%
    天氣概況:{{ $json.descriptions }}
    
    如果降雨機率高於 50%,記得帶傘哦!
    

    image 22.png

  • Discord 有看到訊息代表成功啦

    image 23.png

  • 完工的流程圖會長底下這樣,記得到最上方切換為「Active」來讓流程運作哦

    image 24.png

恭喜!現在你有了一個專屬的天氣小幫手,再也不怕明天下雨沒帶惹

  • 最後附上本次流程的 JSON 內容

    {
      "name": "明天要不要帶傘",
      "nodes": [
        {
          "parameters": {
            "rule": {
              "interval": [
                {
                  "triggerAtHour": 22
                }
              ]
            }
          },
          "type": "n8n-nodes-base.scheduleTrigger",
          "typeVersion": 1.2,
          "position": [0, 0],
          "id": "74d8a024-b3aa-4dbf-aafc-15c3708a3ce3",
          "name": "Schedule Trigger"
        },
        {
          "parameters": {
            "operation": "5DayForecast",
            "cityName": "Taipei",
            "language": "zh_tw"
          },
          "type": "n8n-nodes-base.openWeatherMap",
          "typeVersion": 1,
          "position": [220, 0],
          "id": "001391d0-da07-46ee-aed4-106124a03287",
          "name": "OpenWeatherMap"
        },
        {
          "parameters": {
            "jsCode": "const forecastData = $input.item.json.list;\\n\\n// 計算明天的日期 (格式:YYYY-MM-DD)\\nconst tomorrow = new Date();\\ntomorrow.setDate(tomorrow.getDate() + 1);\\nconst year = tomorrow.getFullYear();\\nconst month = String(tomorrow.getMonth() + 1).padStart(2, '0');\\nconst day = String(tomorrow.getDate()).padStart(2, '0');\\nconst tomorrowDateString = `${year}-${month}-${day}`;\\n\\n// 篩選出所有屬於明天的預報資料\\nconst tomorrowsForecasts = forecastData.filter(item => {\\n  return item.dt_txt.startsWith(tomorrowDateString);\\n});\\n\\n// 如果找不到明天的資料,就回傳空物件\\nif (tomorrowsForecasts.length === 0) {\\n  return { bringUmbrella: false, reason: \\\"找不到明天的天氣資料\\\" };\\n}\\n\\nlet maxPop = 0;\\nconst weatherDescriptions = new Set();\\nlet totalHumidity = 0; // 用來累加濕度的變數\\n\\nfor (const forecast of tomorrowsForecasts) {\\n  // 找出最高的降雨機率\\n  if (forecast.pop > maxPop) {\\n    maxPop = forecast.pop;\\n  }\\n  // 收集天氣描述\\n  weatherDescriptions.add(forecast.weather[0].description);\\n  \\n  // 將每個時段的濕度 (humidity) 加總起來\\n  totalHumidity += forecast.main.humidity;\\n}\\n\\n// 計算平均濕度\\nconst averageHumidity = Math.round(totalHumidity / tomorrowsForecasts.length);\\n\\nconst descriptions = Array.from(weatherDescriptions).join('、');\\n\\nreturn {\\n  maxPop: maxPop,\\n  maxPopPercent: Math.round(maxPop * 100),\\n  descriptions: descriptions,\\n  date: tomorrowDateString,\\n  averageHumidity: averageHumidity \\n};"
          },
          "type": "n8n-nodes-base.code",
          "typeVersion": 2,
          "position": [440, 0],
          "id": "3881fb39-36c2-41bf-b9b4-c0eb8cd18e2f",
          "name": "Code"
        },
        {
          "parameters": {
            "conditions": {
              "options": {
                "caseSensitive": true,
                "leftValue": "",
                "typeValidation": "strict",
                "version": 2
              },
              "conditions": [
                {
                  "id": "7953be12-535e-4b91-bf3f-3e7f232080cf",
                  "leftValue": "={{ $json.maxPop }}",
                  "rightValue": 0.2,
                  "operator": {
                    "type": "number",
                    "operation": "gt"
                  }
                }
              ],
              "combinator": "and"
            },
            "options": {}
          },
          "type": "n8n-nodes-base.if",
          "typeVersion": 2.2,
          "position": [660, 0],
          "id": "67d42393-6be6-4691-829c-94447d409957",
          "name": "If"
        },
        {
          "parameters": {
            "authentication": "webhook",
            "content": "=**明日天氣提醒 ☔️**\\n\\n日期:{{ $json.date }}\\n最高降雨機率:{{ $json.maxPopPercent }}%\\n平均濕度:{{ $json.averageHumidity }}%\\n天氣概況:{{ $json.descriptions }}\\n\\n如果降雨機率高於 50%,記得帶傘哦!",
            "options": {}
          },
          "type": "n8n-nodes-base.discord",
          "typeVersion": 2,
          "position": [920, -100],
          "id": "701e9f88-56a9-4527-90f7-85987f92cd62",
          "name": "Discord"
        }
      ],
      "pinData": {},
      "connections": {
        "Schedule Trigger": {
          "main": [
            [
              {
                "node": "OpenWeatherMap",
                "type": "main",
                "index": 0
              }
            ]
          ]
        },
        "OpenWeatherMap": {
          "main": [
            [
              {
                "node": "Code",
                "type": "main",
                "index": 0
              }
            ]
          ]
        },
        "Code": {
          "main": [
            [
              {
                "node": "If",
                "type": "main",
                "index": 0
              }
            ]
          ]
        },
        "If": {
          "main": [
            [
              {
                "node": "Discord",
                "type": "main",
                "index": 0
              }
            ]
          ]
        }
      },
      "active": true,
      "settings": {
        "executionOrder": "v1",
        "timezone": "Asia/Taipei",
        "callerPolicy": "workflowsFromSameOwner",
        "executionTimeout": -1
      },
      "versionId": "ecb39b67-e15c-4d14-978d-4a12d72cdcf8",
      "meta": {
        "templateCredsSetupCompleted": true
      },
      "id": "VJsgIoA7ecaAJDQ6",
      "tags": []
    }
    

上一篇
[Day08]_午餐吃什麼
下一篇
[Day10]_當季片單資料庫
系列文
告別重複瑣事: n8n workflow 自動化工作實踐10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言