Files
MagicMirror/defaultmodules/weather/providers/pirateweather.js
Kristjan ESPERANTO 3ea3f0a605 chore: upgrade ESLint to v10 and fix newly surfaced issues (#4057)
`eslint-plugin-import-x` was the last thing blocking the ESLint v10
upgrade - it just got v10 support. So here we go.

The upgrade itself is tiny. The rest of the diff is cleanup from issues
ESLint v10 now catches: a few `let` declarations with initial values
that were immediately overwritten anyway (`no-useless-assignment`), and
`Translator` listed in `/* global */` in `main.js` and `module.js`.

Working through those `no-useless-assignment` warnings also surfaced a
dead default in `openmeteo`: `maxEntries: 5` in the constructor, which
was never actually doing anything - `openmeteo` never reads
`this.config.maxEntries` anywhere. And `weather.js` already sets that
default for all providers, so it was just a redundant duplicate. Removed
that too.

No runtime behavior changes.
2026-03-12 11:58:26 +01:00

271 lines
6.7 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) => {
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;