每天出門前總在猶豫要不要帶傘嗎?我們可以利用自動化工具 n8n,串接 OpenWeatherMap 的天氣預報服務,每天自動檢查隔天的降雨機率。如果可能會下雨,就自動發送一則提醒到你的 Discord,讓你再也不用煩惱這個問題
預計會使用 OpenWeatherMap 的免費 API 來取得天氣資料,所以要先去註冊帳號,並拿到 API Key
首先前往註冊頁面
登入後,在導覽列的下拉選單找到「My API Keys」

把「Key」的資料複製下來

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

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

選擇排程「On a schedule」

排程選擇你喜歡的時間

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

設定為台灣時間

設定好排程後,我們要加入節點來抓取天氣資料
下個節點選擇「OpenWeatherMap」

接著選要拿近期的資料

再來同樣要設定憑證

把剛剛的 API Key 貼上來

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

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

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

程式碼撰寫如下
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,
};

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

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

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

數字可以寫「0.2」

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

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

接著在「Message」的欄位撰寫以下內容,接著點選「Execute step」試跑看看
**明日天氣提醒 ☔️**
日期:{{ $json.date }}
最高降雨機率:{{ $json.maxPopPercent }}%
平均濕度:{{ $json.averageHumidity }}%
天氣概況:{{ $json.descriptions }}
如果降雨機率高於 50%,記得帶傘哦!

Discord 有看到訊息代表成功啦

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

恭喜!現在你有了一個專屬的天氣小幫手,再也不怕明天下雨沒帶惹
最後附上本次流程的 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": []
}