mirror of
				https://github.com/MichMich/MagicMirror.git
				synced 2025-11-04 05:15:42 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			368 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			368 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* global Module */
 | 
						|
 | 
						|
/* Magic Mirror
 | 
						|
 * Module: WeatherForecast
 | 
						|
 *
 | 
						|
 * By Michael Teeuw http://michaelteeuw.nl
 | 
						|
 * MIT Licensed.
 | 
						|
 */
 | 
						|
 | 
						|
Module.register("weatherforecast",{
 | 
						|
 | 
						|
	// Default module config.
 | 
						|
	defaults: {
 | 
						|
		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,
 | 
						|
		fade: true,
 | 
						|
		fadePoint: 0.25, // Start on 1/4th of the list.
 | 
						|
		colored: false,
 | 
						|
 | 
						|
		initialLoadDelay: 2500, // 2.5 seconds delay. This delay is used to keep the OpenWeather API happy.
 | 
						|
		retryDelay: 2500,
 | 
						|
 | 
						|
		apiVersion: "2.5",
 | 
						|
		apiBase: "http://api.openweathermap.org/data/",
 | 
						|
		forecastEndpoint: "forecast/daily",
 | 
						|
 | 
						|
		appendLocationNameToHeader: true,
 | 
						|
		calendarClass: "calendar",
 | 
						|
 | 
						|
		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.scheduleUpdate(this.config.initialLoadDelay);
 | 
						|
 | 
						|
		this.updateTimer = null;
 | 
						|
 | 
						|
	},
 | 
						|
 | 
						|
	// 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 = "small";
 | 
						|
 | 
						|
		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 maxTempCell = document.createElement("td");
 | 
						|
			maxTempCell.innerHTML = forecast.maxTemp;
 | 
						|
			maxTempCell.className = "align-right bright max-temp";
 | 
						|
			row.appendChild(maxTempCell);
 | 
						|
 | 
						|
			var minTempCell = document.createElement("td");
 | 
						|
			minTempCell.innerHTML = forecast.minTemp;
 | 
						|
			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 = forecast.rain + " 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);
 | 
						|
 | 
						|
					Log.error(self.name + ": Incorrect APPID.");
 | 
						|
					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;
 | 
						|
		/*
 | 
						|
		 * Submit a specific number of days to forecast, between 1 to 16 days.
 | 
						|
		 * The OpenWeatherMap API properly handles values outside of the 1 - 16 range and returns 7 days by default.
 | 
						|
		 * This is simply being pedantic and doing it ourselves.
 | 
						|
		 */
 | 
						|
		params += "&cnt=" + (((this.config.maxNumberOfDays < 1) || (this.config.maxNumberOfDays > 16)) ? 7 : this.config.maxNumberOfDays);
 | 
						|
		params += "&APPID=" + this.config.appid;
 | 
						|
 | 
						|
		return params;
 | 
						|
	},
 | 
						|
 | 
						|
	/* 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 = [];
 | 
						|
		for (var i = 0, count = data.list.length; i < count; i++) {
 | 
						|
 | 
						|
			var forecast = data.list[i];
 | 
						|
			this.forecast.push({
 | 
						|
 | 
						|
				day: moment(forecast.dt, "X").format("ddd"),
 | 
						|
				icon: this.config.iconTable[forecast.weather[0].icon],
 | 
						|
				maxTemp: this.roundValue(forecast.temp.max),
 | 
						|
				minTemp: this.roundValue(forecast.temp.min),
 | 
						|
				rain: this.roundValue(forecast.rain)
 | 
						|
 | 
						|
			});
 | 
						|
		}
 | 
						|
 | 
						|
		//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);
 | 
						|
	}
 | 
						|
});
 |