469 lines
14 KiB
JavaScript
Raw Normal View History

2016-03-29 13:28:15 +02:00
/* Magic Mirror
* Module: WeatherForecast
*
2020-04-28 23:05:28 +02:00
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
2016-04-05 14:35:11 -04:00
Module.register("weatherforecast",{
2016-03-29 13:28:15 +02:00
// Default module config.
2016-03-29 13:28:15 +02:00
defaults: {
location: false,
locationID: false,
2016-04-05 14:35:11 -04:00
appid: "",
2016-04-24 15:24:44 -05:00
units: config.units,
maxNumberOfDays: 7,
2016-10-14 23:07:13 +02:00
showRainAmount: false,
2016-04-05 14:35:11 -04:00
updateInterval: 10 * 60 * 1000, // every 10 minutes
animationSpeed: 1000,
timeFormat: config.timeFormat,
2016-03-29 13:28:15 +02:00
lang: config.language,
decimalSymbol: ".",
fade: true,
2016-03-29 16:10:50 +02:00
fadePoint: 0.25, // Start on 1/4th of the list.
2017-01-29 00:59:38 +01:00
colored: false,
2017-08-16 12:11:57 +02:00
scale: false,
2016-03-29 13:28:15 +02:00
2016-03-30 15:13:02 +02:00
initialLoadDelay: 2500, // 2.5 seconds delay. This delay is used to keep the OpenWeather API happy.
2016-03-29 13:28:15 +02:00
retryDelay: 2500,
2016-04-05 14:35:11 -04:00
apiVersion: "2.5",
apiBase: "https://api.openweathermap.org/data/",
2016-04-05 14:35:11 -04:00
forecastEndpoint: "forecast/daily",
2016-03-29 13:28:15 +02:00
appendLocationNameToHeader: true,
calendarClass: "calendar",
tableClass: "small",
roundTemp: false,
2016-03-29 13:28:15 +02:00
iconTable: {
2016-04-05 14:35:11 -04:00
"01d": "wi-day-sunny",
"02d": "wi-day-cloudy",
"03d": "wi-cloudy",
"04d": "wi-cloudy-windy",
"09d": "wi-showers",
"10d": "wi-rain",
"11d": "wi-thunderstorm",
"13d": "wi-snow",
"50d": "wi-fog",
"01n": "wi-night-clear",
"02n": "wi-night-cloudy",
"03n": "wi-night-cloudy",
"04n": "wi-night-cloudy",
"09n": "wi-night-showers",
"10n": "wi-night-rain",
"11n": "wi-night-thunderstorm",
"13n": "wi-night-snow",
"50n": "wi-night-alt-cloudy-windy"
2016-03-29 13:28:15 +02:00
},
},
2020-04-22 22:07:03 +02:00
// create a variable for the first upcoming calendar event. Used if no location is specified.
firstEvent: false,
// create a variable to hold the location name based on the API result.
fetchedLocationName: "",
2016-03-29 13:28:15 +02:00
// Define required scripts.
getScripts: function() {
2016-04-05 14:35:11 -04:00
return ["moment.js"];
2016-03-29 13:28:15 +02:00
},
// Define required scripts.
getStyles: function() {
2016-04-05 14:35:11 -04:00
return ["weather-icons.css", "weatherforecast.css"];
2016-03-29 13:28:15 +02:00
},
2016-05-11 12:38:41 +02:00
// Define required translations.
getTranslations: function() {
2017-03-30 22:14:11 +02:00
// The translations for the default modules are defined in the core translation files.
// Therefor we can just return false. Otherwise we should have returned a dictionary.
2019-06-05 09:46:59 +02:00
// If you're trying to build your own module including translations, check out the documentation.
2016-05-11 12:38:41 +02:00
return false;
},
2016-03-29 13:28:15 +02:00
// Define start sequence.
start: function() {
2016-04-05 14:35:11 -04:00
Log.info("Starting module: " + this.name);
2016-03-29 13:28:15 +02:00
// Set locale.
moment.locale(config.language);
2016-03-29 13:28:15 +02:00
this.forecast = [];
this.loaded = false;
this.scheduleUpdate(this.config.initialLoadDelay);
2019-02-09 13:51:23 -08:00
this.updateTimer = null;
2016-03-29 13:28:15 +02:00
},
// Override dom generator.
getDom: function() {
var wrapper = document.createElement("div");
2016-04-05 14:35:11 -04:00
if (this.config.appid === "") {
2016-03-29 13:28:15 +02:00
wrapper.innerHTML = "Please set the correct openweather <i>appid</i> in the config for module: " + this.name + ".";
wrapper.className = "dimmed light small";
return wrapper;
}
if (!this.loaded) {
wrapper.innerHTML = this.translate("LOADING");
2016-03-29 13:28:15 +02:00
wrapper.className = "dimmed light small";
return wrapper;
}
var table = document.createElement("table");
table.className = this.config.tableClass;
2016-03-29 13:28:15 +02:00
for (var f in this.forecast) {
var forecast = this.forecast[f];
var row = document.createElement("tr");
2017-01-29 00:59:38 +01:00
if (this.config.colored) {
row.className = "colored";
}
2016-03-29 13:28:15 +02:00
table.appendChild(row);
var dayCell = document.createElement("td");
2016-04-05 14:35:11 -04:00
dayCell.className = "day";
2016-03-29 13:28:15 +02:00
dayCell.innerHTML = forecast.day;
row.appendChild(dayCell);
var iconCell = document.createElement("td");
iconCell.className = "bright weather-icon";
row.appendChild(iconCell);
var icon = document.createElement("span");
icon.className = "wi weathericon " + forecast.icon;
2016-03-29 13:28:15 +02:00
iconCell.appendChild(icon);
var degreeLabel = "";
2019-01-05 13:13:53 +01:00
if (this.config.units === "metric" || this.config.units === "imperial") {
degreeLabel += "°";
}
if(this.config.scale) {
switch(this.config.units) {
2017-08-31 18:36:52 +02:00
case "metric":
2019-01-05 13:13:53 +01:00
degreeLabel += "C";
2017-08-31 18:36:52 +02:00
break;
case "imperial":
2019-01-05 13:13:53 +01:00
degreeLabel += "F";
2017-08-31 18:36:52 +02:00
break;
case "default":
2019-01-05 13:13:53 +01:00
degreeLabel = "K";
2017-08-31 18:36:52 +02:00
break;
}
}
if (this.config.decimalSymbol === "" || this.config.decimalSymbol === " ") {
this.config.decimalSymbol = ".";
}
var maxTempCell = document.createElement("td");
2018-01-01 13:38:07 +01:00
maxTempCell.innerHTML = forecast.maxTemp.replace(".", this.config.decimalSymbol) + degreeLabel;
2016-04-05 14:35:11 -04:00
maxTempCell.className = "align-right bright max-temp";
2016-03-29 13:28:15 +02:00
row.appendChild(maxTempCell);
var minTempCell = document.createElement("td");
2018-01-01 13:38:07 +01:00
minTempCell.innerHTML = forecast.minTemp.replace(".", this.config.decimalSymbol) + degreeLabel;
2016-04-05 14:35:11 -04:00
minTempCell.className = "align-right min-temp";
2016-03-29 13:28:15 +02:00
row.appendChild(minTempCell);
2016-10-14 23:07:13 +02:00
if (this.config.showRainAmount) {
var rainCell = document.createElement("td");
if (isNaN(forecast.rain)) {
rainCell.innerHTML = "";
} else {
2017-01-15 21:16:01 +01:00
if(config.units !== "imperial") {
rainCell.innerHTML = parseFloat(forecast.rain).toFixed(1).replace(".", this.config.decimalSymbol) + " mm";
2017-01-15 21:16:01 +01:00
} else {
rainCell.innerHTML = (parseFloat(forecast.rain) / 25.4).toFixed(2).replace(".", this.config.decimalSymbol) + " in";
2017-01-15 21:16:01 +01:00
}
2016-10-14 23:07:13 +02:00
}
rainCell.className = "align-right bright rain";
row.appendChild(rainCell);
}
2016-03-29 16:10:50 +02:00
if (this.config.fade && this.config.fadePoint < 1) {
if (this.config.fadePoint < 0) {
this.config.fadePoint = 0;
}
var startingPoint = this.forecast.length * this.config.fadePoint;
var steps = this.forecast.length - startingPoint;
if (f >= startingPoint) {
var currentStep = f - startingPoint;
row.style.opacity = 1 - (1 / steps * currentStep);
}
}
2016-03-29 13:28:15 +02:00
}
2016-03-29 13:28:15 +02:00
return table;
},
// Override getHeader method.
getHeader: function() {
if (this.config.appendLocationNameToHeader) {
return this.data.header + " " + this.fetchedLocationName;
}
return this.data.header;
},
// Override notification handler.
notificationReceived: function(notification, payload, sender) {
if (notification === "DOM_OBJECTS_CREATED") {
if (this.config.appendLocationNameToHeader) {
this.hide(0, {lockString: this.identifier});
}
}
if (notification === "CALENDAR_EVENTS") {
var senderClasses = sender.data.classes.toLowerCase().split(" ");
if (senderClasses.indexOf(this.config.calendarClass.toLowerCase()) !== -1) {
this.firstEvent = false;
for (var e in payload) {
var event = payload[e];
if (event.location || event.geo) {
this.firstEvent = event;
//Log.log("First upcoming event with location: ", event);
break;
}
}
}
}
},
2016-03-29 13:28:15 +02:00
/* updateWeather(compliments)
* Requests new data from openweather.org.
2019-06-05 09:46:59 +02:00
* Calls processWeather on successful response.
*/
2016-03-29 13:28:15 +02:00
updateWeather: function() {
if (this.config.appid === "") {
Log.error("WeatherForecast: APPID not set!");
return;
}
var url = this.config.apiBase + this.config.apiVersion + "/" + this.config.forecastEndpoint + this.getParams();
2016-03-29 13:28:15 +02:00
var self = this;
var retry = true;
var weatherRequest = new XMLHttpRequest();
weatherRequest.open("GET", url, true);
weatherRequest.onreadystatechange = function() {
2016-04-05 14:35:11 -04:00
if (this.readyState === 4) {
if (this.status === 200) {
self.processWeather(JSON.parse(this.response));
} else if (this.status === 401) {
self.updateDom(self.config.animationSpeed);
2019-06-05 09:32:10 +02:00
if (self.config.forecastEndpoint === "forecast/daily") {
2017-09-12 10:28:33 -03:00
self.config.forecastEndpoint = "forecast";
2017-09-29 10:04:42 +02:00
Log.warn(self.name + ": Your AppID does not support long term forecasts. Switching to fallback endpoint.");
2017-09-12 10:28:33 -03:00
}
retry = true;
2016-04-05 14:35:11 -04:00
} else {
Log.error(self.name + ": Could not load weather.");
}
2016-03-29 13:28:15 +02:00
2016-04-05 14:35:11 -04:00
if (retry) {
self.scheduleUpdate((self.loaded) ? -1 : self.config.retryDelay);
}
2016-03-29 13:28:15 +02:00
}
};
weatherRequest.send();
},
/* getParams(compliments)
* Generates an url with api parameters based on the config.
*
* return String - URL params.
*/
2016-03-29 13:28:15 +02:00
getParams: function() {
var params = "?";
if(this.config.locationID) {
2016-05-25 15:23:29 -05:00
params += "id=" + this.config.locationID;
} else if(this.config.location) {
2016-05-25 15:23:29 -05:00
params += "q=" + this.config.location;
} else if (this.firstEvent && this.firstEvent.geo) {
2019-06-05 10:23:58 +02: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;
2016-05-25 15:23:29 -05:00
}
2016-04-05 14:35:11 -04:00
params += "&units=" + this.config.units;
params += "&lang=" + this.config.lang;
params += "&APPID=" + this.config.appid;
2016-03-29 13:28:15 +02:00
return params;
},
2017-09-12 10:28:33 -03:00
/*
* parserDataWeather(data)
*
* Use the parse to keep the same struct between daily and forecast Endpoint
2020-04-22 22:07:03 +02:00
* from openweather.org
*
*/
2017-09-12 10:28:33 -03:00
parserDataWeather: function(data) {
if (data.hasOwnProperty("main")) {
2019-06-05 10:23:58 +02:00
data["temp"] = {"min": data.main.temp_min, "max": data.main.temp_max};
2017-09-12 10:28:33 -03:00
}
return data;
},
2016-03-29 13:28:15 +02:00
/* processWeather(data)
* Uses the received data to set the various values.
*
* argument data object - Weather information received form openweather.org.
*/
2016-03-29 13:28:15 +02:00
processWeather: function(data) {
this.fetchedLocationName = data.city.name + ", " + data.city.country;
2016-03-29 13:28:15 +02:00
this.forecast = [];
2017-09-29 10:04:42 +02:00
var lastDay = null;
2019-06-05 10:23:58 +02:00
var forecastData = {};
2017-09-29 10:04:42 +02:00
2016-03-29 13:28:15 +02:00
for (var i = 0, count = data.list.length; i < count; i++) {
var forecast = data.list[i];
2017-09-12 10:28:33 -03:00
this.parserDataWeather(forecast); // hack issue #1017
2016-03-29 13:28:15 +02:00
var day;
var hour;
2020-04-20 22:09:49 +02:00
if(forecast.dt_txt) {
day = moment(forecast.dt_txt, "YYYY-MM-DD hh:mm:ss").format("ddd");
hour = moment(forecast.dt_txt, "YYYY-MM-DD hh:mm:ss").format("H");
} else {
day = moment(forecast.dt, "X").format("ddd");
hour = moment(forecast.dt, "X").format("H");
}
2017-09-29 10:04:42 +02:00
if (day !== lastDay) {
2020-04-21 07:36:18 +02:00
forecastData = {
2017-09-29 10:04:42 +02:00
day: day,
icon: this.config.iconTable[forecast.weather[0].icon],
maxTemp: this.roundValue(forecast.temp.max),
minTemp: this.roundValue(forecast.temp.min),
rain: this.processRain(forecast, data.list)
2017-09-29 10:04:42 +02:00
};
this.forecast.push(forecastData);
lastDay = day;
// Stop processing when maxNumberOfDays is reached
if (this.forecast.length === this.config.maxNumberOfDays) {
break;
}
2017-09-29 10:04:42 +02:00
} else {
//Log.log("Compare max: ", forecast.temp.max, parseFloat(forecastData.maxTemp));
forecastData.maxTemp = forecast.temp.max > parseFloat(forecastData.maxTemp) ? this.roundValue(forecast.temp.max) : forecastData.maxTemp;
//Log.log("Compare min: ", forecast.temp.min, parseFloat(forecastData.minTemp));
forecastData.minTemp = forecast.temp.min < parseFloat(forecastData.minTemp) ? this.roundValue(forecast.temp.min) : forecastData.minTemp;
// Since we don't want an icon from the start of the day (in the middle of the night)
// we update the icon as long as it's somewhere during the day.
if (hour >= 8 && hour <= 17) {
forecastData.icon = this.config.iconTable[forecast.weather[0].icon];
}
}
2016-03-29 13:28:15 +02:00
}
//Log.log(this.forecast);
this.show(this.config.animationSpeed, {lockString:this.identifier});
2016-03-29 13:28:15 +02:00
this.loaded = true;
this.updateDom(this.config.animationSpeed);
},
/* scheduleUpdate()
* Schedule next update.
*
* argument delay number - Milliseconds before next update. If empty, this.config.updateInterval is used.
*/
2016-03-29 13:28:15 +02:00
scheduleUpdate: function(delay) {
var nextLoad = this.config.updateInterval;
2016-04-05 14:35:11 -04:00
if (typeof delay !== "undefined" && delay >= 0) {
2016-03-29 13:28:15 +02:00
nextLoad = delay;
}
2016-03-29 13:28:15 +02:00
var self = this;
clearTimeout(this.updateTimer);
this.updateTimer = setTimeout(function() {
self.updateWeather();
}, nextLoad);
},
/* ms2Beaufort(ms)
* Converts m2 to beaufort (windspeed).
*
* see:
2020-04-28 23:05:28 +02:00
* https://www.spc.noaa.gov/faq/tornado/beaufort.html
* https://en.wikipedia.org/wiki/Beaufort_scale#Modern_scale
*
* argument ms number - Windspeed in m/s.
*
* return number - Windspeed in beaufort.
*/
2016-03-29 13:28:15 +02:00
ms2Beaufort: function(ms) {
var kmh = ms * 60 * 60 / 1000;
var speeds = [1, 5, 11, 19, 28, 38, 49, 61, 74, 88, 102, 117, 1000];
for (var beaufort in speeds) {
var speed = speeds[beaufort];
if (speed > kmh) {
return beaufort;
}
}
return 12;
},
/* function(temperature)
* Rounds a temperature to 1 decimal or integer (depending on config.roundTemp).
*
* argument temperature number - Temperature.
*
* return string - Rounded Temperature.
*/
2016-04-05 14:35:11 -04:00
roundValue: function(temperature) {
var decimals = this.config.roundTemp ? 0 : 1;
return parseFloat(temperature).toFixed(decimals);
},
/* processRain(forecast, allForecasts)
* Calculates the amount of rain for a whole day even if long term forecasts isn't available for the appid.
*
* When using the the fallback endpoint forecasts are provided in 3h intervals and the rain-property is an object instead of number.
* That object has a property "3h" which contains the amount of rain since the previous forecast in the list.
* This code finds all forecasts that is for the same day and sums the amount of rain and returns that.
*/
processRain: function(forecast, allForecasts) {
//If the amount of rain actually is a number, return it
if (!isNaN(forecast.rain)) {
return forecast.rain;
}
//Find all forecasts that is for the same day
2020-04-20 22:09:49 +02:00
var checkDateTime = (forecast.dt_txt) ? moment(forecast.dt_txt, "YYYY-MM-DD hh:mm:ss") : moment(forecast.dt, "X");
var daysForecasts = allForecasts.filter(function(item) {
2020-04-20 22:09:49 +02:00
var itemDateTime = (item.dt_txt) ? moment(item.dt_txt, "YYYY-MM-DD hh:mm:ss") : moment(item.dt, "X");
return itemDateTime.isSame(checkDateTime, "day") && item.rain instanceof Object;
});
//If no rain this day return undefined so it wont be displayed for this day
if (daysForecasts.length === 0) {
return undefined;
}
//Summarize all the rain from the matching days
return daysForecasts.map(function(item) {
return Object.values(item.rain)[0];
}).reduce(function(a, b) {
return a + b;
}, 0);
2016-03-29 13:28:15 +02:00
}
});