mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-06-27 03:39:55 +00:00
## [2.26.0] - 01-01-2024 Thanks to: @bnitkin, @bugsounet, @dependabot, @jkriegshauser, @kaennchenstruggle, @KristjanESPERANTO and @Ybbet. Special thanks to @khassel, @rejas and @sdetweil for taking over most (if not all) of the work on this release as project collaborators. This version would not be there without their effort. Thank you guys! You are awesome! This release also marks the latest release by Michael Teeuw. For more info, please read the following post: [A New Chapter for MagicMirror: The Community Takes the Lead](https://forum.magicmirror.builders/topic/18329/a-new-chapter-for-magicmirror-the-community-takes-the-lead). ### Added - Added update notification updater (for 3rd party modules) - Added node 21 to the test matrix - Added transform object to calendar:customEvents - Added ESLint rules for jest (including jest/expect-expect and jest/no-done-callback) ### Removed - Removed Codecov workflow (not working anymore, other workflow required) (#3107) - Removed titleReplace from calendar, replaced + extended by customEvents (backward compatibility included) (#3249) - Removed failing unit test (#3254) - Removed some unused variables ### Updated - Update electron to v27 and update other dependencies as well as github actions - Update newsfeed: Use `html-to-text` instead of regex for transform description - Review ESLint config (#3269) - Updated dependencies - Clock module: optionally display current moon phase in addition to rise/set times - electron is now per default started without gpu, if needed it must be enabled with new env var `ELECTRON_ENABLE_GPU=1` on startup (#3226) - Replace prettier by stylistic in ESLint config to lint JavaScript (and disable some rules for `config/config.js*` files) - Update node-ical to v0.17.1 and fix tests ### Fixed - Avoid fade out/in on updateDom when many calendars are used - Fix the option eventClass on customEvents. - Fix yr API version in locationforecast and sunrise call (#3227) - Fix cloneObject() function to respect RegExp (#3237) - Fix newsfeed module for feeds using "a10:updated" tag (#3238) - Fix issue template (#3167) - Fix #3256 filter out bad results from rrule.between - Fix calendar events sometimes not respecting deleted events (#3250) - Fix electron loadurl locally on Windows when address "0.0.0.0" (#2550) - Fix updatanotification (update_helper.js): catch error if reponse is not an JSON format (check PM2) - Fix missing typeof in calendar module - Fix style issues after prettier update - Fix calendar test (#3291) by moving "Exdate check" from e2e to electron to run on a Thursday - Fix calendar config params `fetchInterval` and `excludedEvents` were never used from single calendar config (#3297) - Fix MM_PORT variable not used in electron and allow full path for MM_CONFIG_FILE variable (#3302) --------- Signed-off-by: naveen <172697+naveensrinivasan@users.noreply.github.com> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Karsten Hassel <hassel@gmx.de> Co-authored-by: Malte Hallström <46646495+SkySails@users.noreply.github.com> Co-authored-by: Veeck <github@veeck.de> Co-authored-by: veeck <michael@veeck.de> Co-authored-by: dWoolridge <dwoolridge@charter.net> Co-authored-by: Johan <jojjepersson@yahoo.se> Co-authored-by: Dario Mratovich <dario_mratovich@hotmail.com> Co-authored-by: Dario Mratovich <dario.mratovich@outlook.com> Co-authored-by: Magnus <34011212+MagMar94@users.noreply.github.com> Co-authored-by: Naveen <172697+naveensrinivasan@users.noreply.github.com> Co-authored-by: buxxi <buxxi@omfilm.net> Co-authored-by: Thomas Hirschberger <47733292+Tom-Hirschberger@users.noreply.github.com> Co-authored-by: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Co-authored-by: Andrés Vanegas Jiménez <142350+angeldeejay@users.noreply.github.com> Co-authored-by: Dave Child <dave@addedbytes.com> Co-authored-by: grenagit <46225780+grenagit@users.noreply.github.com> Co-authored-by: Grena <grena@grenabox.fr> Co-authored-by: Magnus Marthinsen <magmar@online.no> Co-authored-by: Patrick <psieg@users.noreply.github.com> Co-authored-by: Piotr Rajnisz <56397164+rajniszp@users.noreply.github.com> Co-authored-by: Suthep Yonphimai <tomzt@users.noreply.github.com> Co-authored-by: CarJem Generations (Carter Wallace) <cwallacecs@gmail.com> Co-authored-by: Nicholas Fogal <nfogal.misc@gmail.com> Co-authored-by: JakeBinney <126349119+JakeBinney@users.noreply.github.com> Co-authored-by: OWL4C <124401812+OWL4C@users.noreply.github.com> Co-authored-by: Oscar Björkman <17575446+oscarb@users.noreply.github.com> Co-authored-by: Ismar Slomic <ismar@slomic.no> Co-authored-by: Jørgen Veum-Wahlberg <jorgen.wahlberg@amedia.no> Co-authored-by: Eddie Hung <6740044+eddiehung@users.noreply.github.com> Co-authored-by: Bugsounet - Cédric <github@bugsounet.fr> Co-authored-by: bugsounet <bugsounet@bugsounet.fr> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Knapoc <Knapoc@users.noreply.github.com> Co-authored-by: sam detweiler <sdetweil@gmail.com> Co-authored-by: veeck <michael.veeck@nebenan.de> Co-authored-by: Paranoid93 <6515818+Paranoid93@users.noreply.github.com> Co-authored-by: NolanKingdon <27908974+NolanKingdon@users.noreply.github.com> Co-authored-by: J. Kenzal Hunter <kenzal.hunter@gmail.com> Co-authored-by: Teddy <teddy.payet@gmail.com> Co-authored-by: TeddyStarinvest <teddy.payet@starinvest.com> Co-authored-by: martingron <61826403+martingron@users.noreply.github.com> Co-authored-by: dgoth <132394363+dgoth@users.noreply.github.com> Co-authored-by: kaennchenstruggle <54073894+kaennchenstruggle@users.noreply.github.com> Co-authored-by: jkriegshauser <jkriegshauser@gmail.com> Co-authored-by: Ben Nitkin <ben@nitkin.net>
608 lines
21 KiB
JavaScript
608 lines
21 KiB
JavaScript
/* global WeatherProvider, WeatherObject */
|
|
|
|
/* MagicMirror²
|
|
* Module: Weather
|
|
* Provider: Yr.no
|
|
*
|
|
* By Magnus Marthinsen
|
|
* MIT Licensed
|
|
*
|
|
* This class is a provider for Yr.no, a norwegian weather service.
|
|
*
|
|
* Terms of service: https://developer.yr.no/doc/TermsOfService/
|
|
*/
|
|
WeatherProvider.register("yr", {
|
|
providerName: "Yr",
|
|
|
|
// Set the default config properties that is specific to this provider
|
|
defaults: {
|
|
useCorsProxy: true,
|
|
apiBase: "https://api.met.no/weatherapi",
|
|
forecastApiVersion: "2.0",
|
|
sunriseApiVersion: "3.0",
|
|
altitude: 0,
|
|
currentForecastHours: 1 //1, 6 or 12
|
|
},
|
|
|
|
start () {
|
|
if (typeof Storage === "undefined") {
|
|
//local storage unavailable
|
|
Log.error("The Yr weather provider requires local storage.");
|
|
throw new Error("Local storage not available");
|
|
}
|
|
Log.info(`Weather provider: ${this.providerName} started.`);
|
|
},
|
|
|
|
fetchCurrentWeather () {
|
|
this.getCurrentWeather()
|
|
.then((currentWeather) => {
|
|
this.setCurrentWeather(currentWeather);
|
|
this.updateAvailable();
|
|
})
|
|
.catch((error) => {
|
|
Log.error(error);
|
|
throw new Error(error);
|
|
});
|
|
},
|
|
|
|
async getCurrentWeather () {
|
|
const [weatherData, stellarData] = await Promise.all([this.getWeatherData(), this.getStellarData()]);
|
|
if (!stellarData) {
|
|
Log.warn("No stellar data available.");
|
|
}
|
|
if (!weatherData.properties.timeseries || !weatherData.properties.timeseries[0]) {
|
|
Log.error("No weather data available.");
|
|
return;
|
|
}
|
|
const currentTime = moment();
|
|
let forecast = weatherData.properties.timeseries[0];
|
|
let closestTimeInPast = currentTime.diff(moment(forecast.time));
|
|
for (const forecastTime of weatherData.properties.timeseries) {
|
|
const comparison = currentTime.diff(moment(forecastTime.time));
|
|
if (0 < comparison && comparison < closestTimeInPast) {
|
|
closestTimeInPast = comparison;
|
|
forecast = forecastTime;
|
|
}
|
|
}
|
|
const forecastXHours = this.getForecastForXHoursFrom(forecast.data);
|
|
forecast.weatherType = this.convertWeatherType(forecastXHours.summary.symbol_code, forecast.time);
|
|
forecast.precipitationAmount = forecastXHours.details?.precipitation_amount;
|
|
forecast.precipitationProbability = forecastXHours.details?.probability_of_precipitation;
|
|
forecast.minTemperature = forecastXHours.details?.air_temperature_min;
|
|
forecast.maxTemperature = forecastXHours.details?.air_temperature_max;
|
|
return this.getWeatherDataFrom(forecast, stellarData, weatherData.properties.meta.units);
|
|
},
|
|
|
|
getWeatherData () {
|
|
return new Promise((resolve, reject) => {
|
|
// If a user has several Yr-modules, for instance one current and one forecast, the API calls must be synchronized across classes.
|
|
// This is to avoid multiple similar calls to the API.
|
|
let shouldWait = localStorage.getItem("yrIsFetchingWeatherData");
|
|
if (shouldWait) {
|
|
const checkForGo = setInterval(function () {
|
|
shouldWait = localStorage.getItem("yrIsFetchingWeatherData");
|
|
}, 100);
|
|
setTimeout(function () {
|
|
clearInterval(checkForGo);
|
|
shouldWait = false;
|
|
}, 5000); //Assume other fetch finished but failed to remove lock
|
|
const attemptFetchWeather = setInterval(() => {
|
|
if (!shouldWait) {
|
|
clearInterval(checkForGo);
|
|
clearInterval(attemptFetchWeather);
|
|
this.getWeatherDataFromYrOrCache(resolve, reject);
|
|
}
|
|
}, 100);
|
|
} else {
|
|
this.getWeatherDataFromYrOrCache(resolve, reject);
|
|
}
|
|
});
|
|
},
|
|
|
|
getWeatherDataFromYrOrCache (resolve, reject) {
|
|
localStorage.setItem("yrIsFetchingWeatherData", "true");
|
|
|
|
let weatherData = this.getWeatherDataFromCache();
|
|
if (this.weatherDataIsValid(weatherData)) {
|
|
localStorage.removeItem("yrIsFetchingWeatherData");
|
|
Log.debug("Weather data found in cache.");
|
|
resolve(weatherData);
|
|
} else {
|
|
this.getWeatherDataFromYr(weatherData?.downloadedAt)
|
|
.then((weatherData) => {
|
|
Log.debug("Got weather data from yr.");
|
|
let data;
|
|
if (weatherData) {
|
|
this.cacheWeatherData(weatherData);
|
|
data = weatherData;
|
|
} else {
|
|
//Undefined if unchanged
|
|
data = this.getWeatherDataFromCache();
|
|
}
|
|
resolve(data);
|
|
})
|
|
.catch((err) => {
|
|
Log.error(err);
|
|
reject("Unable to get weather data from Yr.");
|
|
})
|
|
.finally(() => {
|
|
localStorage.removeItem("yrIsFetchingWeatherData");
|
|
});
|
|
}
|
|
},
|
|
|
|
weatherDataIsValid (weatherData) {
|
|
return (
|
|
weatherData
|
|
&& weatherData.timeout
|
|
&& 0 < moment(weatherData.timeout).diff(moment())
|
|
&& (!weatherData.geometry || !weatherData.geometry.coordinates || !weatherData.geometry.coordinates.length < 2 || (weatherData.geometry.coordinates[0] === this.config.lat && weatherData.geometry.coordinates[1] === this.config.lon))
|
|
);
|
|
},
|
|
|
|
getWeatherDataFromCache () {
|
|
const weatherData = localStorage.getItem("weatherData");
|
|
if (weatherData) {
|
|
return JSON.parse(weatherData);
|
|
} else {
|
|
return undefined;
|
|
}
|
|
},
|
|
|
|
getWeatherDataFromYr (currentDataFetchedAt) {
|
|
const requestHeaders = [{ name: "Accept", value: "application/json" }];
|
|
if (currentDataFetchedAt) {
|
|
requestHeaders.push({ name: "If-Modified-Since", value: currentDataFetchedAt });
|
|
}
|
|
|
|
const expectedResponseHeaders = ["expires", "date"];
|
|
|
|
return this.fetchData(this.getForecastUrl(), "json", requestHeaders, expectedResponseHeaders)
|
|
.then((data) => {
|
|
if (!data || !data.headers) return data;
|
|
data.timeout = data.headers.find((header) => header.name === "expires").value;
|
|
data.downloadedAt = data.headers.find((header) => header.name === "date").value;
|
|
data.headers = undefined;
|
|
return data;
|
|
})
|
|
.catch((err) => {
|
|
Log.error("Could not load weather data.", err);
|
|
throw new Error(err);
|
|
});
|
|
},
|
|
|
|
getConfigOptions () {
|
|
if (!this.config.lat) {
|
|
Log.error("Latitude not provided.");
|
|
throw new Error("Latitude not provided.");
|
|
}
|
|
if (!this.config.lon) {
|
|
Log.error("Longitude not provided.");
|
|
throw new Error("Longitude not provided.");
|
|
}
|
|
|
|
let lat = this.config.lat.toString();
|
|
let lon = this.config.lon.toString();
|
|
const altitude = this.config.altitude ?? 0;
|
|
return { lat, lon, altitude };
|
|
},
|
|
|
|
getForecastUrl () {
|
|
let { lat, lon, altitude } = this.getConfigOptions();
|
|
|
|
if (lat.includes(".") && lat.split(".")[1].length > 4) {
|
|
Log.warn("Latitude is too specific for weather data. Do not use more than four decimals. Trimming to maximum length.");
|
|
const latParts = lat.split(".");
|
|
lat = `${latParts[0]}.${latParts[1].substring(0, 4)}`;
|
|
}
|
|
if (lon.includes(".") && lon.split(".")[1].length > 4) {
|
|
Log.warn("Longitude is too specific for weather data. Do not use more than four decimals. Trimming to maximum length.");
|
|
const lonParts = lon.split(".");
|
|
lon = `${lonParts[0]}.${lonParts[1].substring(0, 4)}`;
|
|
}
|
|
|
|
return `${this.config.apiBase}/locationforecast/${this.config.forecastApiVersion}/complete?&altitude=${altitude}&lat=${lat}&lon=${lon}`;
|
|
},
|
|
|
|
cacheWeatherData (weatherData) {
|
|
localStorage.setItem("weatherData", JSON.stringify(weatherData));
|
|
},
|
|
|
|
getStellarData () {
|
|
// If a user has several Yr-modules, for instance one current and one forecast, the API calls must be synchronized across classes.
|
|
// This is to avoid multiple similar calls to the API.
|
|
return new Promise((resolve, reject) => {
|
|
let shouldWait = localStorage.getItem("yrIsFetchingStellarData");
|
|
if (shouldWait) {
|
|
const checkForGo = setInterval(function () {
|
|
shouldWait = localStorage.getItem("yrIsFetchingStellarData");
|
|
}, 100);
|
|
setTimeout(function () {
|
|
clearInterval(checkForGo);
|
|
shouldWait = false;
|
|
}, 5000); //Assume other fetch finished but failed to remove lock
|
|
const attemptFetchWeather = setInterval(() => {
|
|
if (!shouldWait) {
|
|
clearInterval(checkForGo);
|
|
clearInterval(attemptFetchWeather);
|
|
this.getStellarDataFromYrOrCache(resolve, reject);
|
|
}
|
|
}, 100);
|
|
} else {
|
|
this.getStellarDataFromYrOrCache(resolve, reject);
|
|
}
|
|
});
|
|
},
|
|
|
|
getStellarDataFromYrOrCache (resolve, reject) {
|
|
localStorage.setItem("yrIsFetchingStellarData", "true");
|
|
|
|
let stellarData = this.getStellarDataFromCache();
|
|
const today = moment().format("YYYY-MM-DD");
|
|
const tomorrow = moment().add(1, "days").format("YYYY-MM-DD");
|
|
if (stellarData && stellarData.today && stellarData.today.date === today && stellarData.tomorrow && stellarData.tomorrow.date === tomorrow) {
|
|
Log.debug("Stellar data found in cache.");
|
|
localStorage.removeItem("yrIsFetchingStellarData");
|
|
resolve(stellarData);
|
|
} else if (stellarData && stellarData.tomorrow && stellarData.tomorrow.date === today) {
|
|
Log.debug("stellar data for today found in cache, but not for tomorrow.");
|
|
stellarData.today = stellarData.tomorrow;
|
|
this.getStellarDataFromYr(tomorrow)
|
|
.then((data) => {
|
|
if (data) {
|
|
data.date = tomorrow;
|
|
stellarData.tomorrow = data;
|
|
this.cacheStellarData(stellarData);
|
|
resolve(stellarData);
|
|
} else {
|
|
reject(`No stellar data returned from Yr for ${tomorrow}`);
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
Log.error(err);
|
|
reject(`Unable to get stellar data from Yr for ${tomorrow}`);
|
|
})
|
|
.finally(() => {
|
|
localStorage.removeItem("yrIsFetchingStellarData");
|
|
});
|
|
} else {
|
|
this.getStellarDataFromYr(today, 2)
|
|
.then((stellarData) => {
|
|
if (stellarData) {
|
|
const data = {
|
|
today: stellarData
|
|
};
|
|
data.tomorrow = Object.assign({}, data.today);
|
|
data.today.date = today;
|
|
data.tomorrow.date = tomorrow;
|
|
this.cacheStellarData(data);
|
|
resolve(data);
|
|
} else {
|
|
Log.error(`Something went wrong when fetching stellar data. Responses: ${stellarData}`);
|
|
reject(stellarData);
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
Log.error(err);
|
|
reject("Unable to get stellar data from Yr.");
|
|
})
|
|
.finally(() => {
|
|
localStorage.removeItem("yrIsFetchingStellarData");
|
|
});
|
|
}
|
|
},
|
|
|
|
getStellarDataFromCache () {
|
|
const stellarData = localStorage.getItem("stellarData");
|
|
if (stellarData) {
|
|
return JSON.parse(stellarData);
|
|
} else {
|
|
return undefined;
|
|
}
|
|
},
|
|
|
|
getStellarDataFromYr (date, days = 1) {
|
|
const requestHeaders = [{ name: "Accept", value: "application/json" }];
|
|
return this.fetchData(this.getStellarDataUrl(date, days), "json", requestHeaders)
|
|
.then((data) => {
|
|
Log.debug("Got stellar data from yr.");
|
|
return data;
|
|
})
|
|
.catch((err) => {
|
|
Log.error("Could not load weather data.", err);
|
|
throw new Error(err);
|
|
});
|
|
},
|
|
|
|
getStellarDataUrl (date, days) {
|
|
let { lat, lon, altitude } = this.getConfigOptions();
|
|
|
|
if (lat.includes(".") && lat.split(".")[1].length > 4) {
|
|
Log.warn("Latitude is too specific for stellar data. Do not use more than four decimals. Trimming to maximum length.");
|
|
const latParts = lat.split(".");
|
|
lat = `${latParts[0]}.${latParts[1].substring(0, 4)}`;
|
|
}
|
|
if (lon.includes(".") && lon.split(".")[1].length > 4) {
|
|
Log.warn("Longitude is too specific for stellar data. Do not use more than four decimals. Trimming to maximum length.");
|
|
const lonParts = lon.split(".");
|
|
lon = `${lonParts[0]}.${lonParts[1].substring(0, 4)}`;
|
|
}
|
|
|
|
let utcOffset = moment().utcOffset() / 60;
|
|
let utcOffsetPrefix = "%2B";
|
|
if (utcOffset < 0) {
|
|
utcOffsetPrefix = "-";
|
|
}
|
|
utcOffset = Math.abs(utcOffset);
|
|
let minutes = "00";
|
|
if (utcOffset % 1 !== 0) {
|
|
minutes = "30";
|
|
}
|
|
let hours = Math.floor(utcOffset).toString();
|
|
if (hours.length < 2) {
|
|
hours = `0${hours}`;
|
|
}
|
|
return `${this.config.apiBase}/sunrise/${this.config.sunriseApiVersion}/sun?lat=${lat}&lon=${lon}&date=${date}&offset=${utcOffsetPrefix}${hours}%3A${minutes}`;
|
|
},
|
|
|
|
cacheStellarData (data) {
|
|
localStorage.setItem("stellarData", JSON.stringify(data));
|
|
},
|
|
|
|
getWeatherDataFrom (forecast, stellarData, units) {
|
|
const weather = new WeatherObject();
|
|
|
|
weather.date = moment(forecast.time);
|
|
weather.windSpeed = forecast.data.instant.details.wind_speed;
|
|
weather.windFromDirection = forecast.data.instant.details.wind_from_direction;
|
|
weather.temperature = forecast.data.instant.details.air_temperature;
|
|
weather.minTemperature = forecast.minTemperature;
|
|
weather.maxTemperature = forecast.maxTemperature;
|
|
weather.weatherType = forecast.weatherType;
|
|
weather.humidity = forecast.data.instant.details.relative_humidity;
|
|
weather.precipitationAmount = forecast.precipitationAmount;
|
|
weather.precipitationProbability = forecast.precipitationProbability;
|
|
weather.precipitationUnits = units.precipitation_amount;
|
|
|
|
weather.sunrise = stellarData?.today?.properties?.sunrise?.time;
|
|
weather.sunset = stellarData?.today?.properties?.sunset?.time;
|
|
|
|
return weather;
|
|
},
|
|
|
|
convertWeatherType (weatherType, weatherTime) {
|
|
const weatherHour = moment(weatherTime).format("HH");
|
|
|
|
const weatherTypes = {
|
|
clearsky_day: "day-sunny",
|
|
clearsky_night: "night-clear",
|
|
clearsky_polartwilight: weatherHour < 14 ? "sunrise" : "sunset",
|
|
cloudy: "cloudy",
|
|
fair_day: "day-sunny-overcast",
|
|
fair_night: "night-alt-partly-cloudy",
|
|
fair_polartwilight: "day-sunny-overcast",
|
|
fog: "fog",
|
|
heavyrain: "rain", // Possibly raindrops or raindrop
|
|
heavyrainandthunder: "thunderstorm",
|
|
heavyrainshowers_day: "day-rain",
|
|
heavyrainshowers_night: "night-alt-rain",
|
|
heavyrainshowers_polartwilight: "day-rain",
|
|
heavyrainshowersandthunder_day: "day-thunderstorm",
|
|
heavyrainshowersandthunder_night: "night-alt-thunderstorm",
|
|
heavyrainshowersandthunder_polartwilight: "day-thunderstorm",
|
|
heavysleet: "sleet",
|
|
heavysleetandthunder: "day-sleet-storm",
|
|
heavysleetshowers_day: "day-sleet",
|
|
heavysleetshowers_night: "night-alt-sleet",
|
|
heavysleetshowers_polartwilight: "day-sleet",
|
|
heavysleetshowersandthunder_day: "day-sleet-storm",
|
|
heavysleetshowersandthunder_night: "night-alt-sleet-storm",
|
|
heavysleetshowersandthunder_polartwilight: "day-sleet-storm",
|
|
heavysnow: "snow-wind",
|
|
heavysnowandthunder: "day-snow-thunderstorm",
|
|
heavysnowshowers_day: "day-snow-wind",
|
|
heavysnowshowers_night: "night-alt-snow-wind",
|
|
heavysnowshowers_polartwilight: "day-snow-wind",
|
|
heavysnowshowersandthunder_day: "day-snow-thunderstorm",
|
|
heavysnowshowersandthunder_night: "night-alt-snow-thunderstorm",
|
|
heavysnowshowersandthunder_polartwilight: "day-snow-thunderstorm",
|
|
lightrain: "rain-mix",
|
|
lightrainandthunder: "thunderstorm",
|
|
lightrainshowers_day: "day-rain-mix",
|
|
lightrainshowers_night: "night-alt-rain-mix",
|
|
lightrainshowers_polartwilight: "day-rain-mix",
|
|
lightrainshowersandthunder_day: "thunderstorm",
|
|
lightrainshowersandthunder_night: "thunderstorm",
|
|
lightrainshowersandthunder_polartwilight: "thunderstorm",
|
|
lightsleet: "day-sleet",
|
|
lightsleetandthunder: "day-sleet-storm",
|
|
lightsleetshowers_day: "day-sleet",
|
|
lightsleetshowers_night: "night-alt-sleet",
|
|
lightsleetshowers_polartwilight: "day-sleet",
|
|
lightsnow: "snowflake-cold",
|
|
lightsnowandthunder: "day-snow-thunderstorm",
|
|
lightsnowshowers_day: "day-snow-wind",
|
|
lightsnowshowers_night: "night-alt-snow-wind",
|
|
lightsnowshowers_polartwilight: "day-snow-wind",
|
|
lightssleetshowersandthunder_day: "day-sleet-storm",
|
|
lightssleetshowersandthunder_night: "night-alt-sleet-storm",
|
|
lightssleetshowersandthunder_polartwilight: "day-sleet-storm",
|
|
lightssnowshowersandthunder_day: "day-snow-thunderstorm",
|
|
lightssnowshowersandthunder_night: "night-alt-snow-thunderstorm",
|
|
lightssnowshowersandthunder_polartwilight: "day-snow-thunderstorm",
|
|
partlycloudy_day: "day-cloudy",
|
|
partlycloudy_night: "night-alt-cloudy",
|
|
partlycloudy_polartwilight: "day-cloudy",
|
|
rain: "rain",
|
|
rainandthunder: "thunderstorm",
|
|
rainshowers_day: "day-rain",
|
|
rainshowers_night: "night-alt-rain",
|
|
rainshowers_polartwilight: "day-rain",
|
|
rainshowersandthunder_day: "thunderstorm",
|
|
rainshowersandthunder_night: "lightning",
|
|
rainshowersandthunder_polartwilight: "thunderstorm",
|
|
sleet: "sleet",
|
|
sleetandthunder: "day-sleet-storm",
|
|
sleetshowers_day: "day-sleet",
|
|
sleetshowers_night: "night-alt-sleet",
|
|
sleetshowers_polartwilight: "day-sleet",
|
|
sleetshowersandthunder_day: "day-sleet-storm",
|
|
sleetshowersandthunder_night: "night-alt-sleet-storm",
|
|
sleetshowersandthunder_polartwilight: "day-sleet-storm",
|
|
snow: "snowflake-cold",
|
|
snowandthunder: "lightning",
|
|
snowshowers_day: "day-snow-wind",
|
|
snowshowers_night: "night-alt-snow-wind",
|
|
snowshowers_polartwilight: "day-snow-wind",
|
|
snowshowersandthunder_day: "day-snow-thunderstorm",
|
|
snowshowersandthunder_night: "night-alt-snow-thunderstorm",
|
|
snowshowersandthunder_polartwilight: "day-snow-thunderstorm"
|
|
};
|
|
|
|
return weatherTypes.hasOwnProperty(weatherType) ? weatherTypes[weatherType] : null;
|
|
},
|
|
|
|
getForecastForXHoursFrom (weather) {
|
|
if (this.config.currentForecastHours === 1) {
|
|
if (weather.next_1_hours) {
|
|
return weather.next_1_hours;
|
|
} else if (weather.next_6_hours) {
|
|
return weather.next_6_hours;
|
|
} else {
|
|
return weather.next_12_hours;
|
|
}
|
|
} else if (this.config.currentForecastHours === 6) {
|
|
if (weather.next_6_hours) {
|
|
return weather.next_6_hours;
|
|
} else if (weather.next_12_hours) {
|
|
return weather.next_12_hours;
|
|
} else {
|
|
return weather.next_1_hours;
|
|
}
|
|
} else {
|
|
if (weather.next_12_hours) {
|
|
return weather.next_12_hours;
|
|
} else if (weather.next_6_hours) {
|
|
return weather.next_6_hours;
|
|
} else {
|
|
return weather.next_1_hours;
|
|
}
|
|
}
|
|
},
|
|
|
|
fetchWeatherHourly () {
|
|
this.getWeatherForecast("hourly")
|
|
.then((forecast) => {
|
|
this.setWeatherHourly(forecast);
|
|
this.updateAvailable();
|
|
})
|
|
.catch((error) => {
|
|
Log.error(error);
|
|
throw new Error(error);
|
|
});
|
|
},
|
|
|
|
async getWeatherForecast (type) {
|
|
const [weatherData, stellarData] = await Promise.all([this.getWeatherData(), this.getStellarData()]);
|
|
if (!weatherData.properties.timeseries || !weatherData.properties.timeseries[0]) {
|
|
Log.error("No weather data available.");
|
|
return;
|
|
}
|
|
if (!stellarData) {
|
|
Log.warn("No stellar data available.");
|
|
}
|
|
let forecasts;
|
|
switch (type) {
|
|
case "hourly":
|
|
forecasts = this.getHourlyForecastFrom(weatherData);
|
|
break;
|
|
case "daily":
|
|
default:
|
|
forecasts = this.getDailyForecastFrom(weatherData);
|
|
break;
|
|
}
|
|
const series = [];
|
|
for (const forecast of forecasts) {
|
|
series.push(this.getWeatherDataFrom(forecast, stellarData, weatherData.properties.meta.units));
|
|
}
|
|
return series;
|
|
},
|
|
|
|
getHourlyForecastFrom (weatherData) {
|
|
const series = [];
|
|
|
|
for (const forecast of weatherData.properties.timeseries) {
|
|
forecast.symbol = forecast.data.next_1_hours?.summary?.symbol_code;
|
|
forecast.precipitationAmount = forecast.data.next_1_hours?.details?.precipitation_amount;
|
|
forecast.precipitationProbability = forecast.data.next_1_hours?.details?.probability_of_precipitation;
|
|
forecast.minTemperature = forecast.data.next_1_hours?.details?.air_temperature_min;
|
|
forecast.maxTemperature = forecast.data.next_1_hours?.details?.air_temperature_max;
|
|
forecast.weatherType = this.convertWeatherType(forecast.symbol, forecast.time);
|
|
series.push(forecast);
|
|
}
|
|
return series;
|
|
},
|
|
|
|
getDailyForecastFrom (weatherData) {
|
|
const series = [];
|
|
|
|
const days = weatherData.properties.timeseries.reduce(function (days, forecast) {
|
|
const date = moment(forecast.time).format("YYYY-MM-DD");
|
|
days[date] = days[date] || [];
|
|
days[date].push(forecast);
|
|
return days;
|
|
}, Object.create(null));
|
|
|
|
Object.keys(days).forEach(function (time) {
|
|
let minTemperature = undefined;
|
|
let maxTemperature = undefined;
|
|
|
|
//Default to first entry
|
|
let forecast = days[time][0];
|
|
forecast.symbol = forecast.data.next_12_hours?.summary?.symbol_code;
|
|
forecast.precipitation = forecast.data.next_12_hours?.details?.precipitation_amount;
|
|
|
|
//Coming days
|
|
let forecastDiffToEight = undefined;
|
|
for (const timeseries of days[time]) {
|
|
if (!timeseries.data.next_6_hours) continue; //next_6_hours has the most data
|
|
|
|
if (!minTemperature || timeseries.data.next_6_hours.details.air_temperature_min < minTemperature) minTemperature = timeseries.data.next_6_hours.details.air_temperature_min;
|
|
if (!maxTemperature || maxTemperature < timeseries.data.next_6_hours.details.air_temperature_max) maxTemperature = timeseries.data.next_6_hours.details.air_temperature_max;
|
|
|
|
let closestTime = Math.abs(moment(timeseries.time).local().set({ hour: 8, minute: 0, second: 0, millisecond: 0 }).diff(moment(timeseries.time).local()));
|
|
if ((forecastDiffToEight === undefined || closestTime < forecastDiffToEight) && timeseries.data.next_12_hours) {
|
|
forecastDiffToEight = closestTime;
|
|
forecast = timeseries;
|
|
}
|
|
}
|
|
const forecastXHours = forecast.data.next_12_hours ?? forecast.data.next_6_hours ?? forecast.data.next_1_hours;
|
|
if (forecastXHours) {
|
|
forecast.symbol = forecastXHours.summary?.symbol_code;
|
|
forecast.precipitationAmount = forecastXHours.details?.precipitation_amount ?? forecast.data.next_6_hours?.details?.precipitation_amount; // 6 hours is likely to have precipitation amount even if 12 hours does not
|
|
forecast.precipitationProbability = forecastXHours.details?.probability_of_precipitation;
|
|
forecast.minTemperature = minTemperature;
|
|
forecast.maxTemperature = maxTemperature;
|
|
|
|
series.push(forecast);
|
|
}
|
|
});
|
|
for (const forecast of series) {
|
|
forecast.weatherType = this.convertWeatherType(forecast.symbol, forecast.time);
|
|
}
|
|
return series;
|
|
},
|
|
|
|
fetchWeatherForecast () {
|
|
this.getWeatherForecast("daily")
|
|
.then((forecast) => {
|
|
this.setWeatherForecast(forecast);
|
|
this.updateAvailable();
|
|
})
|
|
.catch((error) => {
|
|
Log.error(error);
|
|
throw new Error(error);
|
|
});
|
|
}
|
|
});
|