mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-06-27 03:39:55 +00:00
450 lines
12 KiB
JavaScript
450 lines
12 KiB
JavaScript
/* global Module */
|
|
|
|
/* Magic Mirror
|
|
* Module: WeatherForecast
|
|
*
|
|
* By Michael Teeuw http://michaelteeuw.nl
|
|
* MIT Licensed.
|
|
*/
|
|
|
|
Module.register("weatherforecast",{
|
|
|
|
// Default module config.
|
|
defaults: {
|
|
autoLocation: false,
|
|
location: false,
|
|
locationID: false,
|
|
appid: "",
|
|
units: config.units,
|
|
maxNumberOfDays: 7,
|
|
showRainAmount: false,
|
|
updateInterval: 10 * 60 * 1000, // every 10 minutes
|
|
animationSpeed: 1000,
|
|
timeFormat: config.timeFormat,
|
|
lang: config.language,
|
|
decimalSymbol: ".",
|
|
fade: true,
|
|
fadePoint: 0.25, // Start on 1/4th of the list.
|
|
colored: false,
|
|
scale: false,
|
|
|
|
initialLoadDelay: 2500, // 2.5 seconds delay. This delay is used to keep the OpenWeather API happy.
|
|
retryDelay: 2500,
|
|
|
|
apiVersion: "2.5",
|
|
apiBase: "https://api.openweathermap.org/data/",
|
|
forecastEndpoint: "forecast/daily",
|
|
|
|
appendLocationNameToHeader: true,
|
|
calendarClass: "calendar",
|
|
tableClass: "small",
|
|
|
|
roundTemp: false,
|
|
|
|
iconTable: {
|
|
"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"
|
|
},
|
|
},
|
|
|
|
// create a variable for the first upcoming calendaar event. Used if no location is specified.
|
|
firstEvent: false,
|
|
|
|
// create a variable to hold the location name based on the API result.
|
|
fetchedLocationName: "",
|
|
|
|
// Define required scripts.
|
|
getScripts: function() {
|
|
return ["moment.js"];
|
|
},
|
|
|
|
// Define required scripts.
|
|
getStyles: function() {
|
|
return ["weather-icons.css", "weatherforecast.css"];
|
|
},
|
|
|
|
// Define required translations.
|
|
getTranslations: function() {
|
|
// 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.
|
|
// If you're trying to build yiur own module including translations, check out the documentation.
|
|
return false;
|
|
},
|
|
|
|
// Define start sequence.
|
|
start: function() {
|
|
Log.info("Starting module: " + this.name);
|
|
|
|
// Set locale.
|
|
moment.locale(config.language);
|
|
|
|
this.forecast = [];
|
|
this.loaded = false;
|
|
this.updateTimer = null;
|
|
|
|
if (this.config.autoLocation) {
|
|
this.sendSocketNotification("AUTO_LOCATION");
|
|
} else {
|
|
this.scheduleUpdate(this.config.initialLoadDelay);
|
|
}
|
|
},
|
|
|
|
socketNotificationReceived: function (notification, payload) {
|
|
if (notification === "UPDATE_LOCATION") {
|
|
this.config.location = payload.location;
|
|
this.scheduleUpdate(this.config.initialLoadDelay);
|
|
}
|
|
},
|
|
|
|
// Override dom generator.
|
|
getDom: function() {
|
|
var wrapper = document.createElement("div");
|
|
|
|
if (this.config.appid === "") {
|
|
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");
|
|
wrapper.className = "dimmed light small";
|
|
return wrapper;
|
|
}
|
|
|
|
var table = document.createElement("table");
|
|
table.className = this.config.tableClass;
|
|
|
|
for (var f in this.forecast) {
|
|
var forecast = this.forecast[f];
|
|
|
|
var row = document.createElement("tr");
|
|
if (this.config.colored) {
|
|
row.className = "colored";
|
|
}
|
|
table.appendChild(row);
|
|
|
|
var dayCell = document.createElement("td");
|
|
dayCell.className = "day";
|
|
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;
|
|
iconCell.appendChild(icon);
|
|
|
|
var degreeLabel = "";
|
|
if (this.config.units === "metric" || this.config.units === "imperial") {
|
|
degreeLabel += "°";
|
|
}
|
|
if(this.config.scale) {
|
|
switch(this.config.units) {
|
|
case "metric":
|
|
degreeLabel += "C";
|
|
break;
|
|
case "imperial":
|
|
degreeLabel += "F";
|
|
break;
|
|
case "default":
|
|
degreeLabel = "K";
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (this.config.decimalSymbol === "" || this.config.decimalSymbol === " ") {
|
|
this.config.decimalSymbol = ".";
|
|
}
|
|
|
|
var maxTempCell = document.createElement("td");
|
|
maxTempCell.innerHTML = forecast.maxTemp.replace(".", this.config.decimalSymbol) + degreeLabel;
|
|
maxTempCell.className = "align-right bright max-temp";
|
|
row.appendChild(maxTempCell);
|
|
|
|
var minTempCell = document.createElement("td");
|
|
minTempCell.innerHTML = forecast.minTemp.replace(".", this.config.decimalSymbol) + degreeLabel;
|
|
minTempCell.className = "align-right min-temp";
|
|
row.appendChild(minTempCell);
|
|
|
|
if (this.config.showRainAmount) {
|
|
var rainCell = document.createElement("td");
|
|
if (isNaN(forecast.rain)) {
|
|
rainCell.innerHTML = "";
|
|
} else {
|
|
if(config.units !== "imperial") {
|
|
rainCell.innerHTML = parseFloat(forecast.rain).toFixed(1) + " mm";
|
|
} else {
|
|
rainCell.innerHTML = (parseFloat(forecast.rain) / 25.4).toFixed(2) + " in";
|
|
}
|
|
}
|
|
rainCell.className = "align-right bright rain";
|
|
row.appendChild(rainCell);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
/* updateWeather(compliments)
|
|
* Requests new data from openweather.org.
|
|
* Calls processWeather on succesfull response.
|
|
*/
|
|
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();
|
|
var self = this;
|
|
var retry = true;
|
|
|
|
var weatherRequest = new XMLHttpRequest();
|
|
weatherRequest.open("GET", url, true);
|
|
weatherRequest.onreadystatechange = function() {
|
|
if (this.readyState === 4) {
|
|
if (this.status === 200) {
|
|
self.processWeather(JSON.parse(this.response));
|
|
} else if (this.status === 401) {
|
|
self.updateDom(self.config.animationSpeed);
|
|
|
|
if (self.config.forecastEndpoint == "forecast/daily") {
|
|
self.config.forecastEndpoint = "forecast";
|
|
Log.warn(self.name + ": Your AppID does not support long term forecasts. Switching to fallback endpoint.");
|
|
}
|
|
|
|
retry = true;
|
|
} else {
|
|
Log.error(self.name + ": Could not load weather.");
|
|
}
|
|
|
|
if (retry) {
|
|
self.scheduleUpdate((self.loaded) ? -1 : self.config.retryDelay);
|
|
}
|
|
}
|
|
};
|
|
weatherRequest.send();
|
|
},
|
|
|
|
/* getParams(compliments)
|
|
* Generates an url with api parameters based on the config.
|
|
*
|
|
* return String - URL params.
|
|
*/
|
|
getParams: function() {
|
|
var params = "?";
|
|
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=" + this.config.units;
|
|
params += "&lang=" + this.config.lang;
|
|
params += "&APPID=" + this.config.appid;
|
|
|
|
return params;
|
|
},
|
|
|
|
/*
|
|
* parserDataWeather(data)
|
|
*
|
|
* Use the parse to keep the same struct between daily and forecast Endpoint
|
|
* from Openweather
|
|
*
|
|
*/
|
|
parserDataWeather: function(data) {
|
|
if (data.hasOwnProperty("main")) {
|
|
data["temp"] = {"min": data.main.temp_min, "max": data.main.temp_max}
|
|
}
|
|
return data;
|
|
},
|
|
|
|
/* processWeather(data)
|
|
* Uses the received data to set the various values.
|
|
*
|
|
* argument data object - Weather information received form openweather.org.
|
|
*/
|
|
processWeather: function(data) {
|
|
this.fetchedLocationName = data.city.name + ", " + data.city.country;
|
|
|
|
this.forecast = [];
|
|
var lastDay = null;
|
|
var forecastData = {}
|
|
|
|
for (var i = 0, count = data.list.length; i < count; i++) {
|
|
|
|
var forecast = data.list[i];
|
|
this.parserDataWeather(forecast); // hack issue #1017
|
|
|
|
var day;
|
|
var hour;
|
|
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");
|
|
}
|
|
|
|
if (day !== lastDay) {
|
|
var forecastData = {
|
|
day: day,
|
|
icon: this.config.iconTable[forecast.weather[0].icon],
|
|
maxTemp: this.roundValue(forecast.temp.max),
|
|
minTemp: this.roundValue(forecast.temp.min),
|
|
rain: forecast.rain
|
|
};
|
|
|
|
this.forecast.push(forecastData);
|
|
lastDay = day;
|
|
|
|
// Stop processing when maxNumberOfDays is reached
|
|
if (this.forecast.length === this.config.maxNumberOfDays) {
|
|
break;
|
|
}
|
|
} 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];
|
|
}
|
|
}
|
|
}
|
|
|
|
//Log.log(this.forecast);
|
|
this.show(this.config.animationSpeed, {lockString:this.identifier});
|
|
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.
|
|
*/
|
|
scheduleUpdate: function(delay) {
|
|
var nextLoad = this.config.updateInterval;
|
|
if (typeof delay !== "undefined" && delay >= 0) {
|
|
nextLoad = delay;
|
|
}
|
|
|
|
var self = this;
|
|
clearTimeout(this.updateTimer);
|
|
this.updateTimer = setTimeout(function() {
|
|
self.updateWeather();
|
|
}, nextLoad);
|
|
},
|
|
|
|
/* ms2Beaufort(ms)
|
|
* Converts m2 to beaufort (windspeed).
|
|
*
|
|
* see:
|
|
* http://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.
|
|
*/
|
|
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.
|
|
*/
|
|
roundValue: function(temperature) {
|
|
var decimals = this.config.roundTemp ? 0 : 1;
|
|
return parseFloat(temperature).toFixed(decimals);
|
|
}
|
|
});
|