每天出門前總在猶豫要不要帶傘嗎?我們可以利用自動化工具 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": []
}