mirror of
https://github.com/MichMich/MagicMirror.git
synced 2026-06-14 11:46:14 +00:00
When a server responds with 304 (nothing changed since last fetch), the response has no body. Several modules were trying to parse that empty body anyway - which either cleared their cached data or threw an exception. The result: a blank calendar, empty newsfeed, or missing weather data after the next refresh cycle. This was reported in the forum: https://forum.magicmirror.builders/topic/20250/calendar-events-broadcasting-nothing-showing The bug was "introduced" by #4120, which correctly started forwarding 304s to consumers - but not all were ready for it. ### Fix Skip parsing on 304 and keep the existing data as-is: - **calendar** - re-broadcasts cached events - **newsfeed** - re-broadcasts cached items - **buienradar, openmeteo, weatherflow, weathergov** - return early before calling `response.json()`
272 lines
6.8 KiB
JavaScript
272 lines
6.8 KiB
JavaScript
const Log = require("logger");
|
|
const HTTPFetcher = require("#http_fetcher");
|
|
|
|
class PirateweatherProvider {
|
|
constructor (config) {
|
|
this.config = {
|
|
apiBase: "https://api.pirateweather.net",
|
|
weatherEndpoint: "/forecast",
|
|
apiKey: "",
|
|
lat: 0,
|
|
lon: 0,
|
|
type: "current",
|
|
updateInterval: 10 * 60 * 1000,
|
|
lang: "en",
|
|
...config
|
|
};
|
|
this.fetcher = null;
|
|
this.onDataCallback = null;
|
|
this.onErrorCallback = null;
|
|
}
|
|
|
|
setCallbacks (onDataCallback, onErrorCallback) {
|
|
this.onDataCallback = onDataCallback;
|
|
this.onErrorCallback = onErrorCallback;
|
|
}
|
|
|
|
initialize () {
|
|
if (!this.config.apiKey) {
|
|
Log.error("[pirateweather] No API key configured");
|
|
if (this.onErrorCallback) {
|
|
this.onErrorCallback({
|
|
message: "API key required",
|
|
translationKey: "MODULE_ERROR_UNSPECIFIED"
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
this.#initializeFetcher();
|
|
}
|
|
|
|
#initializeFetcher () {
|
|
const url = this.#getUrl();
|
|
|
|
this.fetcher = new HTTPFetcher(url, {
|
|
reloadInterval: this.config.updateInterval,
|
|
headers: {
|
|
"Cache-Control": "no-cache",
|
|
Accept: "application/json"
|
|
},
|
|
logContext: "weatherprovider.pirateweather"
|
|
});
|
|
|
|
this.fetcher.on("response", async (response) => {
|
|
if (response.status === 304) return;
|
|
try {
|
|
const data = await response.json();
|
|
this.#handleResponse(data);
|
|
} catch (error) {
|
|
Log.error("[pirateweather] Parse error:", error);
|
|
if (this.onErrorCallback) {
|
|
this.onErrorCallback({
|
|
message: "Failed to parse API response",
|
|
translationKey: "MODULE_ERROR_UNSPECIFIED"
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
this.fetcher.on("error", (errorInfo) => {
|
|
if (this.onErrorCallback) {
|
|
this.onErrorCallback(errorInfo);
|
|
}
|
|
});
|
|
}
|
|
|
|
#handleResponse (data) {
|
|
if (!data || (!data.currently && !data.daily && !data.hourly)) {
|
|
Log.error("[pirateweather] No usable data received");
|
|
if (this.onErrorCallback) {
|
|
this.onErrorCallback({
|
|
message: "No usable data in API response",
|
|
translationKey: "MODULE_ERROR_UNSPECIFIED"
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
let weatherData;
|
|
|
|
switch (this.config.type) {
|
|
case "current":
|
|
weatherData = this.#generateCurrent(data);
|
|
break;
|
|
case "forecast":
|
|
case "daily":
|
|
weatherData = this.#generateDaily(data);
|
|
break;
|
|
case "hourly":
|
|
weatherData = this.#generateHourly(data);
|
|
break;
|
|
default:
|
|
Log.error(`[pirateweather] Unknown weather type: ${this.config.type}`);
|
|
if (this.onErrorCallback) {
|
|
this.onErrorCallback({
|
|
message: `Unknown weather type: ${this.config.type}`,
|
|
translationKey: "MODULE_ERROR_UNSPECIFIED"
|
|
});
|
|
}
|
|
return;
|
|
|
|
}
|
|
|
|
if (weatherData && this.onDataCallback) {
|
|
this.onDataCallback(weatherData);
|
|
}
|
|
}
|
|
|
|
#generateCurrent (data) {
|
|
if (!data.currently || typeof data.currently.temperature === "undefined") {
|
|
return null;
|
|
}
|
|
|
|
const current = {
|
|
date: new Date(),
|
|
humidity: data.currently.humidity != null ? parseFloat(data.currently.humidity) * 100 : null,
|
|
temperature: parseFloat(data.currently.temperature),
|
|
feelsLikeTemp: data.currently.apparentTemperature != null ? parseFloat(data.currently.apparentTemperature) : null,
|
|
windSpeed: data.currently.windSpeed != null ? parseFloat(data.currently.windSpeed) : null,
|
|
windFromDirection: data.currently.windBearing || null,
|
|
weatherType: this.#convertWeatherType(data.currently.icon),
|
|
sunrise: null,
|
|
sunset: null
|
|
};
|
|
|
|
// Add sunrise/sunset from daily data if available
|
|
if (data.daily && data.daily.data && data.daily.data.length > 0) {
|
|
const today = data.daily.data[0];
|
|
if (today.sunriseTime) {
|
|
current.sunrise = new Date(today.sunriseTime * 1000);
|
|
}
|
|
if (today.sunsetTime) {
|
|
current.sunset = new Date(today.sunsetTime * 1000);
|
|
}
|
|
}
|
|
|
|
return current;
|
|
}
|
|
|
|
#generateDaily (data) {
|
|
if (!data.daily || !data.daily.data || !data.daily.data.length) {
|
|
return [];
|
|
}
|
|
|
|
const days = [];
|
|
|
|
for (const forecast of data.daily.data) {
|
|
const day = {
|
|
date: new Date(forecast.time * 1000),
|
|
minTemperature: forecast.temperatureMin != null ? parseFloat(forecast.temperatureMin) : null,
|
|
maxTemperature: forecast.temperatureMax != null ? parseFloat(forecast.temperatureMax) : null,
|
|
weatherType: this.#convertWeatherType(forecast.icon),
|
|
snow: 0,
|
|
rain: 0,
|
|
precipitationAmount: 0,
|
|
precipitationProbability: forecast.precipProbability != null ? parseFloat(forecast.precipProbability) * 100 : null
|
|
};
|
|
|
|
// Handle precipitation
|
|
let precip = 0;
|
|
if (forecast.hasOwnProperty("precipAccumulation")) {
|
|
precip = forecast.precipAccumulation * 10; // cm to mm
|
|
}
|
|
|
|
day.precipitationAmount = precip;
|
|
|
|
if (forecast.precipType) {
|
|
if (forecast.precipType === "snow") {
|
|
day.snow = precip;
|
|
} else {
|
|
day.rain = precip;
|
|
}
|
|
}
|
|
|
|
days.push(day);
|
|
}
|
|
|
|
return days;
|
|
}
|
|
|
|
#generateHourly (data) {
|
|
if (!data.hourly || !data.hourly.data || !data.hourly.data.length) {
|
|
return [];
|
|
}
|
|
|
|
const hours = [];
|
|
|
|
for (const forecast of data.hourly.data) {
|
|
const hour = {
|
|
date: new Date(forecast.time * 1000),
|
|
temperature: forecast.temperature !== undefined ? parseFloat(forecast.temperature) : null,
|
|
feelsLikeTemp: forecast.apparentTemperature !== undefined ? parseFloat(forecast.apparentTemperature) : null,
|
|
weatherType: this.#convertWeatherType(forecast.icon),
|
|
windSpeed: forecast.windSpeed !== undefined ? parseFloat(forecast.windSpeed) : null,
|
|
windFromDirection: forecast.windBearing || null,
|
|
precipitationProbability: forecast.precipProbability ? parseFloat(forecast.precipProbability) * 100 : null,
|
|
snow: 0,
|
|
rain: 0,
|
|
precipitationAmount: 0
|
|
};
|
|
|
|
// Handle precipitation
|
|
let precip = 0;
|
|
if (forecast.hasOwnProperty("precipAccumulation")) {
|
|
precip = forecast.precipAccumulation * 10; // cm to mm
|
|
}
|
|
|
|
hour.precipitationAmount = precip;
|
|
|
|
if (forecast.precipType) {
|
|
if (forecast.precipType === "snow") {
|
|
hour.snow = precip;
|
|
} else {
|
|
hour.rain = precip;
|
|
}
|
|
}
|
|
|
|
hours.push(hour);
|
|
}
|
|
|
|
return hours;
|
|
}
|
|
|
|
#getUrl () {
|
|
const apiBase = this.config.apiBase || "https://api.pirateweather.net";
|
|
const weatherEndpoint = this.config.weatherEndpoint || "/forecast";
|
|
const lang = this.config.lang || "en";
|
|
return `${apiBase}${weatherEndpoint}/${this.config.apiKey}/${this.config.lat},${this.config.lon}?units=si&lang=${lang}`;
|
|
}
|
|
|
|
#convertWeatherType (weatherType) {
|
|
const weatherTypes = {
|
|
"clear-day": "day-sunny",
|
|
"clear-night": "night-clear",
|
|
rain: "rain",
|
|
snow: "snow",
|
|
sleet: "snow",
|
|
wind: "windy",
|
|
fog: "fog",
|
|
cloudy: "cloudy",
|
|
"partly-cloudy-day": "day-cloudy",
|
|
"partly-cloudy-night": "night-cloudy"
|
|
};
|
|
|
|
return weatherTypes[weatherType] || null;
|
|
}
|
|
|
|
start () {
|
|
if (this.fetcher) {
|
|
this.fetcher.startPeriodicFetch();
|
|
}
|
|
}
|
|
|
|
stop () {
|
|
if (this.fetcher) {
|
|
this.fetcher.clearTimer();
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = PirateweatherProvider;
|