mirror of
https://github.com/MichMich/MagicMirror.git
synced 2026-07-19 01:14:20 -07:00
feat(weather): add Buienradar provider (#4164)
This adds the weather provider Buienradar for the Netherlands and Belgium without requiring an API key. In MagicMirrorOrg/MagicMirror-Documentation#380 @plebcity shared his implementation for the old browser-side architecture. I used it as a reference to build this server-side implementation. ### Example screenshot <img width="969" height="578" alt="Ekrankopio de 2026-05-20 20-38-36" src="https://github.com/user-attachments/assets/56623ad8-7439-4047-abad-452ba2ebdcb2" /> ### Example config ```js { module: "weather", position: "top_left", header: "Buienradar - Current", config: { weatherProvider: "buienradar", type: "current", locationId: 2747891 } }, { module: "weather", position: "top_right", header: "Buienradar - Forecast", config: { weatherProvider: "buienradar", type: "forecast", locationId: 2747891 } }, { module: "weather", position: "bottom_left", header: "Buienradar - Hourly", config: { weatherProvider: "buienradar", type: "hourly", locationId: 2747891 } }, ``` ---- When this is accepted and merged I'll update the weather docs.
This commit is contained in:
committed by
GitHub
parent
9a3538de01
commit
fd687bf5fe
+8
-7
@@ -13,9 +13,9 @@
|
||||
"armv",
|
||||
"ashishtank",
|
||||
"autoplay",
|
||||
"Autorestart",
|
||||
"avghumidity",
|
||||
"avgtemp",
|
||||
"Autorestart",
|
||||
"beada",
|
||||
"Behaviour",
|
||||
"Beschreibung",
|
||||
@@ -32,6 +32,7 @@
|
||||
"btoconnor",
|
||||
"bughaver",
|
||||
"bugsounet",
|
||||
"buienradar",
|
||||
"buxxi",
|
||||
"byday",
|
||||
"calcage",
|
||||
@@ -183,8 +184,8 @@
|
||||
"Lightspeed",
|
||||
"loadingcircle",
|
||||
"locationforecast",
|
||||
"logg",
|
||||
"lockstring",
|
||||
"logg",
|
||||
"lstrip",
|
||||
"Luciella",
|
||||
"luxon",
|
||||
@@ -201,8 +202,8 @@
|
||||
"Meteo",
|
||||
"michaelteeuw",
|
||||
"michmich",
|
||||
"mintemp",
|
||||
"Midori",
|
||||
"mintemp",
|
||||
"mirontoli",
|
||||
"MISSINGLANG",
|
||||
"mixasgr",
|
||||
@@ -220,8 +221,8 @@
|
||||
"NEWSFEED",
|
||||
"newsfeedfetcher",
|
||||
"newsfetcher",
|
||||
"newyear",
|
||||
"newsitems",
|
||||
"newyear",
|
||||
"nextdaysrelative",
|
||||
"nfogal",
|
||||
"njwilliams",
|
||||
@@ -260,8 +261,8 @@
|
||||
"Reis",
|
||||
"rejas",
|
||||
"relativehumidity",
|
||||
"resultstring",
|
||||
"Resig",
|
||||
"resultstring",
|
||||
"roboto",
|
||||
"rohitdharavath",
|
||||
"Rosso",
|
||||
@@ -301,8 +302,8 @@
|
||||
"Teil",
|
||||
"TESTMODE",
|
||||
"testpass",
|
||||
"testuser",
|
||||
"teststring",
|
||||
"testuser",
|
||||
"thomasrockhu",
|
||||
"thumbslider",
|
||||
"timeformat",
|
||||
@@ -323,12 +324,12 @@
|
||||
"updatenotification",
|
||||
"uxdt",
|
||||
"Vaice",
|
||||
"VCALENDAR",
|
||||
"veeck",
|
||||
"verjaardag",
|
||||
"VEVENT",
|
||||
"vgtu",
|
||||
"Vitest",
|
||||
"VCALENDAR",
|
||||
"Voelt",
|
||||
"Vorberechnung",
|
||||
"vppencilsharpener",
|
||||
|
||||
@@ -0,0 +1,348 @@
|
||||
const Log = require("logger");
|
||||
const HTTPFetcher = require("#http_fetcher");
|
||||
|
||||
const BUIENRADAR_API_BASE = "https://forecast.buienradar.nl/2.0/forecast";
|
||||
const ERROR_TRANSLATION_KEY = "MODULE_ERROR_UNSPECIFIED";
|
||||
const TIMESTAMP_HAS_TIME_ZONE = /[zZ]|[+-]\d{2}:?\d{2}$/;
|
||||
|
||||
// Mapping from Buienradar icon code to weather-icons class names (https://erikflowers.github.io/weather-icons/).
|
||||
// Icon filenames use the Buienradar code: https://cdn.buienradar.nl/resources/images/icons/weather/30x30/a.png
|
||||
const WEATHER_ICON_MAP = {
|
||||
a: "day-sunny",
|
||||
aa: "night-clear",
|
||||
b: "day-cloudy",
|
||||
bb: "night-alt-cloudy",
|
||||
c: "cloudy",
|
||||
cc: "cloudy",
|
||||
d: "day-fog",
|
||||
dd: "night-fog",
|
||||
f: "day-sprinkle",
|
||||
ff: "night-alt-sprinkle",
|
||||
g: "day-storm-showers",
|
||||
gg: "night-alt-storm-showers",
|
||||
h: "day-rain",
|
||||
hh: "night-alt-rain",
|
||||
i: "day-rain-mix",
|
||||
ii: "night-alt-rain-mix",
|
||||
j: "day-cloudy",
|
||||
jj: "night-alt-cloudy",
|
||||
k: "day-showers",
|
||||
kk: "night-alt-showers",
|
||||
l: "showers",
|
||||
ll: "showers",
|
||||
m: "sprinkle",
|
||||
mm: "sprinkle",
|
||||
n: "day-haze",
|
||||
nn: "night-fog",
|
||||
o: "day-cloudy",
|
||||
oo: "night-alt-cloudy",
|
||||
p: "cloudy",
|
||||
pp: "cloudy",
|
||||
q: "showers",
|
||||
qq: "showers",
|
||||
r: "day-cloudy",
|
||||
rr: "night-alt-cloudy",
|
||||
s: "thunderstorm",
|
||||
ss: "thunderstorm",
|
||||
t: "snow",
|
||||
tt: "snow",
|
||||
u: "day-snow",
|
||||
uu: "night-alt-snow",
|
||||
v: "snow",
|
||||
vv: "snow",
|
||||
w: "rain-mix",
|
||||
ww: "rain-mix"
|
||||
};
|
||||
|
||||
/**
|
||||
* Server-side weather provider for Buienradar
|
||||
* Netherlands/Belgium only, metric system, no API key required
|
||||
* see https://buienradar.nl
|
||||
*/
|
||||
class BuienradarProvider {
|
||||
constructor (config) {
|
||||
this.config = {
|
||||
apiBase: BUIENRADAR_API_BASE,
|
||||
locationId: null,
|
||||
type: "current",
|
||||
maxEntries: 5,
|
||||
updateInterval: 10 * 60 * 1000,
|
||||
...config
|
||||
};
|
||||
|
||||
this.locationName = null;
|
||||
this.fetcher = null;
|
||||
this.onDataCallback = null;
|
||||
this.onErrorCallback = null;
|
||||
}
|
||||
|
||||
initialize () {
|
||||
if (!this.config.locationId) {
|
||||
Log.error("[buienradar] No locationId configured");
|
||||
this.#sendErrorCallback("Buienradar locationId required. See https://www.buienradar.nl/overbuienradar/gratis-weerdata");
|
||||
return;
|
||||
}
|
||||
|
||||
this.#initializeFetcher();
|
||||
}
|
||||
|
||||
setCallbacks (onData, onError) {
|
||||
this.onDataCallback = onData;
|
||||
this.onErrorCallback = onError;
|
||||
}
|
||||
|
||||
start () {
|
||||
if (this.fetcher) {
|
||||
this.fetcher.startPeriodicFetch();
|
||||
}
|
||||
}
|
||||
|
||||
stop () {
|
||||
if (this.fetcher) {
|
||||
this.fetcher.clearTimer();
|
||||
}
|
||||
}
|
||||
|
||||
#initializeFetcher () {
|
||||
this.fetcher = new HTTPFetcher(this.#getUrl(), {
|
||||
reloadInterval: this.config.updateInterval,
|
||||
headers: { "Cache-Control": "no-cache" },
|
||||
logContext: "weatherprovider.buienradar"
|
||||
});
|
||||
|
||||
this.fetcher.on("response", async (response) => {
|
||||
try {
|
||||
const data = await response.json();
|
||||
this.#handleResponse(data);
|
||||
} catch (error) {
|
||||
Log.error("[buienradar] Failed to parse JSON:", error);
|
||||
this.#sendErrorCallback("Failed to parse API response");
|
||||
}
|
||||
});
|
||||
|
||||
this.fetcher.on("error", (errorInfo) => {
|
||||
if (this.onErrorCallback) {
|
||||
this.onErrorCallback(errorInfo);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#handleResponse (data) {
|
||||
try {
|
||||
if (!Array.isArray(data?.days) || data.days.length === 0) {
|
||||
throw new Error("Invalid API response");
|
||||
}
|
||||
|
||||
this.#setLocationName(data.location);
|
||||
|
||||
let weatherData;
|
||||
switch (this.config.type) {
|
||||
case "current":
|
||||
weatherData = this.#generateCurrentWeather(data.days[0]);
|
||||
break;
|
||||
case "forecast":
|
||||
case "daily":
|
||||
weatherData = this.#generateDailyForecast(data.days);
|
||||
break;
|
||||
case "hourly":
|
||||
weatherData = this.#generateHourlyForecast(data.days);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown weather type: ${this.config.type}`);
|
||||
}
|
||||
|
||||
if (this.onDataCallback && weatherData) {
|
||||
this.onDataCallback(weatherData);
|
||||
}
|
||||
} catch (error) {
|
||||
Log.error("[buienradar] Error processing weather data:", error);
|
||||
this.#sendErrorCallback(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
#sendErrorCallback (message) {
|
||||
if (this.onErrorCallback) {
|
||||
this.onErrorCallback({
|
||||
message,
|
||||
translationKey: ERROR_TRANSLATION_KEY
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#setLocationName (location) {
|
||||
if (location?.name) {
|
||||
this.locationName = location.name;
|
||||
}
|
||||
}
|
||||
|
||||
#generateCurrentWeather (day) {
|
||||
const closestHour = this.#getClosestHour(day.hours ?? []);
|
||||
const weather = this.#parseHour(closestHour);
|
||||
|
||||
const sunrise = this.#parseDate(day.sunrise);
|
||||
if (sunrise) weather.sunrise = sunrise;
|
||||
|
||||
const sunset = this.#parseDate(day.sunset);
|
||||
if (sunset) weather.sunset = sunset;
|
||||
|
||||
const minTemperature = this.#parseNumber(day.mintemp);
|
||||
if (minTemperature !== null) weather.minTemperature = minTemperature;
|
||||
|
||||
const maxTemperature = this.#parseNumber(day.maxtemp);
|
||||
if (maxTemperature !== null) weather.maxTemperature = maxTemperature;
|
||||
|
||||
return weather;
|
||||
}
|
||||
|
||||
#generateDailyForecast (days) {
|
||||
return days
|
||||
.slice(0, this.config.maxEntries)
|
||||
.map((day) => this.#parseDay(day));
|
||||
}
|
||||
|
||||
#generateHourlyForecast (days) {
|
||||
const hours = [];
|
||||
|
||||
for (const day of days) {
|
||||
for (const hour of day.hours ?? []) {
|
||||
hours.push(this.#parseHour(hour));
|
||||
if (hours.length >= this.config.maxEntries) {
|
||||
return hours;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hours;
|
||||
}
|
||||
|
||||
#parseDay (day) {
|
||||
const weather = {};
|
||||
|
||||
const date = this.#parseDate(day.date);
|
||||
if (date) weather.date = date;
|
||||
|
||||
const minTemperature = this.#parseNumber(day.mintemp);
|
||||
if (minTemperature !== null) weather.minTemperature = minTemperature;
|
||||
|
||||
const maxTemperature = this.#parseNumber(day.maxtemp);
|
||||
if (maxTemperature !== null) weather.maxTemperature = maxTemperature;
|
||||
|
||||
const humidity = this.#parseNumber(day.humidity);
|
||||
if (humidity !== null) weather.humidity = humidity;
|
||||
|
||||
const windSpeed = this.#parseNumber(day.windspeedms);
|
||||
if (windSpeed !== null) weather.windSpeed = windSpeed;
|
||||
|
||||
const windFromDirection = this.#parseNumber(day.winddirectiondegrees);
|
||||
if (windFromDirection !== null) weather.windFromDirection = windFromDirection;
|
||||
|
||||
this.#applyPrecipitation(weather, day);
|
||||
|
||||
const sunrise = this.#parseDate(day.sunrise);
|
||||
if (sunrise) weather.sunrise = sunrise;
|
||||
|
||||
const sunset = this.#parseDate(day.sunset);
|
||||
if (sunset) weather.sunset = sunset;
|
||||
|
||||
weather.weatherType = this.#convertWeatherType(day.iconcode);
|
||||
|
||||
return weather;
|
||||
}
|
||||
|
||||
#parseHour (hour) {
|
||||
const weather = {};
|
||||
|
||||
const date = this.#parseDate(hour.datetime);
|
||||
if (date) weather.date = date;
|
||||
|
||||
const temperature = this.#parseNumber(hour.temperature);
|
||||
if (temperature !== null) weather.temperature = temperature;
|
||||
|
||||
const feelsLikeTemp = this.#parseNumber(hour.feeltemperature);
|
||||
if (feelsLikeTemp !== null) weather.feelsLikeTemp = feelsLikeTemp;
|
||||
|
||||
const humidity = this.#parseNumber(hour.humidity);
|
||||
if (humidity !== null) weather.humidity = humidity;
|
||||
|
||||
const windSpeed = this.#parseNumber(hour.windspeedms);
|
||||
if (windSpeed !== null) weather.windSpeed = windSpeed;
|
||||
|
||||
const windFromDirection = this.#parseNumber(hour.winddirectiondegrees);
|
||||
if (windFromDirection !== null) weather.windFromDirection = windFromDirection;
|
||||
|
||||
this.#applyPrecipitation(weather, hour);
|
||||
|
||||
weather.weatherType = this.#convertWeatherType(hour.iconcode);
|
||||
|
||||
return weather;
|
||||
}
|
||||
|
||||
#applyPrecipitation (weather, source) {
|
||||
const precipitationAmount = this.#parseNumber(source.precipitationmm);
|
||||
if (precipitationAmount !== null) {
|
||||
weather.precipitationAmount = precipitationAmount;
|
||||
weather.precipitationUnits = "mm";
|
||||
}
|
||||
|
||||
const precipitationProbability = this.#parseNumber(source.precipitation);
|
||||
if (precipitationProbability !== null) {
|
||||
weather.precipitationProbability = precipitationProbability;
|
||||
}
|
||||
}
|
||||
|
||||
#getClosestHour (hours) {
|
||||
if (hours.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
let closest = hours[0];
|
||||
let closestDiff = Number.POSITIVE_INFINITY;
|
||||
|
||||
for (const hour of hours) {
|
||||
const date = this.#parseDate(hour.datetime);
|
||||
if (!date) continue;
|
||||
|
||||
const diff = Math.abs(date.getTime() - now);
|
||||
if (diff < closestDiff) {
|
||||
closestDiff = diff;
|
||||
closest = hour;
|
||||
}
|
||||
}
|
||||
|
||||
return closest;
|
||||
}
|
||||
|
||||
#getUrl () {
|
||||
const now = new Date();
|
||||
const year = now.getUTCFullYear();
|
||||
const month = `${now.getUTCMonth() + 1}`.padStart(2, "0");
|
||||
const day = `${now.getUTCDate()}`.padStart(2, "0");
|
||||
const hours = `${now.getUTCHours()}`.padStart(2, "0");
|
||||
const minutes = `${now.getUTCMinutes()}`.padStart(2, "0");
|
||||
const cacheBust = `${year}${month}${day}${hours}${minutes}`;
|
||||
const params = new URLSearchParams({ btc: cacheBust });
|
||||
|
||||
return `${this.config.apiBase}/${this.config.locationId}?${params}`;
|
||||
}
|
||||
|
||||
#parseDate (value) {
|
||||
if (!value) return null;
|
||||
|
||||
const text = `${value}`;
|
||||
const date = new Date(TIMESTAMP_HAS_TIME_ZONE.test(text) ? text : `${text}Z`);
|
||||
return Number.isNaN(date.getTime()) ? null : date;
|
||||
}
|
||||
|
||||
#parseNumber (value) {
|
||||
const number = parseFloat(value);
|
||||
return Number.isFinite(number) ? number : null;
|
||||
}
|
||||
|
||||
#convertWeatherType (icon) {
|
||||
if (!icon) return null;
|
||||
return WEATHER_ICON_MAP[`${icon}`.toLowerCase()] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BuienradarProvider;
|
||||
@@ -0,0 +1,243 @@
|
||||
/**
|
||||
* Buienradar Provider Tests
|
||||
*
|
||||
* Tests data parsing for current, forecast, and hourly weather types.
|
||||
* Buienradar covers NL/BE only, metric system, no API key required.
|
||||
*/
|
||||
import { http, HttpResponse } from "msw";
|
||||
import { setupServer } from "msw/node";
|
||||
import { describe, it, expect, vi, beforeAll, afterAll, afterEach } from "vitest";
|
||||
|
||||
const BUIENRADAR_URL = "https://forecast.buienradar.nl/2.0/forecast/*";
|
||||
|
||||
/**
|
||||
* Builds a stable Buienradar mock payload for parsing tests.
|
||||
* @returns {object} Buienradar forecast response fixture.
|
||||
*/
|
||||
function buildBuienradarResponse () {
|
||||
const today = "2100-01-01";
|
||||
const tomorrow = "2100-01-02";
|
||||
|
||||
const makeHour = (datetime, overrides = {}) => ({
|
||||
datetime,
|
||||
temperature: 5,
|
||||
feeltemperature: 3,
|
||||
humidity: 80,
|
||||
windspeedms: 4,
|
||||
winddirectiondegrees: 180,
|
||||
precipitationmm: 0.5,
|
||||
precipitation: 60,
|
||||
iconcode: "a",
|
||||
...overrides
|
||||
});
|
||||
|
||||
return {
|
||||
location: {
|
||||
name: "Rotterdam",
|
||||
lat: 51.92,
|
||||
lon: 4.48
|
||||
},
|
||||
days: [
|
||||
{
|
||||
date: today,
|
||||
sunrise: `${today}T07:00:00`,
|
||||
sunset: `${today}T17:00:00`,
|
||||
mintemp: 1,
|
||||
maxtemp: 8,
|
||||
humidity: 75,
|
||||
windspeedms: 5,
|
||||
winddirectiondegrees: 200,
|
||||
precipitationmm: 2.5,
|
||||
precipitation: 70,
|
||||
iconcode: "b",
|
||||
hours: [
|
||||
makeHour(`${today}T12:00:00`, { temperature: 6, iconcode: "a" }),
|
||||
makeHour(`${today}T13:00:00`, { temperature: 7, iconcode: "b" }),
|
||||
makeHour(`${today}T14:00:00`, { temperature: 7.5, iconcode: "c" })
|
||||
]
|
||||
},
|
||||
{
|
||||
date: tomorrow,
|
||||
sunrise: `${tomorrow}T07:01:00`,
|
||||
sunset: `${tomorrow}T17:02:00`,
|
||||
mintemp: 0,
|
||||
maxtemp: 6,
|
||||
windspeedms: 6,
|
||||
winddirectiondegrees: 220,
|
||||
precipitationmm: 1.0,
|
||||
precipitation: 40,
|
||||
iconcode: "f",
|
||||
hours: []
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
let server;
|
||||
|
||||
beforeAll(() => {
|
||||
server = setupServer();
|
||||
server.listen({ onUnhandledRequest: "bypass" });
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
server.close();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
describe("BuienradarProvider", () => {
|
||||
let BuienradarProvider;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module = await import("../../../../../../defaultmodules/weather/providers/buienradar");
|
||||
BuienradarProvider = module.default;
|
||||
});
|
||||
|
||||
describe("Constructor & Configuration", () => {
|
||||
it("should apply defaults and merge params", () => {
|
||||
const provider = new BuienradarProvider({ locationId: 6275, type: "hourly" });
|
||||
expect(provider.config.apiBase).toBe("https://forecast.buienradar.nl/2.0/forecast");
|
||||
expect(provider.config.locationId).toBe(6275);
|
||||
expect(provider.config.type).toBe("hourly");
|
||||
expect(provider.config.updateInterval).toBe(10 * 60 * 1000);
|
||||
});
|
||||
|
||||
it("should call error callback when locationId is missing", () => {
|
||||
const provider = new BuienradarProvider({});
|
||||
const onError = vi.fn();
|
||||
provider.setCallbacks(vi.fn(), onError);
|
||||
provider.initialize();
|
||||
expect(onError).toHaveBeenCalledWith(expect.objectContaining({
|
||||
translationKey: "MODULE_ERROR_UNSPECIFIED"
|
||||
}));
|
||||
expect(provider.fetcher).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Current Weather Parsing", () => {
|
||||
it("should parse current weather, set locationName, and merge day metadata", async () => {
|
||||
const provider = new BuienradarProvider({ locationId: 6275, type: "current" });
|
||||
|
||||
const dataPromise = new Promise((resolve, reject) => {
|
||||
provider.setCallbacks(resolve, reject);
|
||||
});
|
||||
|
||||
server.use(
|
||||
http.get(BUIENRADAR_URL, () => HttpResponse.json(buildBuienradarResponse()))
|
||||
);
|
||||
|
||||
provider.initialize();
|
||||
provider.start();
|
||||
|
||||
const result = await dataPromise;
|
||||
|
||||
expect(provider.locationName).toBe("Rotterdam");
|
||||
expect(typeof result.temperature).toBe("number");
|
||||
expect(result.humidity).toBe(80);
|
||||
expect(result.windSpeed).toBe(4);
|
||||
expect(result.windFromDirection).toBe(180);
|
||||
expect(result.minTemperature).toBe(1);
|
||||
expect(result.maxTemperature).toBe(8);
|
||||
expect(result.precipitationAmount).toBe(0.5);
|
||||
expect(result.precipitationProbability).toBe(60);
|
||||
expect(result.precipitationUnits).toBe("mm");
|
||||
expect(result.sunrise).toBeInstanceOf(Date);
|
||||
expect(result.sunset).toBeInstanceOf(Date);
|
||||
expect(typeof result.weatherType).toBe("string");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Forecast Parsing", () => {
|
||||
it("should parse daily forecast data", async () => {
|
||||
const provider = new BuienradarProvider({ locationId: 6275, type: "forecast", maxEntries: 2 });
|
||||
|
||||
const dataPromise = new Promise((resolve) => {
|
||||
provider.setCallbacks(resolve, vi.fn());
|
||||
});
|
||||
|
||||
server.use(
|
||||
http.get(BUIENRADAR_URL, () => HttpResponse.json(buildBuienradarResponse()))
|
||||
);
|
||||
|
||||
provider.initialize();
|
||||
provider.start();
|
||||
|
||||
const result = await dataPromise;
|
||||
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
expect(result).toHaveLength(2);
|
||||
|
||||
expect(result[0].minTemperature).toBe(1);
|
||||
expect(result[0].maxTemperature).toBe(8);
|
||||
expect(result[0].humidity).toBe(75);
|
||||
expect(result[0].windSpeed).toBe(5);
|
||||
expect(result[0].windFromDirection).toBe(200);
|
||||
expect(result[0].precipitationAmount).toBe(2.5);
|
||||
expect(result[0].precipitationProbability).toBe(70);
|
||||
expect(result[0].sunrise).toBeInstanceOf(Date);
|
||||
expect(result[0].sunset).toBeInstanceOf(Date);
|
||||
expect(result[0].weatherType).toBe("day-cloudy");
|
||||
|
||||
expect(result[1].minTemperature).toBe(0);
|
||||
expect(result[1].maxTemperature).toBe(6);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Hourly Parsing", () => {
|
||||
it("should parse hourly forecast data", async () => {
|
||||
const provider = new BuienradarProvider({ locationId: 6275, type: "hourly", maxEntries: 3 });
|
||||
|
||||
const dataPromise = new Promise((resolve) => {
|
||||
provider.setCallbacks(resolve, vi.fn());
|
||||
});
|
||||
|
||||
server.use(
|
||||
http.get(BUIENRADAR_URL, () => HttpResponse.json(buildBuienradarResponse()))
|
||||
);
|
||||
|
||||
provider.initialize();
|
||||
provider.start();
|
||||
|
||||
const result = await dataPromise;
|
||||
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result[0].temperature).toBe(6);
|
||||
expect(result[0].feelsLikeTemp).toBe(3);
|
||||
expect(result[0].humidity).toBe(80);
|
||||
expect(result[0].windSpeed).toBe(4);
|
||||
expect(result[0].windFromDirection).toBe(180);
|
||||
expect(result[0].precipitationAmount).toBe(0.5);
|
||||
expect(result[0].precipitationProbability).toBe(60);
|
||||
expect(result[0].weatherType).toBe("day-sunny");
|
||||
expect(result[0].date).toBeInstanceOf(Date);
|
||||
|
||||
expect(result[1].weatherType).toBe("day-cloudy");
|
||||
expect(result[2].weatherType).toBe("cloudy");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Error Handling", () => {
|
||||
it("should call error callback on invalid API response", async () => {
|
||||
const provider = new BuienradarProvider({ locationId: 6275, type: "current" });
|
||||
|
||||
const errorPromise = new Promise((resolve) => {
|
||||
provider.setCallbacks(vi.fn(), resolve);
|
||||
});
|
||||
|
||||
server.use(
|
||||
http.get(BUIENRADAR_URL, () => HttpResponse.json({ days: [] }))
|
||||
);
|
||||
|
||||
provider.initialize();
|
||||
provider.start();
|
||||
|
||||
const error = await errorPromise;
|
||||
expect(error).toHaveProperty("message");
|
||||
expect(error).toHaveProperty("translationKey");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user