|
1 | 1 | const got = require('got'); |
| 2 | +const { DateTime } = require('luxon'); |
2 | 3 |
|
3 | | -const stationsURL = |
4 | | - 'http://www.weather.gov.sg/mobile/json/rest-get-all-climate-stations.json'; |
5 | | -let stationsData; |
6 | | -const getStations = async () => { |
7 | | - console.log('GET STATIONS start'); |
8 | | - console.time('GET STATIONS'); |
9 | | - if (stationsData) { |
10 | | - console.timeEnd('GET STATIONS'); |
11 | | - return stationsData; |
12 | | - } |
13 | | - const { body } = await got(stationsURL, { |
14 | | - responseType: 'json', |
15 | | - timeout: 3 * 1000, |
16 | | - headers: { 'user-agent': undefined }, |
17 | | - }); |
18 | | - console.timeEnd('GET STATIONS'); |
19 | | - stationsData = body; |
20 | | - return body; |
21 | | -}; |
| 4 | +// Have to be X minutes in the past, else it's too recent and lack of data |
| 5 | +const datetime = () => |
| 6 | + DateTime.local() |
| 7 | + .minus({ minutes: 10 }) |
| 8 | + .set({ second: 0 }) |
| 9 | + .setZone('Asia/Singapore') |
| 10 | + .toISO() |
| 11 | + .replace(/\..*$/, ''); // Remove the mili-seconds from the ISO-8601 timestamp |
22 | 12 |
|
23 | | -const dataURL = |
24 | | - 'http://www.weather.gov.sg/mobile/json/rest-get-latest-observation-for-all-locs.json'; |
25 | | -// const observationsCache = new Map(); |
26 | | -const numberRegexp = /[\d.]+/; |
27 | | -const getObservations = async () => { |
28 | | - const climateStations = await getStations(); |
| 13 | +const apiURLs = { |
| 14 | + temp_celcius: 'https://api.data.gov.sg/v1/environment/air-temperature', |
| 15 | + rain_mm: 'https://api.data.gov.sg/v1/environment/rainfall', |
| 16 | + relative_humidity: 'https://api.data.gov.sg/v1/environment/relative-humidity', |
| 17 | + wind_direction: 'https://api.data.gov.sg/v1/environment/wind-direction', |
| 18 | + wind_speed: 'https://api.data.gov.sg/v1/environment/wind-speed', |
| 19 | +}; |
| 20 | +const apiKeys = Object.keys(apiURLs); |
29 | 21 |
|
30 | | - console.log('GET OBS start'); |
31 | | - console.time('GET OBS'); |
32 | | - const { body: observations } = await got(dataURL, { |
| 22 | +const fetch = (url) => { |
| 23 | + const u = `${url}?date_time=${datetime()}`; |
| 24 | + console.log(`Fetching ${u}`); |
| 25 | + return got(u, { |
33 | 26 | responseType: 'json', |
34 | 27 | timeout: 2 * 1000, |
35 | 28 | retry: 2, |
36 | | - // cache: observationsCache, |
37 | 29 | maxRedirects: 1, |
38 | 30 | calculateDelay: () => 1000, |
39 | 31 | headers: { 'user-agent': undefined }, |
40 | 32 | }); |
41 | | - console.timeEnd('GET OBS'); |
| 33 | +}; |
42 | 34 |
|
43 | | - const obs = []; |
44 | | - Object.entries(observations.data.station).forEach(([stationID, obj]) => { |
45 | | - const { id, long, lat } = climateStations.data.find( |
46 | | - (d) => d.id === stationID, |
47 | | - ); |
48 | | - const values = {}; |
49 | | - for (let k in obj) { |
50 | | - const v = obj[k]; |
51 | | - if (numberRegexp.test(v)) { |
52 | | - const val = Number(v); |
53 | | - if (val) values[k] = val; |
54 | | - } |
55 | | - } |
| 35 | +// id, lng, lat, temp_celcius, relative_humidity, rain_mm, wind_direction, wind_speed |
56 | 36 |
|
57 | | - // Special case for S121 overlapping with S23 |
58 | | - if (id === 'S121') { |
59 | | - delete values.temp_celcius; |
60 | | - delete values.relative_humidity; |
| 37 | +const getObservations = async () => { |
| 38 | + const climateStations = {}; |
| 39 | + const observations = {}; |
| 40 | + const apiFetches = Object.values(apiURLs).map((url) => fetch(url)); |
| 41 | + const results = await Promise.allSettled(apiFetches); |
| 42 | + results.forEach((result, i) => { |
| 43 | + if (result.status !== 'fulfilled') { |
| 44 | + console.log('API fetch failed:', apiKeys[i]); |
| 45 | + return; |
61 | 46 | } |
62 | | - |
63 | | - if (Object.keys(values).length) { |
64 | | - obs.push({ |
65 | | - id, |
66 | | - lng: +(+long).toFixed(4), |
67 | | - lat: +(+lat).toFixed(4), |
68 | | - ...values, |
| 47 | + const { body } = result.value; |
| 48 | + body.metadata.stations.forEach((station) => { |
| 49 | + climateStations[station.id] = { |
| 50 | + lng: station.location.longitude, |
| 51 | + lat: station.location.latitude, |
| 52 | + }; |
| 53 | + }); |
| 54 | + body.items.forEach((item) => { |
| 55 | + item.readings.forEach((reading) => { |
| 56 | + if (!reading.value) return; |
| 57 | + if (observations[reading.station_id]) { |
| 58 | + observations[reading.station_id][apiKeys[i]] = reading.value; |
| 59 | + } else { |
| 60 | + observations[reading.station_id] = { |
| 61 | + [apiKeys[i]]: reading.value, |
| 62 | + }; |
| 63 | + } |
69 | 64 | }); |
70 | | - } |
| 65 | + }); |
| 66 | + }); |
| 67 | + |
| 68 | + const obs = Object.entries(observations).map(([stationID, observation]) => { |
| 69 | + return { |
| 70 | + id: stationID, |
| 71 | + lng: +climateStations[stationID].lng.toFixed(4), |
| 72 | + lat: +climateStations[stationID].lat.toFixed(4), |
| 73 | + ...observation, |
| 74 | + }; |
71 | 75 | }); |
72 | 76 |
|
73 | 77 | return obs; |
|
0 commit comments