/* global Module */ /* Magic Mirror * Module: WeatherForecast * * By Michael Teeuw http://michaelteeuw.nl * MIT Licensed. */ Module.register("weatherforecast",{ // Default module config. defaults: { location: "", locationID: "", appid: "", units: config.units, maxNumberOfDays: 7, 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. 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", 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" }, }, // 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 defaut modules are defined in the core translation files. // Therefor we can just return false. Otherwise we should have returned a dictionairy. // 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 appid in the config for module: " + this.name + "."; wrapper.className = "dimmed light small"; return wrapper; } if (this.config.location === "") { wrapper.innerHTML = "Please set the openweather location 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"); 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 = 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.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; }, /* updateWeather(compliments) * Requests new data from openweather.org. * Calls processWeather on succesfull response. */ updateWeather: function() { 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.config.appid = ""; self.updateDom(self.config.animationSpeed); Log.error(self.name + ": Incorrect APPID."); retry = false; } 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 { params += "q=" + this.config.location; } 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.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) }); } //Log.log(this.forecast); 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). * * 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. * * argument temperature number - Temperature. * * return number - Rounded Temperature. */ roundValue: function(temperature) { return parseFloat(temperature).toFixed(1); } });