|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518 |
- /* 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 <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).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);
- }
- });
|