mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-06-27 11:50:00 +00:00
Fixes #2953 This is an attempt to fix the issue with precipitation amount and percentage mixup. I have created a separate `precipitationPercentage`-variable where the probability of rain can be stored. The config options now has the old `showPrecipitationAmount` in addition to a new setting: `showPrecipitationProbability` (shows the likelihood of rain). <details> <summary>Examples</summary> ### Yr I tested the Yr weather provider for a Norwegian city Bergen that has a lot of rain. I have removed properties that are irrelevant for this demo from the config-samples below. Config: ```js { module: "weather", config: { weatherProvider: "yr", type: "current", showPrecipitationAmount: true, showPrecipitationProbability: true } }, { module: "weather", config: { weatherProvider: "yr", type: "hourly", showPrecipitationAmount: true, showPrecipitationProbability: true } }, { module: "weather", config: { weatherProvider: "yr", type: "daily", showPrecipitationAmount: true, showPrecipitationProbability: true } } ``` Result:<br/> <img width="444" alt="screenshot" src="https://user-images.githubusercontent.com/34011212/216775423-4e37345c-f915-47e5-8551-7c544ebd24b1.png"> </details> --------- Co-authored-by: Magnus Marthinsen <magmar@online.no> Co-authored-by: Veeck <github@veeck.de>
447 lines
14 KiB
JavaScript
447 lines
14 KiB
JavaScript
/* global WeatherProvider, WeatherObject */
|
|
|
|
/* MagicMirror²
|
|
* Module: Weather
|
|
*
|
|
* By Michael Teeuw https://michaelteeuw.nl
|
|
* MIT Licensed.
|
|
*
|
|
* This class is the blueprint for a weather provider.
|
|
*/
|
|
WeatherProvider.register("openweathermap", {
|
|
// Set the name of the provider.
|
|
// This isn't strictly necessary, since it will fallback to the provider identifier
|
|
// But for debugging (and future alerts) it would be nice to have the real name.
|
|
providerName: "OpenWeatherMap",
|
|
|
|
// Set the default config properties that is specific to this provider
|
|
defaults: {
|
|
apiVersion: "2.5",
|
|
apiBase: "https://api.openweathermap.org/data/",
|
|
weatherEndpoint: "", // can be "onecall", "forecast" or "weather" (for current)
|
|
locationID: false,
|
|
location: false,
|
|
lat: 0, // the onecall endpoint needs lat / lon values, it doesn't support the locationId
|
|
lon: 0,
|
|
apiKey: ""
|
|
},
|
|
|
|
// Overwrite the fetchCurrentWeather method.
|
|
fetchCurrentWeather() {
|
|
this.fetchData(this.getUrl())
|
|
.then((data) => {
|
|
let currentWeather;
|
|
if (this.config.weatherEndpoint === "/onecall") {
|
|
currentWeather = this.generateWeatherObjectsFromOnecall(data).current;
|
|
this.setFetchedLocation(`${data.timezone}`);
|
|
} else {
|
|
currentWeather = this.generateWeatherObjectFromCurrentWeather(data);
|
|
}
|
|
this.setCurrentWeather(currentWeather);
|
|
})
|
|
.catch(function (request) {
|
|
Log.error("Could not load data ... ", request);
|
|
})
|
|
.finally(() => this.updateAvailable());
|
|
},
|
|
|
|
// Overwrite the fetchWeatherForecast method.
|
|
fetchWeatherForecast() {
|
|
this.fetchData(this.getUrl())
|
|
.then((data) => {
|
|
let forecast;
|
|
let location;
|
|
if (this.config.weatherEndpoint === "/onecall") {
|
|
forecast = this.generateWeatherObjectsFromOnecall(data).days;
|
|
location = `${data.timezone}`;
|
|
} else {
|
|
forecast = this.generateWeatherObjectsFromForecast(data.list);
|
|
location = `${data.city.name}, ${data.city.country}`;
|
|
}
|
|
this.setWeatherForecast(forecast);
|
|
this.setFetchedLocation(location);
|
|
})
|
|
.catch(function (request) {
|
|
Log.error("Could not load data ... ", request);
|
|
})
|
|
.finally(() => this.updateAvailable());
|
|
},
|
|
|
|
// Overwrite the fetchWeatherHourly method.
|
|
fetchWeatherHourly() {
|
|
this.fetchData(this.getUrl())
|
|
.then((data) => {
|
|
if (!data) {
|
|
// Did not receive usable new data.
|
|
// Maybe this needs a better check?
|
|
return;
|
|
}
|
|
|
|
this.setFetchedLocation(`(${data.lat},${data.lon})`);
|
|
|
|
const weatherData = this.generateWeatherObjectsFromOnecall(data);
|
|
this.setWeatherHourly(weatherData.hours);
|
|
})
|
|
.catch(function (request) {
|
|
Log.error("Could not load data ... ", request);
|
|
})
|
|
.finally(() => this.updateAvailable());
|
|
},
|
|
|
|
/**
|
|
* Overrides method for setting config to check if endpoint is correct for hourly
|
|
*
|
|
* @param {object} config The configuration object
|
|
*/
|
|
setConfig(config) {
|
|
this.config = config;
|
|
if (!this.config.weatherEndpoint) {
|
|
switch (this.config.type) {
|
|
case "hourly":
|
|
this.config.weatherEndpoint = "/onecall";
|
|
break;
|
|
case "daily":
|
|
case "forecast":
|
|
this.config.weatherEndpoint = "/forecast";
|
|
break;
|
|
case "current":
|
|
this.config.weatherEndpoint = "/weather";
|
|
break;
|
|
default:
|
|
Log.error("weatherEndpoint not configured and could not resolve it based on type");
|
|
}
|
|
}
|
|
},
|
|
|
|
/** OpenWeatherMap Specific Methods - These are not part of the default provider methods */
|
|
/*
|
|
* Gets the complete url for the request
|
|
*/
|
|
getUrl() {
|
|
return this.config.apiBase + this.config.apiVersion + this.config.weatherEndpoint + this.getParams();
|
|
},
|
|
|
|
/*
|
|
* Generate a WeatherObject based on currentWeatherInformation
|
|
*/
|
|
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
|
const currentWeather = new WeatherObject();
|
|
|
|
currentWeather.date = moment.unix(currentWeatherData.dt);
|
|
currentWeather.humidity = currentWeatherData.main.humidity;
|
|
currentWeather.temperature = currentWeatherData.main.temp;
|
|
currentWeather.feelsLikeTemp = currentWeatherData.main.feels_like;
|
|
currentWeather.windSpeed = currentWeatherData.wind.speed;
|
|
currentWeather.windFromDirection = currentWeatherData.wind.deg;
|
|
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.weather[0].icon);
|
|
currentWeather.sunrise = moment.unix(currentWeatherData.sys.sunrise);
|
|
currentWeather.sunset = moment.unix(currentWeatherData.sys.sunset);
|
|
|
|
return currentWeather;
|
|
},
|
|
|
|
/*
|
|
* Generate WeatherObjects based on forecast information
|
|
*/
|
|
generateWeatherObjectsFromForecast(forecasts) {
|
|
if (this.config.weatherEndpoint === "/forecast") {
|
|
return this.fetchForecastHourly(forecasts);
|
|
} else if (this.config.weatherEndpoint === "/forecast/daily") {
|
|
return this.fetchForecastDaily(forecasts);
|
|
}
|
|
// if weatherEndpoint does not match forecast or forecast/daily, what should be returned?
|
|
return [new WeatherObject()];
|
|
},
|
|
|
|
/*
|
|
* Generate WeatherObjects based on One Call forecast information
|
|
*/
|
|
generateWeatherObjectsFromOnecall(data) {
|
|
if (this.config.weatherEndpoint === "/onecall") {
|
|
return this.fetchOnecall(data);
|
|
}
|
|
// if weatherEndpoint does not match onecall, what should be returned?
|
|
return { current: new WeatherObject(), hours: [], days: [] };
|
|
},
|
|
|
|
/*
|
|
* fetch forecast information for 3-hourly forecast (available for free subscription).
|
|
*/
|
|
fetchForecastHourly(forecasts) {
|
|
// initial variable declaration
|
|
const days = [];
|
|
// variables for temperature range and rain
|
|
let minTemp = [];
|
|
let maxTemp = [];
|
|
let rain = 0;
|
|
let snow = 0;
|
|
// variable for date
|
|
let date = "";
|
|
let weather = new WeatherObject();
|
|
|
|
for (const forecast of forecasts) {
|
|
if (date !== moment.unix(forecast.dt).format("YYYY-MM-DD")) {
|
|
// calculate minimum/maximum temperature, specify rain amount
|
|
weather.minTemperature = Math.min.apply(null, minTemp);
|
|
weather.maxTemperature = Math.max.apply(null, maxTemp);
|
|
weather.rain = rain;
|
|
weather.snow = snow;
|
|
weather.precipitationAmount = (weather.rain ?? 0) + (weather.snow ?? 0);
|
|
// push weather information to days array
|
|
days.push(weather);
|
|
// create new weather-object
|
|
weather = new WeatherObject();
|
|
|
|
minTemp = [];
|
|
maxTemp = [];
|
|
rain = 0;
|
|
snow = 0;
|
|
|
|
// set new date
|
|
date = moment.unix(forecast.dt).format("YYYY-MM-DD");
|
|
|
|
// specify date
|
|
weather.date = moment.unix(forecast.dt);
|
|
|
|
// If the first value of today is later than 17:00, we have an icon at least!
|
|
weather.weatherType = this.convertWeatherType(forecast.weather[0].icon);
|
|
}
|
|
|
|
if (moment.unix(forecast.dt).format("H") >= 8 && moment.unix(forecast.dt).format("H") <= 17) {
|
|
weather.weatherType = this.convertWeatherType(forecast.weather[0].icon);
|
|
}
|
|
|
|
// the same day as before
|
|
// add values from forecast to corresponding variables
|
|
minTemp.push(forecast.main.temp_min);
|
|
maxTemp.push(forecast.main.temp_max);
|
|
|
|
if (forecast.hasOwnProperty("rain") && !isNaN(forecast.rain["3h"])) {
|
|
rain += forecast.rain["3h"];
|
|
}
|
|
|
|
if (forecast.hasOwnProperty("snow") && !isNaN(forecast.snow["3h"])) {
|
|
snow += forecast.snow["3h"];
|
|
}
|
|
}
|
|
|
|
// last day
|
|
// calculate minimum/maximum temperature, specify rain amount
|
|
weather.minTemperature = Math.min.apply(null, minTemp);
|
|
weather.maxTemperature = Math.max.apply(null, maxTemp);
|
|
weather.rain = rain;
|
|
weather.snow = snow;
|
|
weather.precipitationAmount = (weather.rain ?? 0) + (weather.snow ?? 0);
|
|
// push weather information to days array
|
|
days.push(weather);
|
|
return days.slice(1);
|
|
},
|
|
|
|
/*
|
|
* fetch forecast information for daily forecast (available for paid subscription or old apiKey).
|
|
*/
|
|
fetchForecastDaily(forecasts) {
|
|
// initial variable declaration
|
|
const days = [];
|
|
|
|
for (const forecast of forecasts) {
|
|
const weather = new WeatherObject();
|
|
|
|
weather.date = moment.unix(forecast.dt);
|
|
weather.minTemperature = forecast.temp.min;
|
|
weather.maxTemperature = forecast.temp.max;
|
|
weather.weatherType = this.convertWeatherType(forecast.weather[0].icon);
|
|
weather.rain = 0;
|
|
weather.snow = 0;
|
|
|
|
// forecast.rain not available if amount is zero
|
|
// The API always returns in millimeters
|
|
if (forecast.hasOwnProperty("rain") && !isNaN(forecast.rain)) {
|
|
weather.rain = forecast.rain;
|
|
}
|
|
|
|
// forecast.snow not available if amount is zero
|
|
// The API always returns in millimeters
|
|
if (forecast.hasOwnProperty("snow") && !isNaN(forecast.snow)) {
|
|
weather.snow = forecast.snow;
|
|
}
|
|
|
|
weather.precipitationAmount = weather.rain + weather.snow;
|
|
weather.precipitationProbability = forecast.pop ? forecast.pop * 100 : undefined;
|
|
|
|
days.push(weather);
|
|
}
|
|
|
|
return days;
|
|
},
|
|
|
|
/*
|
|
* Fetch One Call forecast information (available for free subscription).
|
|
* Factors in timezone offsets.
|
|
* Minutely forecasts are excluded for the moment, see getParams().
|
|
*/
|
|
fetchOnecall(data) {
|
|
let precip = false;
|
|
|
|
// get current weather, if requested
|
|
const current = new WeatherObject();
|
|
if (data.hasOwnProperty("current")) {
|
|
current.date = moment.unix(data.current.dt).utcOffset(data.timezone_offset / 60);
|
|
current.windSpeed = data.current.wind_speed;
|
|
current.windFromDirection = data.current.wind_deg;
|
|
current.sunrise = moment.unix(data.current.sunrise).utcOffset(data.timezone_offset / 60);
|
|
current.sunset = moment.unix(data.current.sunset).utcOffset(data.timezone_offset / 60);
|
|
current.temperature = data.current.temp;
|
|
current.weatherType = this.convertWeatherType(data.current.weather[0].icon);
|
|
current.humidity = data.current.humidity;
|
|
if (data.current.hasOwnProperty("rain") && !isNaN(data.current["rain"]["1h"])) {
|
|
current.rain = data.current["rain"]["1h"];
|
|
precip = true;
|
|
}
|
|
if (data.current.hasOwnProperty("snow") && !isNaN(data.current["snow"]["1h"])) {
|
|
current.snow = data.current["snow"]["1h"];
|
|
precip = true;
|
|
}
|
|
if (precip) {
|
|
current.precipitationAmount = (current.rain ?? 0) + (current.snow ?? 0);
|
|
}
|
|
current.feelsLikeTemp = data.current.feels_like;
|
|
}
|
|
|
|
let weather = new WeatherObject();
|
|
|
|
// get hourly weather, if requested
|
|
const hours = [];
|
|
if (data.hasOwnProperty("hourly")) {
|
|
for (const hour of data.hourly) {
|
|
weather.date = moment.unix(hour.dt).utcOffset(data.timezone_offset / 60);
|
|
weather.temperature = hour.temp;
|
|
weather.feelsLikeTemp = hour.feels_like;
|
|
weather.humidity = hour.humidity;
|
|
weather.windSpeed = hour.wind_speed;
|
|
weather.windFromDirection = hour.wind_deg;
|
|
weather.weatherType = this.convertWeatherType(hour.weather[0].icon);
|
|
weather.precipitationProbability = hour.pop ? hour.pop * 100 : undefined;
|
|
precip = false;
|
|
if (hour.hasOwnProperty("rain") && !isNaN(hour.rain["1h"])) {
|
|
weather.rain = hour.rain["1h"];
|
|
precip = true;
|
|
}
|
|
if (hour.hasOwnProperty("snow") && !isNaN(hour.snow["1h"])) {
|
|
weather.snow = hour.snow["1h"];
|
|
precip = true;
|
|
}
|
|
if (precip) {
|
|
weather.precipitationAmount = (weather.rain ?? 0) + (weather.snow ?? 0);
|
|
}
|
|
|
|
hours.push(weather);
|
|
weather = new WeatherObject();
|
|
}
|
|
}
|
|
|
|
// get daily weather, if requested
|
|
const days = [];
|
|
if (data.hasOwnProperty("daily")) {
|
|
for (const day of data.daily) {
|
|
weather.date = moment.unix(day.dt).utcOffset(data.timezone_offset / 60);
|
|
weather.sunrise = moment.unix(day.sunrise).utcOffset(data.timezone_offset / 60);
|
|
weather.sunset = moment.unix(day.sunset).utcOffset(data.timezone_offset / 60);
|
|
weather.minTemperature = day.temp.min;
|
|
weather.maxTemperature = day.temp.max;
|
|
weather.humidity = day.humidity;
|
|
weather.windSpeed = day.wind_speed;
|
|
weather.windFromDirection = day.wind_deg;
|
|
weather.weatherType = this.convertWeatherType(day.weather[0].icon);
|
|
weather.precipitationProbability = day.pop ? day.pop * 100 : undefined;
|
|
precip = false;
|
|
if (!isNaN(day.rain)) {
|
|
weather.rain = day.rain;
|
|
precip = true;
|
|
}
|
|
if (!isNaN(day.snow)) {
|
|
weather.snow = day.snow;
|
|
precip = true;
|
|
}
|
|
if (precip) {
|
|
weather.precipitationAmount = (weather.rain ?? 0) + (weather.snow ?? 0);
|
|
}
|
|
|
|
days.push(weather);
|
|
weather = new WeatherObject();
|
|
}
|
|
}
|
|
|
|
return { current: current, hours: hours, days: days };
|
|
},
|
|
|
|
/*
|
|
* Convert the OpenWeatherMap icons to a more usable name.
|
|
*/
|
|
convertWeatherType(weatherType) {
|
|
const weatherTypes = {
|
|
"01d": "day-sunny",
|
|
"02d": "day-cloudy",
|
|
"03d": "cloudy",
|
|
"04d": "cloudy-windy",
|
|
"09d": "showers",
|
|
"10d": "rain",
|
|
"11d": "thunderstorm",
|
|
"13d": "snow",
|
|
"50d": "fog",
|
|
"01n": "night-clear",
|
|
"02n": "night-cloudy",
|
|
"03n": "night-cloudy",
|
|
"04n": "night-cloudy",
|
|
"09n": "night-showers",
|
|
"10n": "night-rain",
|
|
"11n": "night-thunderstorm",
|
|
"13n": "night-snow",
|
|
"50n": "night-alt-cloudy-windy"
|
|
};
|
|
|
|
return weatherTypes.hasOwnProperty(weatherType) ? weatherTypes[weatherType] : null;
|
|
},
|
|
|
|
/* getParams(compliments)
|
|
* Generates an url with api parameters based on the config.
|
|
*
|
|
* return String - URL params.
|
|
*/
|
|
getParams() {
|
|
let params = "?";
|
|
if (this.config.weatherEndpoint === "/onecall") {
|
|
params += "lat=" + this.config.lat;
|
|
params += "&lon=" + this.config.lon;
|
|
if (this.config.type === "current") {
|
|
params += "&exclude=minutely,hourly,daily";
|
|
} else if (this.config.type === "hourly") {
|
|
params += "&exclude=current,minutely,daily";
|
|
} else if (this.config.type === "daily" || this.config.type === "forecast") {
|
|
params += "&exclude=current,minutely,hourly";
|
|
} else {
|
|
params += "&exclude=minutely";
|
|
}
|
|
} else if (this.config.lat && this.config.lon) {
|
|
params += "lat=" + this.config.lat + "&lon=" + this.config.lon;
|
|
} else if (this.config.locationID) {
|
|
params += "id=" + this.config.locationID;
|
|
} else if (this.config.location) {
|
|
params += "q=" + this.config.location;
|
|
} else if (this.firstEvent && this.firstEvent.geo) {
|
|
params += "lat=" + this.firstEvent.geo.lat + "&lon=" + this.firstEvent.geo.lon;
|
|
} else if (this.firstEvent && this.firstEvent.location) {
|
|
params += "q=" + this.firstEvent.location;
|
|
} else {
|
|
this.hide(this.config.animationSpeed, { lockString: this.identifier });
|
|
return;
|
|
}
|
|
|
|
params += "&units=metric"; // WeatherProviders should use metric internally and use the units only for when displaying data
|
|
params += "&lang=" + this.config.lang;
|
|
params += "&APPID=" + this.config.apiKey;
|
|
|
|
return params;
|
|
}
|
|
});
|