/* eslint-disable */ /* Magic Mirror * Module: WeatherForecast * * By Michael Teeuw https://michaelteeuw.nl * MIT Licensed. * * This module is deprecated. Any additional feature will no longer be merged. */ Module.register("weatherforecast", { // Default module config. defaults: { location: false, locationID: false, lat: false, lon: 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", excludes: false, 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 calendar 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 your 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 === "" || this.config.appid === "YOUR_OPENWEATHER_API_KEY") { wrapper.innerHTML = "Please set the correct openweather appid 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).replace(".", this.config.decimalSymbol) + " mm"; } else { rainCell.innerHTML = (parseFloat(forecast.rain) / 25.4).toFixed(2).replace(".", this.config.decimalSymbol) + " 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) { if (this.data.header) return this.data.header + " " + this.fetchedLocationName; else return this.fetchedLocationName; } return this.data.header ? 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 successful 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.lat && this.config.lon) { params += "lat=" + this.config.lat + "&lon=" + this.config.lon; } 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; } let numberOfDays; if (this.config.forecastEndpoint === "forecast") { numberOfDays = this.config.maxNumberOfDays < 1 || this.config.maxNumberOfDays > 5 ? 5 : this.config.maxNumberOfDays; // don't get forecasts for the next day, as it would not represent the whole day numberOfDays = numberOfDays * 8 - (Math.round(new Date().getHours() / 3) % 8); } else { numberOfDays = this.config.maxNumberOfDays < 1 || this.config.maxNumberOfDays > 17 ? 7 : this.config.maxNumberOfDays; } params += "&cnt=" + numberOfDays; params += "&exclude=" + this.config.excludes; 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.org * */ 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, momenttz) { let mom = momenttz ? momenttz : moment; // Exception last. // Forcast16 (paid) API endpoint provides this data. Onecall endpoint // does not. if (data.city) { this.fetchedLocationName = data.city.name + ", " + data.city.country; } else if (this.config.location) { this.fetchedLocationName = this.config.location; } else { this.fetchedLocationName = "Unknown"; } this.forecast = []; var lastDay = null; var forecastData = {}; var dayStarts = 8; var dayEnds = 17; if (data.city && data.city.sunrise && data.city.sunset) { dayStarts = new Date(mom.unix(data.city.sunrise).locale("en").format("YYYY/MM/DD HH:mm:ss")).getHours(); dayEnds = new Date(mom.unix(data.city.sunset).locale("en").format("YYYY/MM/DD HH:mm:ss")).getHours(); } // Handle different structs between forecast16 and onecall endpoints var forecastList = null; if (data.list) { forecastList = data.list; } else if (data.daily) { forecastList = data.daily; } else { Log.error("Unexpected forecast data"); return undefined; } for (var i = 0, count = forecastList.length; i < count; i++) { var forecast = forecastList[i]; forecast = this.parserDataWeather(forecast); // hack issue #1017 var day; var hour; if (forecast.dt_txt) { day = mom(forecast.dt_txt, "YYYY-MM-DD hh:mm:ss").format("ddd"); hour = new Date(mom(forecast.dt_txt).locale("en").format("YYYY-MM-DD HH:mm:ss")).getHours(); } else { day = mom(forecast.dt, "X").format("ddd"); hour = new Date(mom(forecast.dt, "X")).getHours(); } if (day !== lastDay) { forecastData = { 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, forecastList, mom) }; 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 > dayStarts && hour < dayEnds) { 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: * 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. */ 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; var roundValue = parseFloat(temperature).toFixed(decimals); return roundValue === "-0" ? 0 : roundValue; }, /* 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, momenttz) { let mom = momenttz ? momenttz : moment; // Exception last. //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 var checkDateTime = forecast.dt_txt ? mom(forecast.dt_txt, "YYYY-MM-DD hh:mm:ss") : mom(forecast.dt, "X"); var daysForecasts = allForecasts.filter(function (item) { var itemDateTime = item.dt_txt ? mom(item.dt_txt, "YYYY-MM-DD hh:mm:ss") : mom(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); } });