diff --git a/app/src/api/routes.d.ts b/app/src/api/routes.d.ts index c51f36e6..d9bfcc25 100644 --- a/app/src/api/routes.d.ts +++ b/app/src/api/routes.d.ts @@ -42,27 +42,22 @@ export interface UnsplashImage { altText?: string; [key: string]: any; } +export declare enum WeatherIcon { + Sun = "sun", + CloudSun = "cloud-sun", + CloudSunRain = "cloud-sun-rain", + Moon = "moon", + CloudMoon = "cloud-moon", + CloudMoonRain = "cloud-moon-rain", + Clouds = "clouds", + CloudShowers = "cloud-showers", + Thunderstorm = "thunderstorm", + Snow = "snow", + Fog = "fog", +} export interface WeatherCondition { description: string; - icon: - | "01d" - | "02d" - | "03d" - | "04d" - | "09d" - | "10d" - | "11d" - | "13d" - | "50d" - | "01n" - | "02n" - | "03n" - | "04n" - | "09n" - | "10n" - | "11n" - | "13n" - | "50n"; + icon: WeatherIcon; [key: string]: any; } export interface WeatherData { diff --git a/app/src/api/routes.js b/app/src/api/routes.js index 975bde38..773f584a 100644 --- a/app/src/api/routes.js +++ b/app/src/api/routes.js @@ -9,6 +9,20 @@ * --------------------------------------------------------------- */ +export var WeatherIcon; +(function (WeatherIcon) { + WeatherIcon["Sun"] = "sun"; + WeatherIcon["CloudSun"] = "cloud-sun"; + WeatherIcon["CloudSunRain"] = "cloud-sun-rain"; + WeatherIcon["Moon"] = "moon"; + WeatherIcon["CloudMoon"] = "cloud-moon"; + WeatherIcon["CloudMoonRain"] = "cloud-moon-rain"; + WeatherIcon["Clouds"] = "clouds"; + WeatherIcon["CloudShowers"] = "cloud-showers"; + WeatherIcon["Thunderstorm"] = "thunderstorm"; + WeatherIcon["Snow"] = "snow"; + WeatherIcon["Fog"] = "fog"; +})(WeatherIcon || (WeatherIcon = {})); import axios from "axios"; export var ContentType; (function (ContentType) { diff --git a/app/src/components/icon/custom/index.tsx b/app/src/components/icon/custom/index.tsx index e681875b..a52aba5c 100644 --- a/app/src/components/icon/custom/index.tsx +++ b/app/src/components/icon/custom/index.tsx @@ -1,6 +1,5 @@ import React from "react"; -import { ReactComponent as Cloud } from "./weather/cloud.svg"; import { ReactComponent as Clouds } from "./weather/clouds.svg"; import { ReactComponent as CloudMoon } from "./weather/cloud-moon.svg"; import { ReactComponent as CloudMoonRain } from "./weather/cloud-moon-rain.svg"; @@ -14,50 +13,33 @@ import { ReactComponent as Sun } from "./weather/sun.svg"; import { ReactComponent as Thunderstorm } from "./weather/thunderstorm.svg"; export type CustomIconName = - | "01d" - | "02d" - | "03d" - | "04d" - | "09d" - | "10d" - | "11d" - | "13d" - | "50d" - | "01n" - | "02n" - | "03n" - | "04n" - | "09n" - | "10n" - | "11n" - | "13n" - | "50n"; + | "sun" + | "cloud-sun" + | "cloud-sun-rain" + | "moon" + | "cloud-moon" + | "cloud-moon-rain" + | "clouds" + | "cloud-showers" + | "thunderstorm" + | "snow" + | "fog"; const icons: Record< CustomIconName, React.FunctionComponent> > = { - // day variants - "01d": Sun, - "02d": CloudSun, - "03d": Cloud, - "04d": Clouds, - "09d": CloudShowers, - "10d": CloudSunRain, - "11d": Thunderstorm, - "13d": Snow, - "50d": Fog, - - // night variants - "01n": Moon, - "02n": CloudMoon, - "03n": Cloud, - "04n": Clouds, - "09n": CloudShowers, - "10n": CloudMoonRain, - "11n": Thunderstorm, - "13n": Snow, - "50n": Fog, + sun: Sun, + "cloud-sun": CloudSun, + "cloud-sun-rain": CloudSunRain, + moon: Moon, + "cloud-moon": CloudMoon, + "cloud-moon-rain": CloudMoonRain, + clouds: Clouds, + "cloud-showers": CloudShowers, + thunderstorm: Thunderstorm, + snow: Snow, + fog: Fog, }; export default icons; diff --git a/app/src/components/icon/custom/weather/cloud.svg b/app/src/components/icon/custom/weather/cloud.svg deleted file mode 100644 index 82b64721..00000000 --- a/app/src/components/icon/custom/weather/cloud.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/app/src/widgets/weather/__stories__/index.stories.tsx b/app/src/widgets/weather/__stories__/index.stories.tsx index d04ad2a0..46fb1be6 100644 --- a/app/src/widgets/weather/__stories__/index.stories.tsx +++ b/app/src/widgets/weather/__stories__/index.stories.tsx @@ -12,24 +12,17 @@ export default { }; const weatherIcons = [ - "01d", - "02d", - "03d", - "04d", - "09d", - "10d", - "11d", - "13d", - "50d", - "01n", - "02n", - "03n", - "04n", - "09n", - "10n", - "11n", - "13n", - "50n", + "sun", + "cloud-sun", + "cloud-sun-rain", + "moon", + "cloud-moon", + "cloud-moon-rain", + "clouds", + "cloud-showers", + "thunderstorm", + "snow", + "fog", ] as const; const createForecast = () => { @@ -69,7 +62,7 @@ export const Variants = () => { temperature: 24, condition: { description: "clear sky", - icon: "01d", + icon: "sun", }, }, forecast: createForecast(), diff --git a/app/src/widgets/weather/__tests__/index.test.tsx b/app/src/widgets/weather/__tests__/index.test.tsx index 5d93e549..c4aff62f 100644 --- a/app/src/widgets/weather/__tests__/index.test.tsx +++ b/app/src/widgets/weather/__tests__/index.test.tsx @@ -2,6 +2,7 @@ import React from "react"; import { render, screen } from "common/testing"; import { widgetContentProps, widgetStatusDisplay } from "common/utils/mock"; +import type { WeatherIcon } from "api"; import Weather from "../index"; @@ -12,7 +13,7 @@ const commonProps = { temperature: 24, condition: { description: "clear sky", - icon: "01d" as const, + icon: "sun" as WeatherIcon, }, }, forecast: [ @@ -20,7 +21,7 @@ const commonProps = { date: 1598180400, condition: { description: "overcast clouds", - icon: "04d" as const, + icon: "clouds" as WeatherIcon, }, temperatureMin: 18, temperatureMax: 30, diff --git a/app/src/widgets/weather/configuration.tsx b/app/src/widgets/weather/configuration.tsx index 2a9a458a..e60e3687 100644 --- a/app/src/widgets/weather/configuration.tsx +++ b/app/src/widgets/weather/configuration.tsx @@ -51,6 +51,15 @@ const Configuration = ({ } options={["metric", "imperial"] as const} /> +
+ {""}, + }} + /> +
); }; diff --git a/docs/changelog/README.md b/docs/changelog/README.md index 31bf7355..77577d43 100644 --- a/docs/changelog/README.md +++ b/docs/changelog/README.md @@ -3,6 +3,7 @@ ## Current development - :boom: Remove Twitter integration. +- :boom: Switch from OpenWeather to [[Open-Meteo](https://open-meteo.com). - :boom: Drop Node 16 support. - :sparkles: New widget: [Weather](https://dashboard.darekkay.com/docs/widgets/weather.html). - :sparkles: New widget: [Day Countdown](https://dashboard.darekkay.com/docs/widgets/day-countdown.html). diff --git a/docs/development/README.md b/docs/development/README.md index b6f64e20..17631a2f 100644 --- a/docs/development/README.md +++ b/docs/development/README.md @@ -18,7 +18,6 @@ Recommended: Some widgets require a 3rd-party API key: - [Unsplash](https://unsplash.com/documentation) -- [OpenWeatherMap](https://openweathermap.org/api) - [YouTube](https://developers.google.com/youtube/v3/getting-started) Those keys need to be stored at `server/.env` (copy `server/.env.example` as a template). The project will run with missing API keys, but the affected widgets will not work. diff --git a/server/.env.example b/server/.env.example index ed0d37ba..f0524e9c 100644 --- a/server/.env.example +++ b/server/.env.example @@ -1,4 +1,3 @@ SAVE_AXIOS_RESPONSES=false -API_OPEN_WEATHER_MAP= API_UNSPLASH= API_YOUTUBE= diff --git a/server/.env.test b/server/.env.test index c2ae9ac9..3e86977b 100644 --- a/server/.env.test +++ b/server/.env.test @@ -1,3 +1,2 @@ -API_OPEN_WEATHER_MAP=mock-api-key API_UNSPLASH=mock-api-key API_YOUTUBE=mock-api-key diff --git a/server/src/api/routes.ts b/server/src/api/routes.ts index aee958d3..105c9641 100644 --- a/server/src/api/routes.ts +++ b/server/src/api/routes.ts @@ -51,11 +51,16 @@ const models: TsoaRoute.Models = { "additionalProperties": true, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "WeatherIcon": { + "dataType": "refAlias", + "type": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["sun"]},{"dataType":"enum","enums":["cloud-sun"]},{"dataType":"enum","enums":["cloud-sun-rain"]},{"dataType":"enum","enums":["moon"]},{"dataType":"enum","enums":["cloud-moon"]},{"dataType":"enum","enums":["cloud-moon-rain"]},{"dataType":"enum","enums":["clouds"]},{"dataType":"enum","enums":["cloud-showers"]},{"dataType":"enum","enums":["thunderstorm"]},{"dataType":"enum","enums":["snow"]},{"dataType":"enum","enums":["fog"]}],"validators":{}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "WeatherCondition": { "dataType": "refObject", "properties": { "description": {"dataType":"string","required":true}, - "icon": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["01d"]},{"dataType":"enum","enums":["02d"]},{"dataType":"enum","enums":["03d"]},{"dataType":"enum","enums":["04d"]},{"dataType":"enum","enums":["09d"]},{"dataType":"enum","enums":["10d"]},{"dataType":"enum","enums":["11d"]},{"dataType":"enum","enums":["13d"]},{"dataType":"enum","enums":["50d"]},{"dataType":"enum","enums":["01n"]},{"dataType":"enum","enums":["02n"]},{"dataType":"enum","enums":["03n"]},{"dataType":"enum","enums":["04n"]},{"dataType":"enum","enums":["09n"]},{"dataType":"enum","enums":["10n"]},{"dataType":"enum","enums":["11n"]},{"dataType":"enum","enums":["13n"]},{"dataType":"enum","enums":["50n"]}],"required":true}, + "icon": {"ref":"WeatherIcon","required":true}, }, "additionalProperties": true, }, diff --git a/server/src/api/swagger.json b/server/src/api/swagger.json index 595b743f..ba9231f1 100644 --- a/server/src/api/swagger.json +++ b/server/src/api/swagger.json @@ -87,33 +87,29 @@ "type": "object", "additionalProperties": true }, + "WeatherIcon": { + "type": "string", + "enum": [ + "sun", + "cloud-sun", + "cloud-sun-rain", + "moon", + "cloud-moon", + "cloud-moon-rain", + "clouds", + "cloud-showers", + "thunderstorm", + "snow", + "fog" + ] + }, "WeatherCondition": { "properties": { "description": { "type": "string" }, "icon": { - "type": "string", - "enum": [ - "01d", - "02d", - "03d", - "04d", - "09d", - "10d", - "11d", - "13d", - "50d", - "01n", - "02n", - "03n", - "04n", - "09n", - "10n", - "11n", - "13n", - "50n" - ] + "$ref": "#/components/schemas/WeatherIcon" } }, "required": [ diff --git a/server/src/config.ts b/server/src/config.ts index 6ae56881..e0dc08c1 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -13,7 +13,6 @@ const config = { port: process.env.PORT ?? 3401, saveAxiosResponses: process.env.SAVE_AXIOS_RESPONSES === "true", api: { - openWeatherMap: process.env.API_OPEN_WEATHER_MAP, unsplash: process.env.API_UNSPLASH, youtube: process.env.API_YOUTUBE, }, diff --git a/server/src/routes/__tests__/__examples__/weather.json b/server/src/routes/__tests__/__examples__/weather.json index a9207d51..ee043373 100644 --- a/server/src/routes/__tests__/__examples__/weather.json +++ b/server/src/routes/__tests__/__examples__/weather.json @@ -1,318 +1,45 @@ { "data": { - "lat": 52.17, - "lon": 11.67, - "timezone": "Europe/Berlin", - "timezone_offset": 7200, + "latitude": 52.16, + "longitude": 11.68, + "generationtime_ms": 0.1150369644165039, + "utc_offset_seconds": 0, + "timezone": "GMT", + "timezone_abbreviation": "GMT", + "elevation": 46.0, + "current_units": { + "time": "iso8601", + "interval": "seconds", + "temperature_2m": "°C", + "is_day": "", + "weather_code": "wmo code" + }, "current": { - "dt": 1597828195, - "sunrise": 1597809847, - "sunset": 1597861798, - "temp": 23.59, - "feels_like": 24.11, - "pressure": 1010, - "humidity": 57, - "dew_point": 14.58, - "uvi": 6, - "clouds": 0, - "visibility": 10000, - "wind_speed": 1.34, - "wind_deg": 90, - "weather": [ - { - "id": 800, - "main": "Clear", - "description": "clear sky", - "icon": "01d" - } - ] + "time": "2024-08-13T16:15", + "interval": 900, + "temperature_2m": 24, + "is_day": 1, + "weather_code": 0 + }, + "daily_units": { + "time": "iso8601", + "weather_code": "wmo code", + "temperature_2m_max": "°C", + "temperature_2m_min": "°C" }, - "daily": [ - { - "dt": 1597834800, - "sunrise": 1597809847, - "sunset": 1597861798, - "temp": { - "day": 24.08, - "min": 18, - "max": 24.84, - "night": 18, - "eve": 21.65, - "morn": 23.59 - }, - "feels_like": { - "day": 23.52, - "night": 17, - "eve": 22.37, - "morn": 23.39 - }, - "pressure": 1010, - "humidity": 53, - "dew_point": 13.91, - "wind_speed": 2.56, - "wind_deg": 283, - "weather": [ - { - "id": 800, - "main": "Clear", - "description": "clear sky", - "icon": "01d" - } - ], - "clouds": 0, - "pop": 0, - "uvi": 6 - }, - { - "dt": 1597921200, - "sunrise": 1597896346, - "sunset": 1597948072, - "temp": { - "day": 25.94, - "min": 18.71, - "max": 27.2, - "night": 21.16, - "eve": 24.31, - "morn": 18.71 - }, - "feels_like": { - "day": 26.03, - "night": 22.01, - "eve": 26.79, - "morn": 17.23 - }, - "pressure": 1011, - "humidity": 57, - "dew_point": 16.86, - "wind_speed": 3.13, - "wind_deg": 219, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "clouds": 92, - "pop": 0.14, - "uvi": 5.88 - }, - { - "dt": 1598007600, - "sunrise": 1597982845, - "sunset": 1598034345, - "temp": { - "day": 30.46, - "min": 21.6, - "max": 30.46, - "night": 21.6, - "eve": 24.91, - "morn": 22.25 - }, - "feels_like": { - "day": 30.02, - "night": 24.03, - "eve": 26.3, - "morn": 21.98 - }, - "pressure": 1013, - "humidity": 45, - "dew_point": 17.47, - "wind_speed": 4.12, - "wind_deg": 195, - "weather": [ - { - "id": 501, - "main": "Rain", - "description": "moderate rain", - "icon": "10d" - } - ], - "clouds": 93, - "pop": 0.86, - "rain": 3.39, - "uvi": 5.93 - }, - { - "dt": 1598094000, - "sunrise": 1598069344, - "sunset": 1598120616, - "temp": { - "day": 20.66, - "min": 16.04, - "max": 22.21, - "night": 16.04, - "eve": 20.26, - "morn": 21.67 - }, - "feels_like": { - "day": 19.59, - "night": 14.95, - "eve": 19.9, - "morn": 21.87 - }, - "pressure": 1014, - "humidity": 78, - "dew_point": 16.74, - "wind_speed": 4.75, - "wind_deg": 210, - "weather": [ - { - "id": 501, - "main": "Rain", - "description": "moderate rain", - "icon": "10d" - } - ], - "clouds": 100, - "pop": 1, - "rain": 3.81, - "uvi": 5.48 - }, - { - "dt": 1598180400, - "sunrise": 1598155842, - "sunset": 1598206887, - "temp": { - "day": 22.22, - "min": 14.26, - "max": 22.22, - "night": 14.26, - "eve": 17.82, - "morn": 16.6 - }, - "feels_like": { - "day": 18.94, - "night": 11.98, - "eve": 16.02, - "morn": 14.42 - }, - "pressure": 1013, - "humidity": 51, - "dew_point": 11.67, - "wind_speed": 5.39, - "wind_deg": 263, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "clouds": 2, - "pop": 0.52, - "rain": 0.41, - "uvi": 5.13 - }, - { - "dt": 1598266800, - "sunrise": 1598242341, - "sunset": 1598293156, - "temp": { - "day": 19.71, - "min": 13.12, - "max": 20.05, - "night": 13.12, - "eve": 16.7, - "morn": 14.38 - }, - "feels_like": { - "day": 17.04, - "night": 11.53, - "eve": 15.16, - "morn": 13.5 - }, - "pressure": 1015, - "humidity": 53, - "dew_point": 10.01, - "wind_speed": 3.83, - "wind_deg": 289, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "clouds": 59, - "pop": 0.63, - "rain": 1.37, - "uvi": 4.72 - }, - { - "dt": 1598353200, - "sunrise": 1598328840, - "sunset": 1598379425, - "temp": { - "day": 21.77, - "min": 13.8, - "max": 22.85, - "night": 15.48, - "eve": 19.15, - "morn": 13.8 - }, - "feels_like": { - "day": 19.77, - "night": 13.44, - "eve": 17.83, - "morn": 13.2 - }, - "pressure": 1015, - "humidity": 43, - "dew_point": 8.83, - "wind_speed": 2.42, - "wind_deg": 199, - "weather": [ - { - "id": 802, - "main": "Clouds", - "description": "scattered clouds", - "icon": "03d" - } - ], - "clouds": 37, - "pop": 0, - "uvi": 5 - }, - { - "dt": 1598439600, - "sunrise": 1598415339, - "sunset": 1598465693, - "temp": { - "day": 26.19, - "min": 15.82, - "max": 27.54, - "night": 15.82, - "eve": 21.11, - "morn": 16.88 - }, - "feels_like": { - "day": 22.97, - "night": 14.06, - "eve": 17.19, - "morn": 14.87 - }, - "pressure": 1006, - "humidity": 45, - "dew_point": 13.4, - "wind_speed": 6.08, - "wind_deg": 212, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "clouds": 73, - "pop": 0.7, - "rain": 1.92, - "uvi": 4.98 - } - ] + "daily": { + "time": [ + "2024-08-13", + "2024-08-14", + "2024-08-15", + "2024-08-16", + "2024-08-17", + "2024-08-18", + "2024-08-19" + ], + "weather_code": [0, 2, 3, 45, 51, 61, 71, 95, 404], + "temperature_2m_max": [25.1, 32.5, 27.8, 28.8, 23.0, 21.3, 23.2, 21.2, 1], + "temperature_2m_min": [18.3, 18.0, 19.6, 19.0, 18.5, 16.7, 13.9, 16.9, -2] + } } } diff --git a/server/src/routes/__tests__/weather.test.ts b/server/src/routes/__tests__/weather.test.ts index ae0db85b..e10647e5 100644 --- a/server/src/routes/__tests__/weather.test.ts +++ b/server/src/routes/__tests__/weather.test.ts @@ -4,7 +4,7 @@ import logger from "@darekkay/logger"; import axios from "axios"; import app from "../../app"; -import openweatherMockResponse from "./__examples__/weather.json"; +import openMeteoMockResponse from "./__examples__/weather.json"; describe("weather", () => { afterEach(() => { @@ -13,7 +13,7 @@ describe("weather", () => { test("should return a valid response", async () => { const mockedAxios = axios as jest.Mocked; - mockedAxios.get.mockResolvedValueOnce(openweatherMockResponse); + mockedAxios.get.mockResolvedValueOnce(openMeteoMockResponse); return request(app) .get("/weather") diff --git a/server/src/routes/weather.ts b/server/src/routes/weather.ts index a6bf09b5..1c97078d 100644 --- a/server/src/routes/weather.ts +++ b/server/src/routes/weather.ts @@ -1,31 +1,27 @@ import _ from "lodash"; +import dayjs from "dayjs"; +import logger from "@darekkay/logger"; import { Controller, Get, Query, Route } from "tsoa"; import axios from "../axios"; -import config from "../config"; import { ttlForWidgetType } from "../utils"; +type WeatherIcon = + | "sun" + | "cloud-sun" + | "cloud-sun-rain" + | "moon" + | "cloud-moon" + | "cloud-moon-rain" + | "clouds" + | "cloud-showers" + | "thunderstorm" + | "snow" + | "fog"; + interface WeatherCondition { description: string; - icon: - | "01d" - | "02d" - | "03d" - | "04d" - | "09d" - | "10d" - | "11d" - | "13d" - | "50d" - | "01n" - | "02n" - | "03n" - | "04n" - | "09n" - | "10n" - | "11n" - | "13n" - | "50n"; + icon: WeatherIcon; } interface WeatherData { @@ -41,6 +37,129 @@ interface WeatherData { }>; } +const getWeatherDescription = (weatherCode: number): string => { + // TODO: internationalize this + // WMO weather code + return ( + { + 0: "Clear sky", + 1: "Mainly clear", + 2: "Partly cloudy", + 3: "Overcast", + 45: "Fog", + 48: "Fog", + 51: "Light drizzle", + 53: "Moderate drizzle", + 55: "Dense drizzle", + 56: "Light freezing drizzle", + 57: "Dense freezing drizzle", + 61: "Slight rain", + 63: "Moderate rain", + 65: "Heavy rain", + 66: "Light freezing rain", + 67: "Heavy freezing rain", + 80: "Slight rain showers", + 81: "Moderate rain showers", + 82: "Violent rain showers", + 71: "Slight snow fall", + 73: "Moderate snow fall", + 75: "Heavy snow fall", + 77: "Snow grains", + 85: "Slight snow showers", + 86: "Heavy snow showers", + 95: "Thunderstorm", + 96: "Thunderstorm with slight hail", + 99: "Thunderstorm with heavy hail", + }[weatherCode] ?? "Unknown" + ); +}; + +// eslint-disable-next-line complexity +const getWeatherIcon = ( + weatherCode: number, + time: "day" | "night" +): WeatherIcon => { + // WMO weather code + switch (weatherCode) { + case 0: + case 1: + return time === "day" ? "sun" : "moon"; + case 2: + return time === "day" ? "cloud-sun" : "cloud-moon"; + case 3: + return "clouds"; + case 45: + case 48: + return "fog"; + case 51: + case 53: + case 55: + case 56: + case 57: + case 80: + case 81: + case 82: + return time === "day" ? "cloud-sun-rain" : "cloud-moon-rain"; + case 61: + case 63: + case 65: + case 66: + case 67: + return "cloud-showers"; + case 71: + case 73: + case 75: + case 77: + case 85: + case 86: + return "snow"; + case 95: + case 96: + case 99: + return "thunderstorm"; + default: { + logger.error(`Unknown weather code: ${weatherCode}`); + // let's return _something_ anyway + return time === "day" ? "sun" : "moon"; + } + } +}; + +const getCondition = ( + weatherCode: number, + time: "day" | "night" +): WeatherCondition => { + return { + description: getWeatherDescription(weatherCode), + icon: getWeatherIcon(weatherCode, time), + }; +}; + +const getForecast = ({ + time, + weather_code, + temperature_2m_max, + temperature_2m_min, +}: { + time: Array; + weather_code: Array; + temperature_2m_min: Array; + temperature_2m_max: Array; +}) => { + return _.zipWith( + time, + weather_code, + temperature_2m_min, + temperature_2m_max, + (date, weatherCode, temperatureMin, temperatureMax) => ({ + date: dayjs(date).unix(), + condition: getCondition(weatherCode, "day"), + temperatureMin: _.round(temperatureMin), + temperatureMax: _.round(temperatureMax), + }) + ).splice(0, 5); +}; + @Route("/weather") export class WeatherController extends Controller { /** @@ -57,32 +176,30 @@ export class WeatherController extends Controller { @Query() unit: "imperial" | "metric" ): Promise { const axiosResponse = await axios.get( - "https://api.openweathermap.org/data/2.5/onecall", + "https://api.open-meteo.com/v1/forecast", { params: { - lat, - lon, - units: unit, // "imperial" | "metric" - exclude: "minutely,hourly", - appid: config.api.openWeatherMap, + latitude: lat, + longitude: lon, + temperature_unit: unit === "imperial" ? "fahrenheit" : "celsius", + current: "temperature_2m,is_day,weather_code", + daily: "weather_code,temperature_2m_max,temperature_2m_min", }, ttl: ttlForWidgetType("weather"), } ); - const values = axiosResponse.data; + const { current, daily } = axiosResponse.data; return { current: { - temperature: _.round(values.current.temp), - condition: values.current.weather?.[0], + temperature: _.round(current.temperature_2m), + condition: getCondition( + current.weather_code, + current.is_day === 1 ? "day" : "night" + ), }, - forecast: values.daily.splice(0, 5).map((daily: any) => ({ - date: daily.dt, - temperatureMin: _.round(daily.temp.min), - temperatureMax: _.round(daily.temp.max), - condition: daily.weather?.[0], - })), + forecast: getForecast(daily), }; } }