mirror of
https://github.com/MichMich/MagicMirror.git
synced 2026-06-04 02:10:43 +00:00
fix(weather): restore OpenWeatherMap v2.5 support (#4101)
After the big weather refactor (#4032), OpenWeatherMap was effectively hard-wired to One Call v3. One Call 2.5 is deprecated and no longer available, so it looked like v2.5 support was effectively over — but the classic `/weather` and `/forecast` endpoints were never actually dropped. This restores support for those. Fixes #4100. ## What this PR does - handles OpenWeatherMap responses by endpoint again (`/onecall`, `/weather`, `/forecast`) - restores v2.5 current and forecast support (including hourly via 3-hour forecast slots) - filters outdated hourly entries centrally while keeping the current hour visible (if available) ## Screenshot <img width="768" height="481" alt="bildo" src="https://github.com/user-attachments/assets/9bce3531-3731-4fd7-b41e-e20603afa725" />
This commit is contained in:
committed by
GitHub
parent
2e97e29ab5
commit
22a58d4ae5
@@ -96,28 +96,41 @@ class OpenWeatherMapProvider {
|
||||
|
||||
#handleResponse (data) {
|
||||
try {
|
||||
// Set location name from timezone
|
||||
if (data.timezone) {
|
||||
this.locationName = data.timezone;
|
||||
}
|
||||
|
||||
let weatherData;
|
||||
const onecallData = this.#generateWeatherObjectsFromOnecall(data);
|
||||
|
||||
switch (this.config.type) {
|
||||
case "current":
|
||||
weatherData = onecallData.current;
|
||||
break;
|
||||
case "forecast":
|
||||
case "daily":
|
||||
weatherData = onecallData.days;
|
||||
break;
|
||||
case "hourly":
|
||||
weatherData = onecallData.hours;
|
||||
break;
|
||||
default:
|
||||
Log.error(`[openweathermap] Unknown type: ${this.config.type}`);
|
||||
throw new Error(`Unknown weather type: ${this.config.type}`);
|
||||
if (this.config.weatherEndpoint === "/onecall") {
|
||||
// One Call API (v3.0)
|
||||
if (data.timezone) {
|
||||
this.locationName = data.timezone;
|
||||
}
|
||||
|
||||
const onecallData = this.#generateWeatherObjectsFromOnecall(data);
|
||||
|
||||
switch (this.config.type) {
|
||||
case "current":
|
||||
weatherData = onecallData.current;
|
||||
break;
|
||||
case "forecast":
|
||||
case "daily":
|
||||
weatherData = onecallData.days;
|
||||
break;
|
||||
case "hourly":
|
||||
weatherData = onecallData.hours;
|
||||
break;
|
||||
default:
|
||||
Log.error(`[openweathermap] Unknown type: ${this.config.type}`);
|
||||
throw new Error(`Unknown weather type: ${this.config.type}`);
|
||||
}
|
||||
} else if (this.config.weatherEndpoint === "/weather") {
|
||||
// Current weather endpoint (API v2.5)
|
||||
weatherData = this.#generateWeatherObjectFromCurrentWeather(data);
|
||||
} else if (this.config.weatherEndpoint === "/forecast") {
|
||||
// 3-hourly forecast endpoint (API v2.5)
|
||||
weatherData = this.config.type === "hourly"
|
||||
? this.#generateHourlyWeatherObjectsFromForecast(data)
|
||||
: this.#generateDailyWeatherObjectsFromForecast(data);
|
||||
} else {
|
||||
throw new Error(`Unknown weather endpoint: ${this.config.weatherEndpoint}`);
|
||||
}
|
||||
|
||||
if (weatherData && this.onDataCallback) {
|
||||
@@ -134,6 +147,123 @@ class OpenWeatherMapProvider {
|
||||
}
|
||||
}
|
||||
|
||||
#generateWeatherObjectFromCurrentWeather (data) {
|
||||
const timezoneOffsetMinutes = (data.timezone ?? 0) / 60;
|
||||
|
||||
if (data.name && data.sys?.country) {
|
||||
this.locationName = `${data.name}, ${data.sys.country}`;
|
||||
} else if (data.name) {
|
||||
this.locationName = data.name;
|
||||
}
|
||||
|
||||
const weather = {};
|
||||
weather.date = weatherUtils.applyTimezoneOffset(new Date(data.dt * 1000), timezoneOffsetMinutes);
|
||||
weather.temperature = data.main.temp;
|
||||
weather.feelsLikeTemp = data.main.feels_like;
|
||||
weather.humidity = data.main.humidity;
|
||||
weather.windSpeed = data.wind.speed;
|
||||
weather.windFromDirection = data.wind.deg;
|
||||
weather.weatherType = weatherUtils.convertWeatherType(data.weather[0].icon);
|
||||
weather.sunrise = weatherUtils.applyTimezoneOffset(new Date(data.sys.sunrise * 1000), timezoneOffsetMinutes);
|
||||
weather.sunset = weatherUtils.applyTimezoneOffset(new Date(data.sys.sunset * 1000), timezoneOffsetMinutes);
|
||||
|
||||
return weather;
|
||||
}
|
||||
|
||||
#extractThreeHourPrecipitation (forecast) {
|
||||
const rain = Number.parseFloat(forecast.rain?.["3h"] ?? "") || 0;
|
||||
const snow = Number.parseFloat(forecast.snow?.["3h"] ?? "") || 0;
|
||||
const precipitationAmount = rain + snow;
|
||||
|
||||
return {
|
||||
rain,
|
||||
snow,
|
||||
precipitationAmount,
|
||||
hasPrecipitation: precipitationAmount > 0
|
||||
};
|
||||
}
|
||||
|
||||
#generateHourlyWeatherObjectsFromForecast (data) {
|
||||
const timezoneOffsetSeconds = data.city?.timezone ?? 0;
|
||||
const timezoneOffsetMinutes = timezoneOffsetSeconds / 60;
|
||||
|
||||
if (data.city?.name && data.city?.country) {
|
||||
this.locationName = `${data.city.name}, ${data.city.country}`;
|
||||
}
|
||||
|
||||
return data.list.map((forecast) => {
|
||||
const weather = {};
|
||||
weather.date = weatherUtils.applyTimezoneOffset(new Date(forecast.dt * 1000), timezoneOffsetMinutes);
|
||||
weather.temperature = forecast.main.temp;
|
||||
weather.feelsLikeTemp = forecast.main.feels_like;
|
||||
weather.humidity = forecast.main.humidity;
|
||||
weather.windSpeed = forecast.wind.speed;
|
||||
weather.windFromDirection = forecast.wind.deg;
|
||||
weather.weatherType = weatherUtils.convertWeatherType(forecast.weather[0].icon);
|
||||
weather.precipitationProbability = forecast.pop !== undefined ? forecast.pop * 100 : undefined;
|
||||
|
||||
const precipitation = this.#extractThreeHourPrecipitation(forecast);
|
||||
if (precipitation.hasPrecipitation) {
|
||||
weather.rain = precipitation.rain;
|
||||
weather.snow = precipitation.snow;
|
||||
weather.precipitationAmount = precipitation.precipitationAmount;
|
||||
}
|
||||
|
||||
return weather;
|
||||
});
|
||||
}
|
||||
|
||||
#generateDailyWeatherObjectsFromForecast (data) {
|
||||
const timezoneOffsetSeconds = data.city?.timezone ?? 0;
|
||||
const timezoneOffsetMinutes = timezoneOffsetSeconds / 60;
|
||||
|
||||
if (data.city?.name && data.city?.country) {
|
||||
this.locationName = `${data.city.name}, ${data.city.country}`;
|
||||
}
|
||||
|
||||
const dayMap = new Map();
|
||||
|
||||
for (const forecast of data.list) {
|
||||
// Shift dt by timezone offset so UTC fields represent local time
|
||||
const localDate = new Date((forecast.dt + timezoneOffsetSeconds) * 1000);
|
||||
const dateKey = `${localDate.getUTCFullYear()}-${String(localDate.getUTCMonth() + 1).padStart(2, "0")}-${String(localDate.getUTCDate()).padStart(2, "0")}`;
|
||||
|
||||
if (!dayMap.has(dateKey)) {
|
||||
dayMap.set(dateKey, {
|
||||
date: weatherUtils.applyTimezoneOffset(new Date(forecast.dt * 1000), timezoneOffsetMinutes),
|
||||
minTemps: [],
|
||||
maxTemps: [],
|
||||
rain: 0,
|
||||
snow: 0,
|
||||
weatherType: weatherUtils.convertWeatherType(forecast.weather[0].icon)
|
||||
});
|
||||
}
|
||||
|
||||
const day = dayMap.get(dateKey);
|
||||
day.minTemps.push(forecast.main.temp_min);
|
||||
day.maxTemps.push(forecast.main.temp_max);
|
||||
|
||||
const hour = localDate.getUTCHours();
|
||||
if (hour >= 8 && hour <= 17) {
|
||||
day.weatherType = weatherUtils.convertWeatherType(forecast.weather[0].icon);
|
||||
}
|
||||
|
||||
const precipitation = this.#extractThreeHourPrecipitation(forecast);
|
||||
day.rain += precipitation.rain;
|
||||
day.snow += precipitation.snow;
|
||||
}
|
||||
|
||||
return Array.from(dayMap.values()).map((day) => ({
|
||||
date: day.date,
|
||||
minTemperature: Math.min(...day.minTemps),
|
||||
maxTemperature: Math.max(...day.maxTemps),
|
||||
weatherType: day.weatherType,
|
||||
rain: day.rain,
|
||||
snow: day.snow,
|
||||
precipitationAmount: day.rain + day.snow
|
||||
}));
|
||||
}
|
||||
|
||||
#generateWeatherObjectsFromOnecall (data) {
|
||||
let precip;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ Module.register("weather", {
|
||||
defaults: {
|
||||
weatherProvider: "openweathermap",
|
||||
roundTemp: false,
|
||||
type: "current", // current, forecast, daily (equivalent to forecast), hourly (only with OpenWeatherMap /onecall endpoint)
|
||||
type: "current", // current, forecast, daily (equivalent to forecast), hourly
|
||||
lang: config.language,
|
||||
units: config.units,
|
||||
tempUnits: config.units,
|
||||
@@ -242,7 +242,23 @@ Module.register("weather", {
|
||||
|
||||
// Add all the data to the template.
|
||||
getTemplateData () {
|
||||
const hourlyData = this.weatherHourlyArray?.filter((e, i) => (i + 1) % this.config.hourlyForecastIncrements === this.config.hourlyForecastIncrements - 1);
|
||||
const now = new Date();
|
||||
// Filter out past entries, but keep the current hour (e.g. show 0:00 at 0:10).
|
||||
// This ensures consistent behavior across all providers, regardless of whether
|
||||
// a provider filters past entries itself.
|
||||
const startOfHour = new Date(now);
|
||||
startOfHour.setMinutes(0, 0, 0);
|
||||
const upcomingHourlyData = this.weatherHourlyArray
|
||||
?.filter((entry) => entry.date?.valueOf() >= startOfHour.getTime());
|
||||
const hourlySourceData = upcomingHourlyData?.length ? upcomingHourlyData : this.weatherHourlyArray;
|
||||
|
||||
const increment = this.config.hourlyForecastIncrements;
|
||||
const keepByConfiguredIncrement = (_entry, index) => {
|
||||
// Keep the existing offset behavior of hourlyForecastIncrements.
|
||||
return (index + 1) % increment === increment - 1;
|
||||
};
|
||||
|
||||
const hourlyData = hourlySourceData?.filter(keepByConfiguredIncrement);
|
||||
|
||||
return {
|
||||
config: this.config,
|
||||
|
||||
28
tests/mocks/weather_owm_current.json
Normal file
28
tests/mocks/weather_owm_current.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"coord": { "lon": 11.58, "lat": 48.14 },
|
||||
"weather": [{ "id": 804, "main": "Clouds", "description": "overcast clouds", "icon": "04d" }],
|
||||
"base": "stations",
|
||||
"main": {
|
||||
"temp": -0.27,
|
||||
"feels_like": -3.9,
|
||||
"temp_min": -1.0,
|
||||
"temp_max": 0.5,
|
||||
"pressure": 1018,
|
||||
"humidity": 54
|
||||
},
|
||||
"visibility": 10000,
|
||||
"wind": { "speed": 3.09, "deg": 220 },
|
||||
"clouds": { "all": 100 },
|
||||
"dt": 1744200000,
|
||||
"sys": {
|
||||
"type": 2,
|
||||
"id": 2002112,
|
||||
"country": "DE",
|
||||
"sunrise": 1744170000,
|
||||
"sunset": 1744218000
|
||||
},
|
||||
"timezone": 7200,
|
||||
"id": 2867714,
|
||||
"name": "Munich",
|
||||
"cod": 200
|
||||
}
|
||||
180
tests/mocks/weather_owm_forecast.json
Normal file
180
tests/mocks/weather_owm_forecast.json
Normal file
@@ -0,0 +1,180 @@
|
||||
{
|
||||
"cod": "200",
|
||||
"message": 0,
|
||||
"cnt": 16,
|
||||
"list": [
|
||||
{
|
||||
"dt": 1744156800,
|
||||
"main": { "temp": -1.0, "feels_like": -4.0, "temp_min": -1.5, "temp_max": -0.5, "pressure": 1018, "humidity": 60 },
|
||||
"weather": [{ "id": 804, "main": "Clouds", "description": "overcast clouds", "icon": "04n" }],
|
||||
"clouds": { "all": 100 },
|
||||
"wind": { "speed": 3.0, "deg": 210 },
|
||||
"pop": 0.2,
|
||||
"sys": { "pod": "n" },
|
||||
"dt_txt": "2026-04-09 00:00:00"
|
||||
},
|
||||
{
|
||||
"dt": 1744167600,
|
||||
"main": { "temp": -1.2, "feels_like": -4.2, "temp_min": -1.5, "temp_max": -0.9, "pressure": 1018, "humidity": 62 },
|
||||
"weather": [{ "id": 804, "main": "Clouds", "description": "overcast clouds", "icon": "04n" }],
|
||||
"clouds": { "all": 100 },
|
||||
"wind": { "speed": 3.1, "deg": 215 },
|
||||
"pop": 0.2,
|
||||
"sys": { "pod": "n" },
|
||||
"dt_txt": "2026-04-09 03:00:00"
|
||||
},
|
||||
{
|
||||
"dt": 1744178400,
|
||||
"main": { "temp": -0.5, "feels_like": -3.5, "temp_min": -1.0, "temp_max": 0.0, "pressure": 1019, "humidity": 58 },
|
||||
"weather": [{ "id": 804, "main": "Clouds", "description": "overcast clouds", "icon": "04d" }],
|
||||
"clouds": { "all": 95 },
|
||||
"wind": { "speed": 2.8, "deg": 220 },
|
||||
"pop": 0.3,
|
||||
"sys": { "pod": "d" },
|
||||
"dt_txt": "2026-04-09 06:00:00"
|
||||
},
|
||||
{
|
||||
"dt": 1744189200,
|
||||
"main": { "temp": 1.0, "feels_like": -2.0, "temp_min": 0.5, "temp_max": 1.5, "pressure": 1019, "humidity": 55 },
|
||||
"weather": [{ "id": 500, "main": "Rain", "description": "light rain", "icon": "10d" }],
|
||||
"clouds": { "all": 90 },
|
||||
"wind": { "speed": 2.5, "deg": 225 },
|
||||
"pop": 0.8,
|
||||
"rain": { "3h": 0.6 },
|
||||
"sys": { "pod": "d" },
|
||||
"dt_txt": "2026-04-09 09:00:00"
|
||||
},
|
||||
{
|
||||
"dt": 1744200000,
|
||||
"main": { "temp": 2.0, "feels_like": -1.0, "temp_min": 1.5, "temp_max": 2.5, "pressure": 1018, "humidity": 52 },
|
||||
"weather": [{ "id": 500, "main": "Rain", "description": "light rain", "icon": "10d" }],
|
||||
"clouds": { "all": 88 },
|
||||
"wind": { "speed": 2.4, "deg": 230 },
|
||||
"pop": 0.9,
|
||||
"rain": { "3h": 0.6 },
|
||||
"sys": { "pod": "d" },
|
||||
"dt_txt": "2026-04-09 12:00:00"
|
||||
},
|
||||
{
|
||||
"dt": 1744210800,
|
||||
"main": { "temp": 1.5, "feels_like": -1.5, "temp_min": 1.0, "temp_max": 2.0, "pressure": 1018, "humidity": 54 },
|
||||
"weather": [{ "id": 500, "main": "Rain", "description": "light rain", "icon": "10d" }],
|
||||
"clouds": { "all": 90 },
|
||||
"wind": { "speed": 2.6, "deg": 228 },
|
||||
"pop": 0.8,
|
||||
"sys": { "pod": "d" },
|
||||
"dt_txt": "2026-04-09 15:00:00"
|
||||
},
|
||||
{
|
||||
"dt": 1744221600,
|
||||
"main": { "temp": 0.8, "feels_like": -2.2, "temp_min": 0.5, "temp_max": 1.2, "pressure": 1018, "humidity": 57 },
|
||||
"weather": [{ "id": 500, "main": "Rain", "description": "light rain", "icon": "10d" }],
|
||||
"clouds": { "all": 92 },
|
||||
"wind": { "speed": 2.7, "deg": 222 },
|
||||
"pop": 0.6,
|
||||
"sys": { "pod": "d" },
|
||||
"dt_txt": "2026-04-09 18:00:00"
|
||||
},
|
||||
{
|
||||
"dt": 1744232400,
|
||||
"main": { "temp": -0.2, "feels_like": -3.2, "temp_min": -0.5, "temp_max": 0.1, "pressure": 1019, "humidity": 60 },
|
||||
"weather": [{ "id": 804, "main": "Clouds", "description": "overcast clouds", "icon": "04n" }],
|
||||
"clouds": { "all": 95 },
|
||||
"wind": { "speed": 2.9, "deg": 218 },
|
||||
"pop": 0.3,
|
||||
"sys": { "pod": "n" },
|
||||
"dt_txt": "2026-04-09 21:00:00"
|
||||
},
|
||||
{
|
||||
"dt": 1744243200,
|
||||
"main": { "temp": 0.5, "feels_like": -2.5, "temp_min": 0.0, "temp_max": 1.0, "pressure": 1020, "humidity": 58 },
|
||||
"weather": [{ "id": 804, "main": "Clouds", "description": "overcast clouds", "icon": "04n" }],
|
||||
"clouds": { "all": 85 },
|
||||
"wind": { "speed": 2.5, "deg": 200 },
|
||||
"pop": 0.1,
|
||||
"sys": { "pod": "n" },
|
||||
"dt_txt": "2026-04-10 00:00:00"
|
||||
},
|
||||
{
|
||||
"dt": 1744254000,
|
||||
"main": { "temp": 1.0, "feels_like": -2.0, "temp_min": 0.5, "temp_max": 1.5, "pressure": 1021, "humidity": 56 },
|
||||
"weather": [{ "id": 804, "main": "Clouds", "description": "overcast clouds", "icon": "04n" }],
|
||||
"clouds": { "all": 80 },
|
||||
"wind": { "speed": 2.3, "deg": 205 },
|
||||
"pop": 0.1,
|
||||
"sys": { "pod": "n" },
|
||||
"dt_txt": "2026-04-10 03:00:00"
|
||||
},
|
||||
{
|
||||
"dt": 1744264800,
|
||||
"main": { "temp": 2.0, "feels_like": -1.0, "temp_min": 1.5, "temp_max": 2.5, "pressure": 1021, "humidity": 53 },
|
||||
"weather": [{ "id": 804, "main": "Clouds", "description": "overcast clouds", "icon": "04d" }],
|
||||
"clouds": { "all": 75 },
|
||||
"wind": { "speed": 2.1, "deg": 210 },
|
||||
"pop": 0.1,
|
||||
"sys": { "pod": "d" },
|
||||
"dt_txt": "2026-04-10 06:00:00"
|
||||
},
|
||||
{
|
||||
"dt": 1744275600,
|
||||
"main": { "temp": 3.5, "feels_like": 0.5, "temp_min": 3.0, "temp_max": 4.0, "pressure": 1020, "humidity": 50 },
|
||||
"weather": [{ "id": 520, "main": "Rain", "description": "light shower rain", "icon": "09d" }],
|
||||
"clouds": { "all": 70 },
|
||||
"wind": { "speed": 2.0, "deg": 215 },
|
||||
"pop": 0.5,
|
||||
"snow": { "3h": 0.5 },
|
||||
"sys": { "pod": "d" },
|
||||
"dt_txt": "2026-04-10 09:00:00"
|
||||
},
|
||||
{
|
||||
"dt": 1744286400,
|
||||
"main": { "temp": 5.0, "feels_like": 2.0, "temp_min": 4.5, "temp_max": 5.5, "pressure": 1019, "humidity": 48 },
|
||||
"weather": [{ "id": 520, "main": "Rain", "description": "light shower rain", "icon": "09d" }],
|
||||
"clouds": { "all": 65 },
|
||||
"wind": { "speed": 1.9, "deg": 220 },
|
||||
"pop": 0.4,
|
||||
"sys": { "pod": "d" },
|
||||
"dt_txt": "2026-04-10 12:00:00"
|
||||
},
|
||||
{
|
||||
"dt": 1744297200,
|
||||
"main": { "temp": 4.5, "feels_like": 1.5, "temp_min": 4.0, "temp_max": 5.0, "pressure": 1019, "humidity": 50 },
|
||||
"weather": [{ "id": 520, "main": "Rain", "description": "light shower rain", "icon": "09d" }],
|
||||
"clouds": { "all": 68 },
|
||||
"wind": { "speed": 2.0, "deg": 218 },
|
||||
"pop": 0.4,
|
||||
"sys": { "pod": "d" },
|
||||
"dt_txt": "2026-04-10 15:00:00"
|
||||
},
|
||||
{
|
||||
"dt": 1744308000,
|
||||
"main": { "temp": 3.0, "feels_like": 0.0, "temp_min": 2.5, "temp_max": 3.5, "pressure": 1019, "humidity": 53 },
|
||||
"weather": [{ "id": 520, "main": "Rain", "description": "light shower rain", "icon": "09d" }],
|
||||
"clouds": { "all": 72 },
|
||||
"wind": { "speed": 2.1, "deg": 212 },
|
||||
"pop": 0.3,
|
||||
"sys": { "pod": "d" },
|
||||
"dt_txt": "2026-04-10 18:00:00"
|
||||
},
|
||||
{
|
||||
"dt": 1744318800,
|
||||
"main": { "temp": 1.5, "feels_like": -1.5, "temp_min": 1.0, "temp_max": 2.0, "pressure": 1020, "humidity": 56 },
|
||||
"weather": [{ "id": 804, "main": "Clouds", "description": "overcast clouds", "icon": "04n" }],
|
||||
"clouds": { "all": 80 },
|
||||
"wind": { "speed": 2.2, "deg": 208 },
|
||||
"pop": 0.2,
|
||||
"sys": { "pod": "n" },
|
||||
"dt_txt": "2026-04-10 21:00:00"
|
||||
}
|
||||
],
|
||||
"city": {
|
||||
"id": 2867714,
|
||||
"name": "Munich",
|
||||
"coord": { "lat": 48.14, "lon": 11.58 },
|
||||
"country": "DE",
|
||||
"population": 1260391,
|
||||
"timezone": 0,
|
||||
"sunrise": 1744170000,
|
||||
"sunset": 1744218000
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ import { setupServer } from "msw/node";
|
||||
import { describe, it, expect, vi, beforeAll, afterAll, afterEach } from "vitest";
|
||||
|
||||
import onecallData from "../../../../../mocks/weather_owm_onecall.json" with { type: "json" };
|
||||
import currentData from "../../../../../mocks/weather_owm_current.json" with { type: "json" };
|
||||
import forecastData from "../../../../../mocks/weather_owm_forecast.json" with { type: "json" };
|
||||
|
||||
let server;
|
||||
|
||||
@@ -232,4 +234,321 @@ describe("OpenWeatherMapProvider", () => {
|
||||
expect(provider.locationName).toBe("America/New_York");
|
||||
});
|
||||
});
|
||||
|
||||
describe("API v2.5 - Current Weather (/weather endpoint)", () => {
|
||||
it("should parse current weather from /weather endpoint", async () => {
|
||||
const provider = new OpenWeatherMapProvider({
|
||||
lat: 48.14,
|
||||
lon: 11.58,
|
||||
apiKey: "test-key",
|
||||
apiVersion: "2.5",
|
||||
weatherEndpoint: "/weather",
|
||||
type: "current"
|
||||
});
|
||||
|
||||
const dataPromise = new Promise((resolve) => {
|
||||
provider.setCallbacks(resolve, vi.fn());
|
||||
});
|
||||
|
||||
server.use(
|
||||
http.get("https://api.openweathermap.org/data/2.5/weather", () => {
|
||||
return HttpResponse.json(currentData);
|
||||
})
|
||||
);
|
||||
|
||||
await provider.initialize();
|
||||
provider.start();
|
||||
|
||||
const result = await dataPromise;
|
||||
|
||||
expect(result.temperature).toBe(-0.27);
|
||||
expect(result.feelsLikeTemp).toBe(-3.9);
|
||||
expect(result.humidity).toBe(54);
|
||||
expect(result.windSpeed).toBe(3.09);
|
||||
expect(result.windFromDirection).toBe(220);
|
||||
expect(result.weatherType).toBe("cloudy-windy");
|
||||
expect(result.sunrise).toBeInstanceOf(Date);
|
||||
expect(result.sunset).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it("should set location name from city name and country", async () => {
|
||||
const provider = new OpenWeatherMapProvider({
|
||||
lat: 48.14,
|
||||
lon: 11.58,
|
||||
apiKey: "test-key",
|
||||
apiVersion: "2.5",
|
||||
weatherEndpoint: "/weather",
|
||||
type: "current"
|
||||
});
|
||||
|
||||
const dataPromise = new Promise((resolve) => {
|
||||
provider.setCallbacks(resolve, vi.fn());
|
||||
});
|
||||
|
||||
server.use(
|
||||
http.get("https://api.openweathermap.org/data/2.5/weather", () => {
|
||||
return HttpResponse.json(currentData);
|
||||
})
|
||||
);
|
||||
|
||||
await provider.initialize();
|
||||
provider.start();
|
||||
|
||||
await dataPromise;
|
||||
|
||||
expect(provider.locationName).toBe("Munich, DE");
|
||||
});
|
||||
});
|
||||
|
||||
describe("API v2.5 - Forecast (/forecast endpoint)", () => {
|
||||
it("should parse /forecast endpoint into daily grouped forecast", async () => {
|
||||
const provider = new OpenWeatherMapProvider({
|
||||
lat: 48.14,
|
||||
lon: 11.58,
|
||||
apiKey: "test-key",
|
||||
apiVersion: "2.5",
|
||||
weatherEndpoint: "/forecast",
|
||||
type: "forecast"
|
||||
});
|
||||
|
||||
const dataPromise = new Promise((resolve) => {
|
||||
provider.setCallbacks(resolve, vi.fn());
|
||||
});
|
||||
|
||||
server.use(
|
||||
http.get("https://api.openweathermap.org/data/2.5/forecast", () => {
|
||||
return HttpResponse.json(forecastData);
|
||||
})
|
||||
);
|
||||
|
||||
await provider.initialize();
|
||||
provider.start();
|
||||
|
||||
const result = await dataPromise;
|
||||
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("should correctly aggregate min/max temperatures per day", async () => {
|
||||
const provider = new OpenWeatherMapProvider({
|
||||
lat: 48.14,
|
||||
lon: 11.58,
|
||||
apiKey: "test-key",
|
||||
apiVersion: "2.5",
|
||||
weatherEndpoint: "/forecast",
|
||||
type: "forecast"
|
||||
});
|
||||
|
||||
const dataPromise = new Promise((resolve) => {
|
||||
provider.setCallbacks(resolve, vi.fn());
|
||||
});
|
||||
|
||||
server.use(
|
||||
http.get("https://api.openweathermap.org/data/2.5/forecast", () => {
|
||||
return HttpResponse.json(forecastData);
|
||||
})
|
||||
);
|
||||
|
||||
await provider.initialize();
|
||||
provider.start();
|
||||
|
||||
const result = await dataPromise;
|
||||
|
||||
// Day 1: temp_min values: -1.5, -1.5, -1.0, 0.5, 1.5, 1.0, 0.5, -0.5 → min=-1.5
|
||||
expect(result[0].minTemperature).toBe(-1.5);
|
||||
// Day 1: temp_max values: -0.5, -0.9, 0.0, 1.5, 2.5, 2.0, 1.2, 0.1 → max=2.5
|
||||
expect(result[0].maxTemperature).toBe(2.5);
|
||||
// Day 2: temp_min values: 0.0, 0.5, 1.5, 3.0, 4.5, 4.0, 2.5, 1.0 → min=0.0
|
||||
expect(result[1].minTemperature).toBe(0.0);
|
||||
// Day 2: temp_max values: 1.0, 1.5, 2.5, 4.0, 5.5, 5.0, 3.5, 2.0 → max=5.5
|
||||
expect(result[1].maxTemperature).toBe(5.5);
|
||||
});
|
||||
|
||||
it("should pick daytime weather type (8-17h)", async () => {
|
||||
const provider = new OpenWeatherMapProvider({
|
||||
lat: 48.14,
|
||||
lon: 11.58,
|
||||
apiKey: "test-key",
|
||||
apiVersion: "2.5",
|
||||
weatherEndpoint: "/forecast",
|
||||
type: "forecast"
|
||||
});
|
||||
|
||||
const dataPromise = new Promise((resolve) => {
|
||||
provider.setCallbacks(resolve, vi.fn());
|
||||
});
|
||||
|
||||
server.use(
|
||||
http.get("https://api.openweathermap.org/data/2.5/forecast", () => {
|
||||
return HttpResponse.json(forecastData);
|
||||
})
|
||||
);
|
||||
|
||||
await provider.initialize();
|
||||
provider.start();
|
||||
|
||||
const result = await dataPromise;
|
||||
|
||||
// Day 1 daytime entries have icon "10d" → "rain"
|
||||
expect(result[0].weatherType).toBe("rain");
|
||||
// Day 2 daytime entries have icon "09d" → "showers"
|
||||
expect(result[1].weatherType).toBe("showers");
|
||||
});
|
||||
|
||||
it("should accumulate precipitation per day", async () => {
|
||||
const provider = new OpenWeatherMapProvider({
|
||||
lat: 48.14,
|
||||
lon: 11.58,
|
||||
apiKey: "test-key",
|
||||
apiVersion: "2.5",
|
||||
weatherEndpoint: "/forecast",
|
||||
type: "forecast"
|
||||
});
|
||||
|
||||
const dataPromise = new Promise((resolve) => {
|
||||
provider.setCallbacks(resolve, vi.fn());
|
||||
});
|
||||
|
||||
server.use(
|
||||
http.get("https://api.openweathermap.org/data/2.5/forecast", () => {
|
||||
return HttpResponse.json(forecastData);
|
||||
})
|
||||
);
|
||||
|
||||
await provider.initialize();
|
||||
provider.start();
|
||||
|
||||
const result = await dataPromise;
|
||||
|
||||
// Day 1: two rain entries of 0.6 each = 1.2
|
||||
expect(result[0].rain).toBeCloseTo(1.2);
|
||||
expect(result[0].precipitationAmount).toBeCloseTo(1.2);
|
||||
// Day 2: one snow entry of 0.5
|
||||
expect(result[1].snow).toBeCloseTo(0.5);
|
||||
expect(result[1].precipitationAmount).toBeCloseTo(0.5);
|
||||
});
|
||||
|
||||
it("should set location name from city in forecast response", async () => {
|
||||
const provider = new OpenWeatherMapProvider({
|
||||
lat: 48.14,
|
||||
lon: 11.58,
|
||||
apiKey: "test-key",
|
||||
apiVersion: "2.5",
|
||||
weatherEndpoint: "/forecast",
|
||||
type: "forecast"
|
||||
});
|
||||
|
||||
const dataPromise = new Promise((resolve) => {
|
||||
provider.setCallbacks(resolve, vi.fn());
|
||||
});
|
||||
|
||||
server.use(
|
||||
http.get("https://api.openweathermap.org/data/2.5/forecast", () => {
|
||||
return HttpResponse.json(forecastData);
|
||||
})
|
||||
);
|
||||
|
||||
await provider.initialize();
|
||||
provider.start();
|
||||
|
||||
await dataPromise;
|
||||
|
||||
expect(provider.locationName).toBe("Munich, DE");
|
||||
});
|
||||
});
|
||||
|
||||
describe("API v2.5 - Hourly (/forecast endpoint with type hourly)", () => {
|
||||
it("should return individual 3h entries instead of aggregating", async () => {
|
||||
const provider = new OpenWeatherMapProvider({
|
||||
lat: 48.14,
|
||||
lon: 11.58,
|
||||
apiKey: "test-key",
|
||||
apiVersion: "2.5",
|
||||
weatherEndpoint: "/forecast",
|
||||
type: "hourly"
|
||||
});
|
||||
|
||||
const dataPromise = new Promise((resolve) => {
|
||||
provider.setCallbacks(resolve, vi.fn());
|
||||
});
|
||||
|
||||
server.use(
|
||||
http.get("https://api.openweathermap.org/data/2.5/forecast", () => {
|
||||
return HttpResponse.json(forecastData);
|
||||
})
|
||||
);
|
||||
|
||||
await provider.initialize();
|
||||
provider.start();
|
||||
|
||||
const result = await dataPromise;
|
||||
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
expect(result).toHaveLength(forecastData.list.length);
|
||||
});
|
||||
|
||||
it("should map temperature and wind from each 3h slot", async () => {
|
||||
const provider = new OpenWeatherMapProvider({
|
||||
lat: 48.14,
|
||||
lon: 11.58,
|
||||
apiKey: "test-key",
|
||||
apiVersion: "2.5",
|
||||
weatherEndpoint: "/forecast",
|
||||
type: "hourly"
|
||||
});
|
||||
|
||||
const dataPromise = new Promise((resolve) => {
|
||||
provider.setCallbacks(resolve, vi.fn());
|
||||
});
|
||||
|
||||
server.use(
|
||||
http.get("https://api.openweathermap.org/data/2.5/forecast", () => {
|
||||
return HttpResponse.json(forecastData);
|
||||
})
|
||||
);
|
||||
|
||||
await provider.initialize();
|
||||
provider.start();
|
||||
|
||||
const result = await dataPromise;
|
||||
|
||||
expect(result[0].temperature).toBe(forecastData.list[0].main.temp);
|
||||
expect(result[0].windSpeed).toBe(forecastData.list[0].wind.speed);
|
||||
expect(result[0].precipitationProbability).toBe(forecastData.list[0].pop * 100);
|
||||
});
|
||||
|
||||
it("should include precipitation when present in a slot", async () => {
|
||||
const provider = new OpenWeatherMapProvider({
|
||||
lat: 48.14,
|
||||
lon: 11.58,
|
||||
apiKey: "test-key",
|
||||
apiVersion: "2.5",
|
||||
weatherEndpoint: "/forecast",
|
||||
type: "hourly"
|
||||
});
|
||||
|
||||
const dataPromise = new Promise((resolve) => {
|
||||
provider.setCallbacks(resolve, vi.fn());
|
||||
});
|
||||
|
||||
server.use(
|
||||
http.get("https://api.openweathermap.org/data/2.5/forecast", () => {
|
||||
return HttpResponse.json(forecastData);
|
||||
})
|
||||
);
|
||||
|
||||
await provider.initialize();
|
||||
provider.start();
|
||||
|
||||
const result = await dataPromise;
|
||||
|
||||
// Entry at index 3 has rain: { "3h": 0.6 }
|
||||
expect(result[3].rain).toBe(0.6);
|
||||
expect(result[3].precipitationAmount).toBe(0.6);
|
||||
// Entry at index 11 has snow: { "3h": 0.5 }
|
||||
expect(result[11].snow).toBe(0.5);
|
||||
expect(result[11].precipitationAmount).toBe(0.5);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user