diff --git a/src/clients/weather/openmeteo.spec.ts b/src/clients/weather/openmeteo.spec.ts new file mode 100644 index 0000000..7b2875d --- /dev/null +++ b/src/clients/weather/openmeteo.spec.ts @@ -0,0 +1,26 @@ +import { createOpenmeteoClient } from './openmeteo'; + +describe('createOpenmeteoClient', () => { + const client = createOpenmeteoClient(); + it('returns some weather for the sample address', async () => { + const result = await client.getCurrentWeather({ + latitude: 52.28, + longitude: 10.52, + }); + expect(result.apparentTemperature).toMatch(/^\d+(\.?\d+)?°C$/); + expect(result.temperature).toMatch(/^\d+(\.?\d+)?°C$/); + expect(result.relativeHumidity).toMatch(/^\d+%$/); + + // Just to make sure that the returned weather seems to be reasonable + expect(result).not.toEqual({ + apparentTemperature: '0°C', + temperature: '0°C', + relativeHumidity: '0%', + }); + expect(result).not.toEqual({ + apparentTemperature: '0.0°C', + temperature: '0.0°C', + relativeHumidity: '0%', + }); + }); +}); diff --git a/src/clients/weather/openmeteo.ts b/src/clients/weather/openmeteo.ts new file mode 100644 index 0000000..1daf7d7 --- /dev/null +++ b/src/clients/weather/openmeteo.ts @@ -0,0 +1,68 @@ +import fetch from 'node-fetch'; +import PQueue from 'p-queue'; +import { WeatherProvider } from './types'; + +const requestedFields = [ + 'temperature_2m', + 'relativehumidity_2m', + 'apparent_temperature', + 'is_day', + 'precipitation', + 'rain', + 'showers', + 'snowfall', + 'weathercode', + 'cloudcover', + 'pressure_msl', + 'surface_pressure', + 'windspeed_10m', + 'winddirection_10m', + 'windgusts_10m', +] as const; + +type OpenmeteoKey = (typeof requestedFields)[number]; +type OpenmeteoResponse = + | { + current_units?: Record; + current?: Record; + } + | undefined; + +export const createOpenmeteoClient = (): WeatherProvider => { + // Rate limit (according to https://open-meteo.com/en/terms it's 10k per day; + // here we set it 1 per second, should be enough for the demo app goal) + const queue = new PQueue({ + interval: 1000, + intervalCap: 1, + timeout: 2000, + throwOnTimeout: true, + }); + + return { + getCurrentWeather: async ({ latitude, longitude }) => + queue.add(async () => { + const response = await fetch( + `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=${requestedFields.join( + ',', + )}`, + ); + const body = (await response.json()) as OpenmeteoResponse; + if (!body || !body.current || !body.current_units) { + throw new Error('cannot load weather'); + } + + const { current, current_units } = body; + const getReadableValue = (key: OpenmeteoKey) => + `${current[key]}${current_units[key]}`; + + return { + temperature: getReadableValue('temperature_2m'), + apparentTemperature: getReadableValue( + 'apparent_temperature', + ), + relativeHumidity: getReadableValue('relativehumidity_2m'), + //... + }; + }), + }; +}; diff --git a/src/clients/weather/types.ts b/src/clients/weather/types.ts new file mode 100644 index 0000000..871ea2b --- /dev/null +++ b/src/clients/weather/types.ts @@ -0,0 +1,10 @@ +export type WeatherProvider = { + getCurrentWeather(params: { + longitude: number; + latitude: number; + }): Promise<{ + temperature: string; + apparentTemperature: string; + relativeHumidity: string; + }>; +};