如何使用中央氣象署的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 整理成人類看得懂的樣子。