481 lines
16 KiB
JavaScript
Raw Normal View History

/* global WeatherProvider, WeatherObject */
/* MagicMirror²
* Module: Weather
*
2020-04-28 23:05:28 +02:00
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
2019-01-05 17:16:19 +01:00
*
* This class is the blueprint for a weather provider.
*/
WeatherProvider.register("openweathermap", {
// Set the name of the provider.
2018-12-27 17:14:03 +01:00
// 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() {
2018-12-27 17:14:03 +01:00
this.fetchData(this.getUrl())
.then((data) => {
if (this.config.weatherEndpoint === "/onecall") {
const weatherData = this.generateWeatherObjectsFromOnecall(data);
this.setCurrentWeather(weatherData.current);
this.setFetchedLocation(`${data.timezone}`);
} else {
const currentWeather = this.generateWeatherObjectFromCurrentWeather(data);
this.setCurrentWeather(currentWeather);
}
})
.catch(function (request) {
2018-12-27 19:37:02 +01:00
Log.error("Could not load data ... ", request);
})
2020-04-20 11:15:11 +02:00
.finally(() => this.updateAvailable());
},
// Overwrite the fetchWeatherForecast method.
fetchWeatherForecast() {
2018-12-27 17:14:03 +01:00
this.fetchData(this.getUrl())
.then((data) => {
if (this.config.weatherEndpoint === "/onecall") {
const weatherData = this.generateWeatherObjectsFromOnecall(data);
this.setWeatherForecast(weatherData.days);
this.setFetchedLocation(`${data.timezone}`);
} else {
const forecast = this.generateWeatherObjectsFromForecast(data.list);
this.setWeatherForecast(forecast);
this.setFetchedLocation(`${data.city.name}, ${data.city.country}`);
2018-12-27 17:14:03 +01:00
}
})
.catch(function (request) {
2018-12-27 19:37:02 +01:00
Log.error("Could not load data ... ", request);
2018-12-27 17:14:03 +01:00
})
2020-04-20 11:15:11 +02:00
.finally(() => this.updateAvailable());
2017-09-22 13:26:44 +02:00
},
// 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
*
2021-08-01 09:53:28 +02:00
* @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 */
2018-12-27 17:14:03 +01:00
/*
* Gets the complete url for the request
*/
getUrl() {
2018-12-27 19:37:02 +01:00
return this.config.apiBase + this.config.apiVersion + this.config.weatherEndpoint + this.getParams();
2018-12-27 17:14:03 +01:00
},
2019-01-05 17:16:19 +01:00
/*
* Generate a WeatherObject based on currentWeatherInformation
*/
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
2018-12-27 19:37:02 +01:00
currentWeather.humidity = currentWeatherData.main.humidity;
currentWeather.temperature = currentWeatherData.main.temp;
currentWeather.feelsLikeTemp = currentWeatherData.main.feels_like;
currentWeather.windSpeed = currentWeatherData.wind.speed;
2018-12-27 19:37:02 +01:00
currentWeather.windDirection = 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);
2018-12-27 19:37:02 +01:00
return currentWeather;
},
2018-12-27 17:14:03 +01:00
/*
* Generate WeatherObjects based on forecast information
*/
generateWeatherObjectsFromForecast(forecasts) {
2019-06-05 09:32:10 +02:00
if (this.config.weatherEndpoint === "/forecast") {
return this.fetchForecastHourly(forecasts);
2019-06-05 09:32:10 +02:00
} 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(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh)];
},
2019-01-05 17:16:19 +01:00
/*
* 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(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh), hours: [], days: [] };
},
/*
* fetch forecast information for 3-hourly forecast (available for free subscription).
*/
fetchForecastHourly(forecasts) {
2019-01-04 12:13:39 +01:00
// initial variable declaration
const days = [];
2019-01-04 12:13:39 +01:00
// variables for temperature range and rain
2019-02-27 09:09:37 -06:00
let minTemp = [];
let maxTemp = [];
let rain = 0;
let snow = 0;
2019-01-04 12:13:39 +01:00
// variable for date
let date = "";
let weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
2019-01-05 17:16:19 +01:00
for (const forecast of forecasts) {
if (date !== moment.unix(forecast.dt).format("YYYY-MM-DD")) {
2019-01-04 12:13:39 +01:00
// 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.precipitation = weather.rain + weather.snow;
2019-01-04 12:13:39 +01:00
// push weather information to days array
days.push(weather);
2019-01-04 12:13:39 +01:00
// create new weather-object
weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
2019-01-05 17:16:19 +01:00
minTemp = [];
maxTemp = [];
rain = 0;
snow = 0;
2019-01-05 17:16:19 +01:00
2019-01-04 12:13:39 +01:00
// set new date
date = moment.unix(forecast.dt).format("YYYY-MM-DD");
2019-01-05 17:16:19 +01:00
2019-01-04 12:13:39 +01:00
// specify date
weather.date = moment.unix(forecast.dt);
2019-01-05 17:16:19 +01:00
2019-02-14 16:48:45 +01:00
// 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) {
2019-02-14 16:48:45 +01:00
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")) {
if (this.config.units === "imperial" && !isNaN(forecast.rain["3h"])) {
rain += forecast.rain["3h"] / 25.4;
} else if (!isNaN(forecast.rain["3h"])) {
rain += forecast.rain["3h"];
}
}
if (forecast.hasOwnProperty("snow")) {
if (this.config.units === "imperial" && !isNaN(forecast.snow["3h"])) {
snow += forecast.snow["3h"] / 25.4;
} else if (!isNaN(forecast.snow["3h"])) {
snow += forecast.snow["3h"];
}
}
2018-12-27 17:14:03 +01:00
}
// 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.precipitation = weather.rain + weather.snow;
// push weather information to days array
days.push(weather);
return days.slice(1);
2018-12-27 17:14:03 +01:00
},
2019-01-05 17:16:19 +01:00
/*
* fetch forecast information for daily forecast (available for paid subscription or old apiKey).
*/
fetchForecastDaily(forecasts) {
// initial variable declaration
const days = [];
2018-12-27 17:14:03 +01:00
for (const forecast of forecasts) {
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
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;
2019-01-05 17:16:19 +01:00
// forecast.rain not available if amount is zero
// The API always returns in millimeters
if (forecast.hasOwnProperty("rain")) {
if (this.config.units === "imperial" && !isNaN(forecast.rain)) {
weather.rain = forecast.rain / 25.4;
} else if (!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")) {
if (this.config.units === "imperial" && !isNaN(forecast.snow)) {
weather.snow = forecast.snow / 25.4;
} else if (!isNaN(forecast.snow)) {
weather.snow = forecast.snow;
}
}
weather.precipitation = weather.rain + weather.snow;
days.push(weather);
}
2020-04-20 11:15:11 +02:00
return days;
},
2019-01-05 17:16:19 +01:00
/*
* 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(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
if (data.hasOwnProperty("current")) {
current.date = moment.unix(data.current.dt).utcOffset(data.timezone_offset / 60);
current.windSpeed = data.current.wind_speed;
current.windDirection = 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"])) {
if (this.config.units === "imperial") {
current.rain = data.current["rain"]["1h"] / 25.4;
} else {
current.rain = data.current["rain"]["1h"];
}
precip = true;
}
if (data.current.hasOwnProperty("snow") && !isNaN(data.current["snow"]["1h"])) {
if (this.config.units === "imperial") {
current.snow = data.current["snow"]["1h"] / 25.4;
} else {
current.snow = data.current["snow"]["1h"];
}
precip = true;
}
if (precip) {
2020-08-01 14:03:48 -04:00
current.precipitation = current.rain + current.snow;
}
current.feelsLikeTemp = data.current.feels_like;
}
let weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
// 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.windDirection = hour.wind_deg;
weather.weatherType = this.convertWeatherType(hour.weather[0].icon);
precip = false;
if (hour.hasOwnProperty("rain") && !isNaN(hour.rain["1h"])) {
if (this.config.units === "imperial") {
weather.rain = hour.rain["1h"] / 25.4;
} else {
weather.rain = hour.rain["1h"];
}
precip = true;
}
if (hour.hasOwnProperty("snow") && !isNaN(hour.snow["1h"])) {
if (this.config.units === "imperial") {
weather.snow = hour.snow["1h"] / 25.4;
} else {
weather.snow = hour.snow["1h"];
}
precip = true;
}
if (precip) {
2020-08-01 14:03:48 -04:00
weather.precipitation = weather.rain + weather.snow;
}
hours.push(weather);
weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
}
}
// 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.windDirection = day.wind_deg;
weather.weatherType = this.convertWeatherType(day.weather[0].icon);
precip = false;
if (!isNaN(day.rain)) {
if (this.config.units === "imperial") {
weather.rain = day.rain / 25.4;
} else {
weather.rain = day.rain;
}
precip = true;
}
if (!isNaN(day.snow)) {
if (this.config.units === "imperial") {
weather.snow = day.snow / 25.4;
} else {
weather.snow = day.snow;
}
precip = true;
}
if (precip) {
2020-08-01 14:03:48 -04:00
weather.precipitation = weather.rain + weather.snow;
}
days.push(weather);
weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
}
}
2020-08-01 14:03:48 -04:00
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"
2018-12-27 19:37:02 +01:00
};
2018-12-27 19:37:02 +01:00
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) {
2018-12-27 19:37:02 +01:00
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=" + this.config.units;
params += "&lang=" + this.config.lang;
2020-06-30 12:36:23 -04:00
params += "&APPID=" + this.config.apiKey;
return params;
2018-12-27 19:37:02 +01:00
}
});