diff --git a/modules/default/weather/README.md b/modules/default/weather/README.md new file mode 100644 index 00000000..325732cc --- /dev/null +++ b/modules/default/weather/README.md @@ -0,0 +1,17 @@ +# Weather Module + +This module is aimed to be the replacement for the current `currentweather` and `weatherforcast` modules. The module will be configurable to be used as a current weather view, or to show the forecast. This way the module can be used twice to fullfil both purposes. + +The biggest cange is the use of weather providers. This way we are not bound to one API source. And users can choose which API they want to use as their source. Initially the current OpenWeatherMap will be added as the first source, but more will follow soon. + +The module is in a very early stage, and needs a lot of work. It's API isn't set in stone, so keep that in mind when you want to contribute. + +## TODO + +- [ ] Add Current Weather View +- [ ] Add Weather Forecast View +- [ ] Add Forecast API Call +- [ ] Add all original configuration options +- [ ] Add more providers +- [ ] Finish thi Todo list +- [ ] Write the documentation \ No newline at end of file diff --git a/modules/default/weather/providers/openweathermap.js b/modules/default/weather/providers/openweathermap.js index 4f3651a6..c9e56d2c 100644 --- a/modules/default/weather/providers/openweathermap.js +++ b/modules/default/weather/providers/openweathermap.js @@ -1,4 +1,4 @@ -/* global WeatherProvider, WeatherDay */ +/* global WeatherProvider, WeatherObject */ /* Magic Mirror * Module: Weather @@ -34,7 +34,7 @@ WeatherProvider.register("openweathermap", { return; } - var currentWeather = this.generateWeatherDayFromCurrentWeather(data) + var currentWeather = this.generateWeatherObjectFromCurrentWeather(data) this.setCurrentWeather(currentWeather) }) .catch(function(request) { @@ -42,23 +42,47 @@ WeatherProvider.register("openweathermap", { }) }, + // Overwrite the fetchCurrentWeather method. + fetchWeatherForecast: function() { + + // I haven't yet implemented the real api call, so let's just generate some random data. + + var forecast = [] + var today = moment() + + for (var i = 0; i < 5; i++ ) { + var weatherObject = new WeatherObject() + + weatherObject.date = moment(today).add(i, "days") + weatherObject.minTemperature = Math.random() * 10 + 10 + weatherObject.maxTemperature = Math.random() * 15 + 10 + + forecast.push(weatherObject) + } + + this.setWeatherForecast(forecast) + }, + + /** OpenWeatherMap Specific Methods - These are not part of the default provider methods */ /* - * Generate a WeatherDay based on currentWeatherInformation + * Generate a WeatherObject based on currentWeatherInformation */ - generateWeatherDayFromCurrentWeather: function(currentWeatherData) { - var currentWeather = new WeatherDay() + generateWeatherObjectFromCurrentWeather: function(currentWeatherData) { + var currentWeather = new WeatherObject() - currentWeather.humidity = parseFloat(currentWeatherData.main.humidity) - currentWeather.temperature = parseFloat(currentWeatherData.main.temp) - currentWeather.windSpeed = parseFloat(currentWeatherData.wind.speed) - currentWeather.windDirection = currentWeatherData.wind.deg - currentWeather.weatherType = this.convertWeatherType(currentWeatherData.weather[0].icon) - currentWeather.sunrise = new Date(currentWeatherData.sys.sunrise * 1000) - currentWeather.sunset = new Date(currentWeatherData.sys.sunset * 1000) + currentWeather.date = new Date + + currentWeather.humidity = currentWeatherData.main.humidity ? parseFloat(currentWeatherData.main.humidity) : null + currentWeather.temperature = currentWeatherData.main.temp ? parseFloat(currentWeatherData.main.temp) : null + currentWeather.windSpeed = currentWeatherData.wind.speed ? parseFloat(currentWeatherData.wind.speed) : null + currentWeather.windDirection = currentWeatherData.wind.deg ? currentWeatherData.wind.deg : null + currentWeather.weatherType = currentWeatherData.weather[0].icon ? this.convertWeatherType(currentWeatherData.weather[0].icon) : null + currentWeather.sunrise = currentWeatherData.sys.sunrise ? new Date(currentWeatherData.sys.sunrise * 1000) : null + currentWeather.sunset = currentWeatherData.sys.sunset ? new Date(currentWeatherData.sys.sunset * 1000) : null return currentWeather }, diff --git a/modules/default/weather/weather.css b/modules/default/weather/weather.css new file mode 100644 index 00000000..febc777c --- /dev/null +++ b/modules/default/weather/weather.css @@ -0,0 +1,46 @@ +.weather .weathericon, +.weather .fa-home { + font-size: 75%; + line-height: 65px; + display: inline-block; + -ms-transform: translate(0, -3px); /* IE 9 */ + -webkit-transform: translate(0, -3px); /* Safari */ + transform: translate(0, -3px); +} + +.weather .humidityIcon { + padding-right: 4px; +} + +.weather .humidity-padding { + padding-bottom: 6px; +} + + +.weather .day { + padding-left: 0; + padding-right: 25px; +} + +.weather .weather-icon { + padding-right: 30px; + text-align: center; +} + +.weather .min-temp { + padding-left: 20px; + padding-right: 0; +} + +.weather .rain { + padding-left: 20px; + padding-right: 0; +} + +.weather tr.colored .min-temp { + color: #BCDDFF; +} + +.weather tr.colored .max-temp { + color: #FF8E99; +} diff --git a/modules/default/weather/weather.js b/modules/default/weather/weather.js index 2be3ff0b..7f33ffa8 100644 --- a/modules/default/weather/weather.js +++ b/modules/default/weather/weather.js @@ -11,18 +11,26 @@ Module.register("weather",{ // Default module config. defaults: { - foo: "bar", - weatherProvider: "openweathermap" + updateInterval: 10 * 60 * 1000, + weatherProvider: "openweathermap", + units: config.units, + roundTemp: false, + displayType: "full" //current, forecast, full }, // Module properties. weatherProvider: null, + // Define required scripts. + getStyles: function() { + return ["weather-icons.css", "weather.css"]; + }, + // Return the scripts that are nessecery for the weather module. getScripts: function () { var scripts = [ "weatherprovider.js", - "weatherday.js" + "weatherobject.js" ]; // Add the provider file to the required scripts. @@ -39,24 +47,147 @@ Module.register("weather",{ // Let the weather provider know we are starting. this.weatherProvider.start() - // Fetch the current weather. This is something we need to schedule. - this.weatherProvider.fetchCurrentWeather() + // Schedule the first update. + this.scheduleUpdate(0) }, // Generate the dom. This is now pretty simple for debugging. getDom: function() { - var wrapper = document.createElement("div") - - wrapper.innerHTML += "Name: " + this.weatherProvider.providerName + "
" - wrapper.innerHTML += JSON.stringify(this.weatherProvider.currentWeather()) - - return wrapper + switch (this.config.displayType) { + case "current": + return this.currentWeatherView() + break; + case "forecast": + return this.weatherForecastView() + break; + default: + return this.fullWeatherView() + break; + } }, // What to do when the weather provider has new information available? updateAvailable: function() { Log.log("New weather information available.") console.info(this.weatherProvider.currentWeather()) - this.updateDom(0); + this.updateDom(0) + this.scheduleUpdate() + }, + + scheduleUpdate: function(delay = null) { + var nextLoad = this.config.updateInterval; + if (delay !== null && delay >= 0) { + nextLoad = delay; + } + + setTimeout(() => { + switch (this.config.displayType) { + case "current": + this.weatherProvider.fetchCurrentWeather() + break; + case "forecast": + this.weatherProvider.fetchWeatherForecast() + break; + default: + this.weatherProvider.fetchCurrentWeather() + this.weatherProvider.fetchWeatherForecast() + break; + } + }, nextLoad); + }, + + /* Views */ + + // Generate the current weather view + currentWeatherView: function () { + + var currentWeather = this.weatherProvider.currentWeather() + var wrapper = document.createElement("div") + + if (currentWeather === null) { + return wrapper + } + + // Detail bar. + + var detailBar = document.createElement("div") + + this.addValueToWrapper(detailBar, null, "wi wi-strong-wind dimmed", "span", true) // Wind Icon + this.addValueToWrapper(detailBar, currentWeather.windSpeed ? Math.round(currentWeather.windSpeed) : null) // WindSpeed + this.addValueToWrapper(detailBar, currentWeather.windDirection ? this.translate(currentWeather.cardinalWindDirection()) + "  " : " ", "", "sup") // WindDirection + + var now = new Date(); + var sunriseSunsetTime = (currentWeather.sunrise < now && currentWeather.sunset > now) ? currentWeather.sunset : currentWeather.sunrise + var sunriseSunsetIcon = (currentWeather.sunrise < now && currentWeather.sunset > now) ? "wi-sunset" : "wi-sunrise" + this.addValueToWrapper(detailBar, null, "wi dimmed " + sunriseSunsetIcon, "span", true) // Sunrise / Sunset Icon + this.addValueToWrapper(detailBar, moment(sunriseSunsetTime).format("HH:mm")) // Sunrise / Sunset Time + + detailBar.className = "normal medium" + wrapper.appendChild(detailBar) + + // Main info + + var mainInfo = document.createElement("div") + + this.addValueToWrapper(mainInfo, null, "weathericon wi wi-" + currentWeather.weatherType, "span", true) // Wind Icon + this.addValueToWrapper(mainInfo, currentWeather.temperature.toFixed(this.config.roundTemp ? 0 : 1) + "°", "bright" ) // WindSpeed + + mainInfo.className = "large light" + wrapper.appendChild(mainInfo) + + return wrapper + }, + + weatherForecastView: function() { + // This is just a dummy view to test it ... it needs A LOT of work :) + // Currently it outputs a div, it should be a table. + + var wrapper = document.createElement("div") + wrapper.className = "small" + + if (this.weatherProvider.weatherForecast() === null) { + return wrapper + } + + this.weatherProvider.weatherForecast().forEach((weatherObject) => { + var day = document.createElement("div") + + this.addValueToWrapper(day, moment(weatherObject.date).format("dd")) + this.addValueToWrapper(day, weatherObject.maxTemperature ? weatherObject.maxTemperature.toFixed(this.config.roundTemp ? 0 : 1) : null, "bright") + this.addValueToWrapper(day, weatherObject.minTemperature ? weatherObject.minTemperature.toFixed(this.config.roundTemp ? 0 : 1) : null, "dimmed") + + wrapper.appendChild(day) + }); + + return wrapper + }, + + fullWeatherView: function() { + var wrapper = document.createElement("div") + + wrapper.appendChild(this.currentWeatherView()) + wrapper.appendChild(this.weatherForecastView()) + + return wrapper + }, + + // A convenience function to add an element to a wrapper with a specific value and class. + addValueToWrapper: function(wrapper, value, classNames, element = "span", forceAdd = false, addSpacer = true) { + if (value === null && !forceAdd) { + return + } + + var valueWrapper = document.createElement(element) + valueWrapper.className = classNames + if (addSpacer) { + valueWrapper.innerHTML = " " + } + + if (value !== null) { + valueWrapper.innerHTML += value + } + + wrapper.appendChild(valueWrapper) } + }); diff --git a/modules/default/weather/weatherday.js b/modules/default/weather/weatherday.js deleted file mode 100644 index c8f63057..00000000 --- a/modules/default/weather/weatherday.js +++ /dev/null @@ -1,24 +0,0 @@ -/* global Class */ - -/* Magic Mirror - * Module: Weather - * - * By Michael Teeuw http://michaelteeuw.nl - * MIT Licensed. - * - * This class is the blueprint for a day which includes weather information. - */ - -// Currently this is focused on the information which is nessecery for the current weather. -// As soon as we start implementing the forecast, mode properties will be added. - -class WeatherDay { - constructor() { - this.windSpeed = null - this.windDirection = null - this.sunrise = null - this.sunset = null - this.temperature = null - this.weatherType = null - } -}; \ No newline at end of file diff --git a/modules/default/weather/weatherobject.js b/modules/default/weather/weatherobject.js new file mode 100644 index 00000000..7448d118 --- /dev/null +++ b/modules/default/weather/weatherobject.js @@ -0,0 +1,63 @@ +/* global Class */ + +/* Magic Mirror + * Module: Weather + * + * By Michael Teeuw http://michaelteeuw.nl + * MIT Licensed. + * + * This class is the blueprint for a day which includes weather information. + */ + +// Currently this is focused on the information which is nessecery for the current weather. +// As soon as we start implementing the forecast, mode properties will be added. + +class WeatherObject { + constructor() { + this.date = null + this.windSpeed = null + this.windDirection = null + this.sunrise = null + this.sunset = null + this.temperature = null + this.minTemperature = null, + this.maxTemperature = null, + this.weatherType = null + } + + cardinalWindDirection () { + if (this.windDirection>11.25 && this.windDirection<=33.75){ + return "NNE"; + } else if (this.windDirection > 33.75 && this.windDirection <= 56.25) { + return "NE"; + } else if (this.windDirection > 56.25 && this.windDirection <= 78.75) { + return "ENE"; + } else if (this.windDirection > 78.75 && this.windDirection <= 101.25) { + return "E"; + } else if (this.windDirection > 101.25 && this.windDirection <= 123.75) { + return "ESE"; + } else if (this.windDirection > 123.75 && this.windDirection <= 146.25) { + return "SE"; + } else if (this.windDirection > 146.25 && this.windDirection <= 168.75) { + return "SSE"; + } else if (this.windDirection > 168.75 && this.windDirection <= 191.25) { + return "S"; + } else if (this.windDirection > 191.25 && this.windDirection <= 213.75) { + return "SSW"; + } else if (this.windDirection > 213.75 && this.windDirection <= 236.25) { + return "SW"; + } else if (this.windDirection > 236.25 && this.windDirection <= 258.75) { + return "WSW"; + } else if (this.windDirection > 258.75 && this.windDirection <= 281.25) { + return "W"; + } else if (this.windDirection > 281.25 && this.windDirection <= 303.75) { + return "WNW"; + } else if (this.windDirection > 303.75 && this.windDirection <= 326.25) { + return "NW"; + } else if (this.windDirection > 326.25 && this.windDirection <= 348.75) { + return "NNW"; + } else { + return "N"; + } + } +}; \ No newline at end of file diff --git a/modules/default/weather/weatherprovider.js b/modules/default/weather/weatherprovider.js index 08489aeb..f31b22b7 100644 --- a/modules/default/weather/weatherprovider.js +++ b/modules/default/weather/weatherprovider.js @@ -19,7 +19,7 @@ var WeatherProvider = Class.extend({ // The following properties have accestor methods. // Try to not access them directly. - currentWeatherDay: null, + currentWeatherObject: null, weatherForecastArray: null, // The following properties will be set automaticly. @@ -57,13 +57,13 @@ var WeatherProvider = Class.extend({ // This method should start the API request to fetch the weather forecast. // This method should definetly be overwritten in the provider. - fetchWeatherForeCast: function() { - Log.warn("Weather provider: " + this.providerName + " does not subclass the fetchWeatherForeCast method.") + fetchWeatherForecast: function() { + Log.warn("Weather provider: " + this.providerName + " does not subclass the fetchWeatherForecast method.") }, // This returns a WeatherDay object for the current weather. currentWeather: function() { - return this.currentWeatherDay + return this.currentWeatherObject }, // This returns an array of WeatherDay objects for the weather forecast. @@ -71,10 +71,18 @@ var WeatherProvider = Class.extend({ return this.weatherForecastArray }, - // Set the currentWeather and notify the delegate that new information is availabe. - setCurrentWeather: function(currentWeatherDay) { + // Set the currentWeather and notify the delegate that new information is available. + setCurrentWeather: function(currentWeatherObject) { // We should check here if we are passing a WeatherDay - this.currentWeatherDay = currentWeatherDay + this.currentWeatherObject = currentWeatherObject + + this.updateAvailable() + }, + + // Set the weatherForecastArray and notify the delegate that new information is available. + setWeatherForecast: function(weatherForecastArray) { + // We should check here if we are passing a WeatherDay + this.weatherForecastArray = weatherForecastArray this.updateAvailable() },