Day: April 25, 2026

  • 如何使用中央氣象署的api 獲取當前天氣和預報

    我之前一直通過n8n 使用OpenWeatherMap 的api 來獲取天氣和預報並通過pushover 發送到手機,但是,由於台灣是海島型氣候,天氣多變,預報往往不太準確,而比較權威的the weather channel 又不提供api,中央氣象署的api 其實我很早就註冊了,之前沒有AI 幫忙,他們的文檔又非常讓人困擾,所以沒有使用。

    今天讓AI 看了一下他們的文檔,終於替換掉了OpenWeatherMap 的api 。

    中央氣象署開放資料平臺之資料擷取API – https://opendata.cwa.gov.tw/dist/opendata-swagger.html#/

    除了實時天氣之外,還有預報信息,由於台灣很小,所以可以具體到某一個區域,例如板橋區。

    但問題在於,板橋區的實時天氣和預報是兩個不同的api,而UV index 則不是在所有的氣象監測站都有,比如板橋區就沒有,新北只有一個。

    所以,為了生成這麼一條信息,需要調取三個api。

    當然,首先您需要去註冊一個中央氣象署會員 – https://pweb.cwa.gov.tw/emember/register/authorization

    註冊成功後,在會員資料部分,從“API授權碼” 中申請一個授權碼,您將會在後面的每一個api call 中使用他。

    我記得我註冊的時候他還是中央氣象局,升級署了。

    天氣預報api,板橋區監測站

    https://opendata.cwa.gov.tw/api/v1/rest/datastore/F-D0047-069?Authorization=YOUR-CWB-API-KEY&LocationName=板橋區

    實時天氣api,距離我最近的監測站

    https://opendata.cwa.gov.tw/api/v1/rest/datastore/O-A0001-001?Authorization=YOUR-CWB-API-KEY&StationId=C0AJ80

    UV index api, 只有比較大的站有。

    https://opendata.cwa.gov.tw/api/v1/rest/datastore/O-A0003-001?Authorization=YOUR-CWB-API-KEY&StationId=466881

    之所以使用三個api 是因為實時天氣我選了距離我最近的一個,如果您不需要那麼準確,可以只用新北區的監測站就可以。

    n8n 的 workflow 中,因為merge block 只支持兩個input,所以只能使用串行執行,如果只用兩個api 的話就可以merge ,稍快,但對於每個小時發送一次的天氣來說,這點時間差異其實沒有必要,串行就可以了。

    workflow 大概這個樣子。

    其中呢,code block 是把這三個api 的結果梳理一下,送給pushover。

    const obs = $('CWB Realtime Banqiao District').first().json.records.Station[0];
    const we = obs.WeatherElement;
    const fc = $('CWB Forecast Banqiao District').first().json.records.Locations[0].Location[0];
    const uvStation = $('CWA UV Index').first().json.records.Station[0].WeatherElement;
    
    const el = {};
    for (const e of fc.WeatherElement) el[e.ElementName] = e.Time;
    
    const at = el['體感溫度'];
    const pop = el['3小時降雨機率'];
    const wx = el['天氣現象'];
    const rh = el['相對濕度'];
    const ci = el['舒適度指數'];
    
    const days = {};
    for (const t of at) {
      const d = t.DataTime.slice(0, 10);
      if (!days[d]) days[d] = {};
      const h = t.DataTime.slice(11, 13);
      if (h === '06') days[d].morn = t.ElementValue[0].ApparentTemperature;
      if (h === '12') days[d].day = t.ElementValue[0].ApparentTemperature;
      if (h === '21') days[d].night = t.ElementValue[0].ApparentTemperature;
    }
    for (const t of wx) {
      const d = t.StartTime.slice(0, 10);
      if (!days[d]) days[d] = {};
      if (t.StartTime.slice(11, 13) === '12') days[d].wx = t.ElementValue[0].Weather;
    }
    for (const t of pop) {
      const d = t.StartTime.slice(0, 10);
      if (!days[d]) days[d] = {};
      if (t.StartTime.slice(11, 13) === '12') days[d].pop = t.ElementValue[0].ProbabilityOfPrecipitation;
    }
    for (const t of rh) {
      const d = t.DataTime.slice(0, 10);
      if (!days[d]) days[d] = {};
      if (t.DataTime.slice(11, 13) === '12') days[d].rh = t.ElementValue[0].RelativeHumidity;
    }
    for (const t of ci) {
      const d = t.DataTime.slice(0, 10);
      if (!days[d]) days[d] = {};
      if (t.DataTime.slice(11, 13) === '12') days[d].comfort = t.ElementValue[0].ComfortIndexDescription;
    }
    
    const dayTemp = {};
    for (const t of at) {
      const d = t.DataTime.slice(0, 10);
      const v = parseFloat(t.ElementValue[0].ApparentTemperature);
      if (!dayTemp[d]) dayTemp[d] = { min: v, max: v };
      if (v < dayTemp[d].min) dayTemp[d].min = v;
      if (v > dayTemp[d].max) dayTemp[d].max = v;
    }
    
    const sorted = Object.keys(days).sort();
    
    return [{json: {
      station: obs.StationName,
      weather: we.Weather !== '-99' ? we.Weather : uvStation.Weather,
      temp: we.AirTemperature,
      humidity: we.RelativeHumidity,
      wind: we.WindSpeed,
      windDir: el['風向'][0].ElementValue[0].WindDirection,
      rain: we.Now.Precipitation,
      pressure: we.AirPressure,
      high: we.DailyExtreme.DailyHigh.TemperatureInfo.AirTemperature,
      low: we.DailyExtreme.DailyLow.TemperatureInfo.AirTemperature,
      pop: pop[0].ElementValue[0].ProbabilityOfPrecipitation,
      comfort: ci[0].ElementValue[0].ComfortIndexDescription,
      feelsLike: at[0].ElementValue[0].ApparentTemperature,
      uv: uvStation.UVIndex,
      d0_morn: days[sorted[0]]?.morn || '-',
      d0_night: days[sorted[0]]?.night || '-',
      d1_wx: days[sorted[1]]?.wx || '-',
      d1_comfort: days[sorted[1]]?.comfort || '-',
      d1_min: dayTemp[sorted[1]]?.min || '-',
      d1_max: dayTemp[sorted[1]]?.max || '-',
      d1_rh: days[sorted[1]]?.rh || '-',
      d1_pop: days[sorted[1]]?.pop || '-',
      d2_wx: days[sorted[2]]?.wx || '-',
      d2_comfort: days[sorted[2]]?.comfort || '-',
      d2_min: dayTemp[sorted[2]]?.min || '-',
      d2_max: dayTemp[sorted[2]]?.max || '-',
      d2_rh: days[sorted[2]]?.rh || '-',
      d2_pop: days[sorted[2]]?.pop || '-',
    }}];

    在pushover 的block 中將這些output 整理成人類看得懂的樣子。